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