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