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