Exemplo n.º 1
0
    def page(self) -> CBORPageResult:
        assert user.id is not None

        if not is_two_factor_login_enabled(user.id):
            raise MKGeneralException(
                _("Two-factor authentication not enabled"))

        data: dict[str, object] = cbor.decode(request.get_data())
        credential_id = data["credentialId"]
        client_data = ClientData(data["clientDataJSON"])
        auth_data = AuthenticatorData(data["authenticatorData"])
        signature = data["signature"]
        logger.debug("ClientData: %r", client_data)
        logger.debug("AuthenticatorData: %r", auth_data)

        make_fido2_server().authenticate_complete(
            session.session_info.webauthn_action_state,
            [
                AttestedCredentialData.unpack_from(v["credential_data"])[0]
                for v in load_two_factor_credentials(user.id)
                ["webauthn_credentials"].values()
            ],
            credential_id,
            client_data,
            auth_data,
            signature,
        )
        session.session_info.webauthn_action_state = None
        set_two_factor_completed()
        return {"status": "OK"}
Exemplo n.º 2
0
    def page(self) -> CBORPageResult:
        assert user.id is not None
        user.need_permission("general.manage_2fa")

        raw_data = request.get_data()
        logger.debug("Raw request: %r", raw_data)
        data: dict[str, object] = cbor.decode(raw_data)
        client_data = ClientData(data["clientDataJSON"])
        att_obj = AttestationObject(data["attestationObject"])
        logger.debug("Client data: %r", client_data)
        logger.debug("Attestation object: %r", att_obj)

        auth_data = make_fido2_server().register_complete(
            session.session_info.webauthn_action_state, client_data, att_obj
        )

        ident = auth_data.credential_data.credential_id.hex()
        credentials = load_two_factor_credentials(user.id, lock=True)

        if ident in credentials["webauthn_credentials"]:
            raise MKGeneralException(_("Your WebAuthn credetial is already in use"))

        credentials["webauthn_credentials"][ident] = WebAuthnCredential(
            {
                "credential_id": ident,
                "registered_at": int(time.time()),
                "alias": "",
                "credential_data": bytes(auth_data.credential_data),
            }
        )
        save_two_factor_credentials(user.id, credentials)

        flash(_("Registration successful"))
        return {"status": "OK"}
