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",
|
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. |
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 ) -> None:
111 # Always return HTTP 200 to signal that the request got processed.
112 # The actual errors are passed in the JSON structure response.
113 super().__init__(200)
114 self.response_code = response_code
115 self.response_msg = response_msg
116
117
118 class InvalidInputError(ApiError):
119 def __init__(self, msg: str = "Input is not valid JSON object"):
120 super().__init__(communication.const.COM_STATUS_INPUT_ERROR, msg)
121
122
123 class _BaseApiV1Handler(BaseHandler):
124 """
125 Base handler for the REST API
126
127 Defines all common functions used by handlers, message body preprocessing,
128 and HTTP(S) settings.
129 """
130
131 scheduler: Scheduler
132 json: Optional[dict[str, Any]] = None
133 _auth_provider: ApiAuthProviderInterface
134
135 _real_user: AuthUser
136 _desired_user: DesiredUser
137
138 def initialize(
139 self,
140 api_auth_provider_factory: ApiAuthProviderFactoryInterface,
141 scheduler: Scheduler,
142 ) -> None:
143 super().initialize()
144 self._auth_provider = api_auth_provider_factory.create(self)
145 self.scheduler = scheduler
146
147 def _preprocess_json(self) -> None:
148 try:
149 self.json = json.loads(self.request.body)
150 except json.JSONDecodeError as e:
151 raise InvalidInputError() from e
152
153 async def prepare(self) -> None:
154 self.add_header("Content-Type", "application/json")
155
156 # Authentication
157 try:
158 self._real_user = await self._auth_provider.auth_user()
159 except NotAuthorizedException as e:
160 raise ApiError(
161 response_code=communication.const.COM_STATUS_NOT_AUTHORIZED,
162 response_msg="",
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 # Always return HTTP 200 to signal that the request got processed.
178 # The actual errors are passed in the JSON structure response.
179 del status_code
180 response = communication.dto.InternalCommunicationResultDto(
181 status=communication.const.COM_STATUS_EXCEPTION,
182 status_msg=None,
183 report_list=[],
184 data=None,
185 )
186
187 if "exc_info" in kwargs:
188 _, exc, _ = kwargs["exc_info"]
189 if isinstance(exc, ApiError):
190 if (
191 exc.response_code
192 == communication.const.COM_STATUS_NOT_AUTHORIZED
193 ):
194 # Mimic original ruby daemon behavior. Authentication was
195 # done in `before` filter which run before the actual URL
196 # handler code. Thus specific APIv1 formatted message could
197 # not be produced.
198 # Unlike original ruby daemon, we do not return HTTP 401.
199 # Doing so would causes web UI frontend to display a login
200 # screen, which is not correct: It makes the user login to
201 # a web UI backend. It does not fix missing / bad token to
202 # an actual cluster / cluster node.
203 self.finish(json.dumps({"notauthorized": "true"}))
204 return
205 response = communication.dto.InternalCommunicationResultDto(
206 status=exc.response_code,
207 status_msg=exc.response_msg,
208 report_list=[],
209 data=None,
210 )
211
212 self.send_response(response)
213
214 async def process_request(
215 self, cmd: str
216 ) -> communication.dto.InternalCommunicationResultDto:
217 if cmd not in API_V1_MAP:
218 raise ApiError(
219 communication.const.COM_STATUS_UNKNOWN_CMD,
220 f"Unknown command '{cmd}'",
221 )
222 if self.json is None:
223 raise InvalidInputError()
224 command_dto = CommandDto(
225 command_name=API_V1_MAP[cmd],
226 params=self.json,
227 # the scheduler/executor handles whether the command is run with
228 # real_user permissions or the effective user is used
229 options=CommandOptionsDto(
230 effective_username=self._desired_user.username,
231 effective_groups=list(self._desired_user.groups)
232 if self._desired_user.groups
233 else None,
234 ),
235 )
236 task_ident = self.scheduler.new_task(
237 Command(command_dto, is_legacy_command=True), self._real_user
238 )
239
240 try:
241 task_result_dto = await self.scheduler.wait_for_task(
242 task_ident, self._real_user
243 )
244 except TaskNotFoundError as e:
245 raise ApiError(
246 communication.const.COM_STATUS_EXCEPTION,
247 "Internal server error",
248 ) from e
249 if (
250 task_result_dto.task_finish_type == types.TaskFinishType.FAIL
251 and task_result_dto.reports
252 and task_result_dto.reports[0].message.code
253 == reports.codes.NOT_AUTHORIZED
254 and not task_result_dto.reports[0].context
255 ):
256 raise ApiError(
257 communication.const.COM_STATUS_PERMISSION_DENIED,
258 "Permission denied",
259 )
260
261 status_map = {
262 types.TaskFinishType.SUCCESS: communication.const.COM_STATUS_SUCCESS,
263 types.TaskFinishType.FAIL: communication.const.COM_STATUS_ERROR,
264 }
265 return communication.dto.InternalCommunicationResultDto(
266 status=status_map.get(
267 task_result_dto.task_finish_type,
268 communication.const.COM_STATUS_EXCEPTION,
269 ),
270 status_msg=None,
271 report_list=task_result_dto.reports,
272 data=task_result_dto.result,
273 )
274
275
276 class ApiV1Handler(_BaseApiV1Handler):
277 async def post(self, cmd: str) -> None:
278 self.send_response(await self.process_request(cmd))
279
280 # TODO: test get method
281 async def get(self, cmd: str) -> None:
282 self.send_response(await self.process_request(cmd))
283
284
285 class LegacyApiV1Handler(_BaseApiV1Handler):
286 @staticmethod
287 def _get_cmd() -> str:
288 raise NotImplementedError()
289
290 def _preprocess_json(self) -> None:
291 try:
292 self.json = json.loads(self.get_argument("data_json", default=""))
293 except json.JSONDecodeError as e:
294 raise InvalidInputError() from e
295
296 def send_response(
297 self, response: communication.dto.InternalCommunicationResultDto
298 ) -> None:
299 result = to_dict(response)
300 result["report_list"] = [
301 dict(
302 severity=report.severity.level,
303 code=report.message.code,
304 info=report.message.payload,
305 forceable=report.severity.force_code,
306 report_text=report.message.message,
307 )
308 for report in response.report_list
309 ]
310 self.finish(json.dumps(result))
311
312 async def post(self) -> None:
313 self.send_response(await self.process_request(self._get_cmd()))
314
315 # TODO: test get method
316 async def get(self) -> None:
317 self.send_response(await self.process_request(self._get_cmd()))
318
319
320 class ClusterStatusLegacyHandler(LegacyApiV1Handler):
321 @staticmethod
322 def _get_cmd() -> str:
323 return "status-full-cluster-status-plaintext/v1"
324
325
326 class ClusterAddNodesLegacyHandler(LegacyApiV1Handler):
327 @staticmethod
328 def _get_cmd() -> str:
329 return "cluster-add-nodes/v1"
330
331
332 def get_routes(
333 api_auth_provider_factory: ApiAuthProviderFactoryInterface,
334 scheduler: Scheduler,
335 ) -> RoutesType:
336 params = dict(
337 api_auth_provider_factory=api_auth_provider_factory, scheduler=scheduler
338 )
339 return [
340 (
341 "/remote/cluster_status_plaintext",
342 ClusterStatusLegacyHandler,
343 params,
344 ),
345 ("/remote/cluster_add_nodes", ClusterAddNodesLegacyHandler, params),
346 (r"/api/v1/(.*)", ApiV1Handler, params),
347 ]
348