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