Exemplo n.º 3
0
        def _validating_wrapper(
                param: typing.Mapping[str, Any]) -> cmk_http.Response:
            # TODO: Better error messages, pointing to the location where variables are missing

            _params = dict(param)
            del param

            def _format_fields(_messages: Union[List, Dict]) -> str:
                if isinstance(_messages, list):
                    return ", ".join(_messages)
                if isinstance(_messages, dict):
                    return ", ".join(_messages.keys())
                return ""

            def _problem(exc_, status_code=400):
                if isinstance(exc_.messages, dict):
                    messages = exc_.messages
                else:
                    messages = {"exc": exc_.messages}
                return problem(
                    status=status_code,
                    title=http.client.responses[status_code],
                    detail=
                    f"These fields have problems: {_format_fields(exc_.messages)}",
                    fields=messages,
                )

            if self.method in ("post", "put") and request.get_data(cache=True):
                try:
                    self._is_expected_content_type(request.content_type)
                except ValueError as exc:
                    return problem(
                        status=415,
                        detail=str(exc),
                        title="Content type not valid for this endpoint.",
                    )

            try:
                if path_schema:
                    _params.update(path_schema().load(_params))
            except ValidationError as exc:
                return _problem(exc, status_code=404)

            try:
                if query_schema:
                    _params.update(query_schema().load(
                        _from_multi_dict(request.args)))

                if header_schema:
                    _params.update(header_schema().load(request.headers))

                if request_schema:
                    # Try to decode only when there is data. Decoding an empty string will fail.
                    if request.get_data(cache=True):
                        json_data = request.json or {}
                    else:
                        json_data = {}
                    _params["body"] = request_schema().load(json_data)
            except ValidationError as exc:
                return _problem(exc, status_code=400)

            if not request.accept_mimetypes:
                return problem(status=406,
                               title="Not Acceptable",
                               detail="Please specify an Accept Header.")
            if not request.accept_mimetypes.best_match([self.content_type]):
                return problem(
                    status=406,
                    title="Not Acceptable",
                    detail=
                    "Can not send a response with the content type specified in the 'Accept' Header."
                    f" Accept Header: {request.accept_mimetypes}."
                    f" Supported content types: [{self.content_type}]",
                )

            # make pylint happy
            assert callable(self.func)

            if self.tag_group == "Setup" and not active_config.wato_enabled:
                return problem(
                    status=403,
                    title="Forbidden: WATO is disabled",
                    detail="This endpoint is currently disabled via the "
                    "'Disable remote configuration' option in 'Distributed Monitoring'. "
                    "You may be able to query the central site.",
                )

            # TODO: Uncomment in later commit
            # if self.permissions_required is None:
            #     # Intentionally generate a crash report.
            #     raise PermissionError(f"Permissions need to be specified for {self}")

            try:
                response = self.func(_params)
            except ValidationError as exc:
                return _problem(exc, status_code=400)

            # We don't expect a permission to be triggered when an endpoint ran into an error.
            if response.status_code < 400:
                if (self.permissions_required is not None
                        and not self.permissions_required.validate(
                            list(self._used_permissions))):
                    # Intentionally generate a crash report.
                    raise PermissionError(
                        "There can be some causes for this error:\n"
                        "* a permission which was required (successfully) was not declared\n"
                        "* a permission which was declared (not optional) was not required\n"
                        "* No permission was required at all, although permission were declared\n"
                        f"Endpoint: {self}\n"
                        f"Required: {list(self._used_permissions)}\n"
                        f"Declared: {self.permissions_required}\n")

            if self.output_empty and response.status_code < 400 and response.data:
                return problem(
                    status=500,
                    title="Unexpected data was sent.",
                    detail=
                    f"Endpoint {self.operation_id}\nThis is a bug, please report.",
                    ext={"data_sent": str(response.data)},
                )

            if self.output_empty:
                response.content_type = ""

            if response.status_code not in self._expected_status_codes:
                return problem(
                    status=500,
                    title=
                    f"Unexpected status code returned: {response.status_code}",
                    detail=
                    f"Endpoint {self.operation_id}\nThis is a bug, please report.",
                    ext={"codes": self._expected_status_codes},
                )

            # We assume something has been modified and increase the config generation ID
            # by one. This is necessary to ensure a warning in the "Activate Changes" GUI
            # about there being new changes to activate can be given to the user.
            if (self.method != "get" and response.status_code < 300
                    and self.update_config_generation):
                # We assume no configuration change on GET and no configuration change on
                # non-ok responses.
                activate_changes_update_config_generation()
                if active_config.wato_use_git:
                    do_git_commit()

            if (self.content_type == "application/json"
                    and response.status_code < 300 and response_schema
                    and response.data):
                try:
                    data = json.loads(response.data.decode("utf-8"))
                except json.decoder.JSONDecodeError as exc:
                    return problem(
                        status=500,
                        title="Server was about to send invalid JSON data.",
                        detail="This is an error of the implementation.",
                        ext={
                            "errors": str(exc),
                            "orig": response.data,
                        },
                    )
                try:
                    outbound = response_schema().dump(data)
                except ValidationError as exc:
                    return problem(
                        status=500,
                        title="Server was about to send an invalid response.",
                        detail="This is an error of the implementation.",
                        ext={
                            "errors": exc.messages,
                            "orig": data,
                        },
                    )

                if self.convert_response:
                    response.set_data(json.dumps(outbound))

            response.freeze()
            return response
Exemplo n.º 4
0
        def _validating_wrapper(param):
            # TODO: Better error messages, pointing to the location where variables are missing

            def _format_fields(_messages: Union[List, Dict]) -> str:
                if isinstance(_messages, list):
                    return ', '.join(_messages)
                if isinstance(_messages, dict):
                    return ', '.join(_messages.keys())
                return ''

            def _problem(exc_, status_code=400):
                if isinstance(exc_.messages, dict):
                    messages = exc_.messages
                else:
                    messages = {'exc': exc_.messages}
                return problem(
                    status=status_code,
                    title=http.client.responses[status_code],
                    detail=
                    f"These fields have problems: {_format_fields(exc_.messages)}",
                    ext={'fields': messages},
                )

            if (self.method in ("post", "put") and request.get_data(cache=True)
                    and request.content_type != self.content_type):
                return problem(
                    status=415,
                    title=
                    f"Content type {request.content_type!r} not supported on this endpoint.",
                )

            try:
                if path_schema:
                    param.update(path_schema().load(param))
            except ValidationError as exc:
                return _problem(exc, status_code=404)

            try:
                if query_schema:
                    param.update(query_schema().load(request.args))

                if header_schema:
                    param.update(header_schema().load(request.headers))

                if request_schema:
                    # Try to decode only when there is data. Decoding an empty string will fail.
                    if request.get_data(cache=True):
                        json = request.json or {}
                    else:
                        json = {}
                    param['body'] = request_schema().load(json)
            except ValidationError as exc:
                return _problem(exc, status_code=400)

            # make pylint happy
            assert callable(self.func)
            # FIXME
            # We need to get the "original data" somewhere and are currently "piggy-backing"
            # it on the response instance. This is somewhat problematic because it's not
            # expected behaviour and not a valid interface of Response. Needs refactoring.
            response = self.func(param)

            if response.status_code not in self._expected_status_codes:
                return problem(
                    status=500,
                    title=
                    f"Unexpected status code returned: {response.status_code}",
                    detail=f"Endpoint {self.operation_id}",
                    ext={'codes': self._expected_status_codes})

            if hasattr(response, 'original_data') and response_schema:
                try:
                    response_schema().load(response.original_data)
                    return response
                except ValidationError as exc:
                    # Hope we never get here in production.
                    return problem(
                        status=500,
                        title="Server was about to send an invalid response.",
                        detail="This is an error of the implementation.",
                        ext={
                            'errors': exc.messages,
                            'orig': response.original_data
                        },
                    )

            return response
