1 import json
2 from typing import Any, Mapping, Optional
3
4 from tornado.web import HTTPError
5
6 from pcs.common import communication, reports
7 from pcs.common.async_tasks import types
8 from pcs.common.async_tasks.dto import CommandDto, CommandOptionsDto
9 from pcs.common.interface.dto import to_dict
10 from pcs.daemon import log
11 from pcs.daemon.app.auth_provider import (
12 ApiAuthProviderFactoryInterface,
13 ApiAuthProviderInterface,
14 NotAuthorizedException,
15 )
16 from pcs.daemon.app.common import get_legacy_desired_user_from_request
17 from pcs.daemon.async_tasks.scheduler import Scheduler, TaskNotFoundError
18 from pcs.daemon.async_tasks.types import Command
19 from pcs.lib.auth.tools import DesiredUser
20 from pcs.lib.auth.types import AuthUser
21
22 from .common import BaseHandler, RoutesType
23
24 API_V1_MAP: Mapping[str, str] = {
25 "acl-create-role/v1": "acl.create_role",
26 "acl-remove-role/v1": "acl.remove_role",
27 "acl-assign-role-to-target/v1": "acl.assign_role_to_target",
28 "acl-assign-role-to-group/v1": "acl.assign_role_to_group",
29 "acl-unassign-role-from-target/v1": "acl.unassign_role_from_target",
30 "acl-unassign-role-from-group/v1": "acl.unassign_role_from_group",
31 "acl-create-target/v1": "acl.create_target",
32 "acl-create-group/v1": "acl.create_group",
33 "acl-remove-target/v1": "acl.remove_target",
34 "acl-remove-group/v1": "acl.remove_group",
35 "acl-add-permission/v1": "acl.add_permission",
36 "acl-remove-permission/v1": "acl.remove_permission",
37 "alert-create-alert/v1": "alert.create_alert",
38 "alert-update-alert/v1": "alert.update_alert",
39 "alert-remove-alert/v1": "alert.remove_alert",
40 "alert-add-recipient/v1": "alert.add_recipient",
41 "alert-update-recipient/v1": "alert.update_recipient",
42 "alert-remove-recipient/v1": "alert.remove_recipient",
43 "cfgsync-get-configs/v1": "pcs_cfgsync.get_configs",
44 "cib-element-description-get/v1": "cib.element_description_get",
45 "cib-element-description-set/v1": "cib.element_description_set",
46 "cluster-add-nodes/v1": "cluster.add_nodes",
47 "cluster-node-clear/v1": "cluster.node_clear",
48 "cluster-remove-nodes/v1": "cluster.remove_nodes",
49 "cluster-setup/v1": "cluster.setup",
50 "cluster-generate-cluster-uuid/v1": "cluster.generate_cluster_uuid",
51 "cluster-property-remove-name/v1": "cluster_property.remove_cluster_name",
52 "constraint-colocation-create-with-set/v1": "constraint.colocation.create_with_set",
53 "constraint-order-create-with-set/v1": "constraint.order.create_with_set",
54 "constraint-ticket-create-with-set/v1": "constraint.ticket.create_with_set",
55 "constraint-ticket-create/v1": "constraint.ticket.create",
56 "constraint-ticket-remove/v1": "constraint.ticket.remove",
57 "fencing-topology-add-level/v1": "fencing_topology.add_level",
58 "fencing-topology-remove-all-levels/v1": "fencing_topology.remove_all_levels",
59 "fencing-topology-remove-levels-by-params/v1": "fencing_topology.remove_levels_by_params",
60 "fencing-topology-verify/v1": "fencing_topology.verify",
61 "node-maintenance-unmaintenance/v1": "node.maintenance_unmaintenance_list",
62 "node-maintenance-unmaintenance-all/v1": "node.maintenance_unmaintenance_all",
63 "node-standby-unstandby/v1": "node.standby_unstandby_list",
64 "node-standby-unstandby-all/v1": "node.standby_unstandby_all",
65 "qdevice-client-net-import-certificate/v1": "qdevice.client_net_import_certificate",
66 "qdevice-qdevice-net-sign-certificate-request/v1": "qdevice.qdevice_net_sign_certificate_request",
67 # deprecated, use resource-agent-get-agent-metadata/v1 instead
68 "resource-agent-describe-agent/v1": "resource_agent.describe_agent",
69 "resource-agent-get-agents-list/v1": "resource_agent.get_agents_list",
70 "resource-agent-get-agent-metadata/v1": "resource_agent.get_agent_metadata",
71 "resource-agent-get-meta-attributes-metadata/v1": "resource_agent.get_meta_attributes_metadata",
72 # deprecated, use resource-agent-get-agents-list/v1 instead
73 "resource-agent-list-agents/v1": "resource_agent.list_agents",
74 "resource-agent-list-agents-for-standard-and-provider/v1": "resource_agent.list_agents_for_standard_and_provider",
75 "resource-agent-list-ocf-providers/v1": "resource_agent.list_ocf_providers",
76 "resource-agent-list-standards/v1": "resource_agent.list_standards",
77 "resource-ban/v1": "resource.ban",
78 "resource-create/v1": "resource.create",
79 "resource-create-as-clone/v1": "resource.create_as_clone",
80 "resource-create-in-group/v1": "resource.create_in_group",
81 "resource-disable/v1": "resource.disable",
82 "resource-disable-safe/v1": "resource.disable_safe",
83 "resource-disable-simulate/v1": "resource.disable_simulate",
84 "resource-enable/v1": "resource.enable",
|
(1) Event Sigma main event: |
A secret, such as a password, cryptographic key, or token is stored in plaintext directly in the source code, in an application's properties, or configuration file. Users with access to the secret may then use the secret to access resources that they otherwise would not have access to. Secret type: Secret (generic). |
|
(2) Event remediation: |
Avoid setting sensitive configuration values as string literals. Instead, these values should be set using variables with the sensitive data loaded from an encrypted file or a secret store. |
85 "resource-get-cibsecrets/v1": "resource.get_cibsecrets",
86 "resource-group-add/v1": "resource.group_add",
87 "resource-manage/v1": "resource.manage",
88 "resource-move/v1": "resource.move",
89 "resource-move-autoclean/v1": "resource.move_autoclean",
90 "resource-unmanage/v1": "resource.unmanage",
91 "resource-unmove-unban/v1": "resource.unmove_unban",
92 "sbd-disable-sbd/v1": "sbd.disable_sbd",
93 "sbd-enable-sbd/v1": "sbd.enable_sbd",
94 "scsi-unfence-node/v2": "scsi.unfence_node",
95 "scsi-unfence-node-mpath/v1": "scsi.unfence_node_mpath",
96 "status-full-cluster-status-plaintext/v1": "status.full_cluster_status_plaintext",
97 # deprecated, use resource-agent-get-agent-metadata/v1 instead
98 "stonith-agent-describe-agent/v1": "stonith_agent.describe_agent",
99 # deprecated, use resource-agent-get-agents-list/v1 instead
100 "stonith-agent-list-agents/v1": "stonith_agent.list_agents",
101 "stonith-create/v1": "stonith.create",
102 }
103
104
105 class ApiError(HTTPError):
106 def __init__(
107 self,
108 response_code: communication.types.CommunicationResultStatus,
109 response_msg: str,
110 http_code: int = 200,
111 ) -> None:
112 super().__init__(http_code)
113 self.response_code = response_code
114 self.response_msg = response_msg
115
116
117 class InvalidInputError(ApiError):
118 def __init__(self, msg: str = "Input is not valid JSON object"):
119 super().__init__(communication.const.COM_STATUS_INPUT_ERROR, msg)
120
121
122 class _BaseApiV1Handler(BaseHandler):
123 """
124 Base handler for the REST API
125
126 Defines all common functions used by handlers, message body preprocessing,
127 and HTTP(S) settings.
128 """
129
130 scheduler: Scheduler
131 json: Optional[dict[str, Any]] = None
132 _auth_provider: ApiAuthProviderInterface
133
134 _real_user: AuthUser
135 _desired_user: DesiredUser
136
137 def initialize(
138 self,
139 api_auth_provider_factory: ApiAuthProviderFactoryInterface,
140 scheduler: Scheduler,
141 ) -> None:
142 super().initialize()
143 self._auth_provider = api_auth_provider_factory.create(self)
144 self.scheduler = scheduler
145
146 def _preprocess_json(self) -> None:
147 try:
148 self.json = json.loads(self.request.body)
149 except json.JSONDecodeError as e:
150 raise InvalidInputError() from e
151
152 async def prepare(self) -> None:
153 self.add_header("Content-Type", "application/json")
154
155 # Authentication
156 try:
157 self._real_user = await self._auth_provider.auth_user()
158 except NotAuthorizedException as e:
159 raise ApiError(
160 response_code=communication.const.COM_STATUS_NOT_AUTHORIZED,
161 response_msg="",
162 http_code=401,
163 ) from e
164 self._desired_user = get_legacy_desired_user_from_request(
165 self, log.pcsd
166 )
167
168 # JSON preprocessing
169 self._preprocess_json()
170
171 def send_response(
172 self, response: communication.dto.InternalCommunicationResultDto
173 ) -> None:
174 self.finish(json.dumps(to_dict(response)))
175
176 def write_error(self, status_code: int, **kwargs: Any) -> None:
177 del status_code
178 response = communication.dto.InternalCommunicationResultDto(
179 status=communication.const.COM_STATUS_EXCEPTION,
180 status_msg=None,
181 report_list=[],
182 data=None,
183 )
184
185 if "exc_info" in kwargs:
186 _, exc, _ = kwargs["exc_info"]
187 if isinstance(exc, ApiError):
188 if (
189 exc.response_code
190 == communication.const.COM_STATUS_NOT_AUTHORIZED
191 ):
192 self.finish(json.dumps({"notauthorized": "true"}))
193 return
194 response = communication.dto.InternalCommunicationResultDto(
195 status=exc.response_code,
196 status_msg=exc.response_msg,
197 report_list=[],
198 data=None,
199 )
200
201 self.send_response(response)
202
203 async def process_request(
204 self, cmd: str
205 ) -> communication.dto.InternalCommunicationResultDto:
206 if cmd not in API_V1_MAP:
207 raise ApiError(
208 communication.const.COM_STATUS_UNKNOWN_CMD,
209 f"Unknown command '{cmd}'",
210 )
211 if self.json is None:
212 raise InvalidInputError()
213 command_dto = CommandDto(
214 command_name=API_V1_MAP[cmd],
215 params=self.json,
216 # the scheduler/executor handles whether the command is run with
217 # real_user permissions or the effective user is used
218 options=CommandOptionsDto(
219 effective_username=self._desired_user.username,
220 effective_groups=list(self._desired_user.groups)
221 if self._desired_user.groups
222 else None,
223 ),
224 )
225 task_ident = self.scheduler.new_task(
226 Command(command_dto, is_legacy_command=True), self._real_user
227 )
228
229 try:
230 task_result_dto = await self.scheduler.wait_for_task(
231 task_ident, self._real_user
232 )
233 except TaskNotFoundError as e:
234 raise ApiError(
235 communication.const.COM_STATUS_EXCEPTION,
236 "Internal server error",
237 ) from e
238 if (
239 task_result_dto.task_finish_type == types.TaskFinishType.FAIL
240 and task_result_dto.reports
241 and task_result_dto.reports[0].message.code
242 == reports.codes.NOT_AUTHORIZED
243 and not task_result_dto.reports[0].context
244 ):
245 raise ApiError(
246 communication.const.COM_STATUS_NOT_AUTHORIZED, "Not authorized"
247 )
248
249 status_map = {
250 types.TaskFinishType.SUCCESS: communication.const.COM_STATUS_SUCCESS,
251 types.TaskFinishType.FAIL: communication.const.COM_STATUS_ERROR,
252 }
253 return communication.dto.InternalCommunicationResultDto(
254 status=status_map.get(
255 task_result_dto.task_finish_type,
256 communication.const.COM_STATUS_EXCEPTION,
257 ),
258 status_msg=None,
259 report_list=task_result_dto.reports,
260 data=task_result_dto.result,
261 )
262
263
264 class ApiV1Handler(_BaseApiV1Handler):
265 async def post(self, cmd: str) -> None:
266 self.send_response(await self.process_request(cmd))
267
268 # TODO: test get method
269 async def get(self, cmd: str) -> None:
270 self.send_response(await self.process_request(cmd))
271
272
273 class LegacyApiV1Handler(_BaseApiV1Handler):
274 @staticmethod
275 def _get_cmd() -> str:
276 raise NotImplementedError()
277
278 def _preprocess_json(self) -> None:
279 try:
280 self.json = json.loads(self.get_argument("data_json", default=""))
281 except json.JSONDecodeError as e:
282 raise InvalidInputError() from e
283
284 def send_response(
285 self, response: communication.dto.InternalCommunicationResultDto
286 ) -> None:
287 result = to_dict(response)
288 result["report_list"] = [
289 dict(
290 severity=report.severity.level,
291 code=report.message.code,
292 info=report.message.payload,
293 forceable=report.severity.force_code,
294 report_text=report.message.message,
295 )
296 for report in response.report_list
297 ]
298 self.finish(json.dumps(result))
299
300 async def post(self) -> None:
301 self.send_response(await self.process_request(self._get_cmd()))
302
303 # TODO: test get method
304 async def get(self) -> None:
305 self.send_response(await self.process_request(self._get_cmd()))
306
307
308 class ClusterStatusLegacyHandler(LegacyApiV1Handler):
309 @staticmethod
310 def _get_cmd() -> str:
311 return "status-full-cluster-status-plaintext/v1"
312
313
314 class ClusterAddNodesLegacyHandler(LegacyApiV1Handler):
315 @staticmethod
316 def _get_cmd() -> str:
317 return "cluster-add-nodes/v1"
318
319
320 def get_routes(
321 api_auth_provider_factory: ApiAuthProviderFactoryInterface,
322 scheduler: Scheduler,
323 ) -> RoutesType:
324 params = dict(
325 api_auth_provider_factory=api_auth_provider_factory, scheduler=scheduler
326 )
327 return [
328 (
329 "/remote/cluster_status_plaintext",
330 ClusterStatusLegacyHandler,
331 params,
332 ),
333 ("/remote/cluster_add_nodes", ClusterAddNodesLegacyHandler, params),
334 (r"/api/v1/(.*)", ApiV1Handler, params),
335 ]
336