Exemplo n.º 5
0
        def _validating_wrapper(param):
            # TODO: Better error messages, pointing to the location where variables are missing

            def _format_fields(_messages: Union[List, Dict]) -> str:
                if isinstance(_messages, list):
                    return ", ".join(_messages)
                if isinstance(_messages, dict):
                    return ", ".join(_messages.keys())
                return ""

            def _problem(exc_, status_code=400):
                if isinstance(exc_.messages, dict):
                    messages = exc_.messages
                else:
                    messages = {"exc": exc_.messages}
                return problem(
                    status=status_code,
                    title=http.client.responses[status_code],
                    detail=
                    f"These fields have problems: {_format_fields(exc_.messages)}",
                    ext={"fields": messages},
                )

            if self.method in ("post", "put") and request.get_data(cache=True):
                try:
                    self._is_expected_content_type(request.content_type)
                except ValueError as exc:
                    return problem(
                        status=415,
                        detail=str(exc),
                        title="Content type not valid for this endpoint.",
                    )

            try:
                if path_schema:
                    param.update(path_schema().load(param))
            except ValidationError as exc:
                return _problem(exc, status_code=404)

            try:
                if query_schema:
                    param.update(query_schema().load(
                        _from_multi_dict(request.args)))

                if header_schema:
                    param.update(header_schema().load(request.headers))

                if request_schema:
                    # Try to decode only when there is data. Decoding an empty string will fail.
                    if request.get_data(cache=True):
                        json_data = request.json or {}
                    else:
                        json_data = {}
                    param["body"] = request_schema().load(json_data)
            except ValidationError as exc:
                return _problem(exc, status_code=400)

            # make pylint happy
            assert callable(self.func)

            if self.tag_group == "Setup" and not config.wato_enabled:
                return problem(
                    status=403,
                    title="Forbidden: WATO is disabled",
                    detail="This endpoint is currently disabled via the "
                    "'Disable remote configuration' option in 'Distributed Monitoring'. "
                    "You may be able to query the central site.",
                )

            if not self.skip_locking and self.method != "get":
                with store.lock_checkmk_configuration():
                    response = self.func(param)
            else:
                response = self.func(param)

            response.freeze()

            if self.output_empty and response.status_code < 400 and response.data:
                return problem(
                    status=500,
                    title="Unexpected data was sent.",
                    detail=(f"Endpoint {self.operation_id}\n"
                            "This is a bug, please report."),
                    ext={"data_sent": str(response.data)},
                )

            if self.output_empty:
                response.content_type = None

            if response.status_code not in self._expected_status_codes:
                return problem(
                    status=500,
                    title=
                    f"Unexpected status code returned: {response.status_code}",
                    detail=(f"Endpoint {self.operation_id}\n"
                            "This is a bug, please report."),
                    ext={"codes": self._expected_status_codes},
                )

            # We assume something has been modified and increase the config generation ID
            # by one. This is necessary to ensure a warning in the "Activate Changes" GUI
            # about there being new changes to activate can be given to the user.
            if request.method != "get" and response.status_code < 300:
                update_config_generation()

                # We assume no configuration change on GET and no configuration change on
                # non-ok responses.
                if config.wato_use_git:
                    do_git_commit()

            # We assume something has been modified and increase the config generation ID
            # by one. This is necessary to ensure a warning in the "Activate Changes" GUI
            # about there being new changes to activate can be given to the user.
            if request.method != "get" and response.status_code < 300:
                update_config_generation()

                # We assume no configuration change on GET and no configuration change on
                # non-ok responses.
                if config.wato_use_git:
                    do_git_commit()

            # We assume something has been modified and increase the config generation ID
            # by one. This is necessary to ensure a warning in the "Activate Changes" GUI
            # about there being new changes to activate can be given to the user.
            if request.method != "get" and response.status_code < 300:
                update_config_generation()

                # We assume no configuration change on GET and no configuration change on
                # non-ok responses.
                if config.wato_use_git:
                    do_git_commit()

            if (self.content_type == "application/json"
                    and response.status_code < 300 and response_schema
                    and response.data):
                try:
                    data = json.loads(response.data.decode("utf-8"))
                except json.decoder.JSONDecodeError as exc:
                    return problem(
                        status=500,
                        title="Server was about to send invalid JSON data.",
                        detail="This is an error of the implementation.",
                        ext={
                            "errors": str(exc),
                            "orig": response.data,
                        },
                    )
                try:
                    outbound = response_schema().dump(data)
                except ValidationError as exc:
                    return problem(
                        status=500,
                        title="Server was about to send an invalid response.",
                        detail="This is an error of the implementation.",
                        ext={
                            "errors": exc.messages,
                            "orig": data,
                        },
                    )

                if self.convert_response:
                    response.set_data(json.dumps(outbound))

            response.freeze()
            return response
Exemplo n.º 6
0
        def _validating_wrapper(param):
            # TODO: Better error messages, pointing to the location where variables are missing

            def _format_fields(_messages: Union[List, Dict]) -> str:
                if isinstance(_messages, list):
                    return ', '.join(_messages)
                if isinstance(_messages, dict):
                    return ', '.join(_messages.keys())
                return ''

            def _problem(exc_, status_code=400):
                if isinstance(exc_.messages, dict):
                    messages = exc_.messages
                else:
                    messages = {'exc': exc_.messages}
                return problem(
                    status=status_code,
                    title=http.client.responses[status_code],
                    detail=
                    f"These fields have problems: {_format_fields(exc_.messages)}",
                    ext={'fields': messages},
                )

            if (self.method in ("post", "put") and request.get_data(cache=True)
                    and request.content_type != self.content_type):
                return problem(
                    status=415,
                    title=
                    f"Content type {request.content_type!r} not supported on this endpoint.",
                )

            try:
                if path_schema:
                    param.update(path_schema().load(param))
            except ValidationError as exc:
                return _problem(exc, status_code=404)

            try:
                if query_schema:
                    param.update(query_schema().load(
                        _from_multi_dict(request.args)))

                if header_schema:
                    param.update(header_schema().load(request.headers))

                if request_schema:
                    # Try to decode only when there is data. Decoding an empty string will fail.
                    if request.get_data(cache=True):
                        json = request.json or {}
                    else:
                        json = {}
                    param['body'] = request_schema().load(json)
            except ValidationError as exc:
                return _problem(exc, status_code=400)

            # make pylint happy
            assert callable(self.func)
            # FIXME
            # We need to get the "original data" somewhere and are currently "piggy-backing"
            # it on the response instance. This is somewhat problematic because it's not
            # expected behaviour and not a valid interface of Response. Needs refactoring.

            if not self.skip_locking and self.method != 'get':
                with store.lock_checkmk_configuration():
                    response = self.func(param)
            else:
                response = self.func(param)

            response.freeze()

            if self.output_empty and response.status_code < 400 and response.data:
                return problem(status=500,
                               title="Unexpected data was sent.",
                               detail=(f"Endpoint {self.operation_id}\n"
                                       "This is a bug, please report."),
                               ext={'data_sent': str(response.data)})

            if self.output_empty:
                response.content_type = None

            if response.status_code not in self._expected_status_codes:
                return problem(
                    status=500,
                    title=
                    f"Unexpected status code returned: {response.status_code}",
                    detail=(f"Endpoint {self.operation_id}\n"
                            "This is a bug, please report."),
                    ext={'codes': self._expected_status_codes})

            # We assume something has been modified and increase the config generation ID
            # by one. This is necessary to ensure a warning in the "Activate Changes" GUI
            # about there being new changes to activate can be given to the user.
            if request.method != 'get' and response.status_code < 300:
                update_config_generation()

                # We assume no configuration change on GET and no configuration change on
                # non-ok responses.
                if config.wato_use_git:
                    do_git_commit()

            if hasattr(response, 'original_data') and response_schema:
                try:
                    response_schema().load(response.original_data)
                    return response
                except ValidationError as exc:
                    # Hope we never get here in production.
                    return problem(
                        status=500,
                        title="Server was about to send an invalid response.",
                        detail="This is an error of the implementation.",
                        ext={
                            'errors': exc.messages,
                            'orig': response.original_data
                        },
                    )

            return response