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
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
def _wato_page_handler(current_mode: str, mode_permissions: List[PermissionName], mode_class: Type[WatoMode]) -> None: try: init_wato_datastructures(with_wato_lock=not html.is_transaction()) except Exception: # Snapshot must work in any case if current_mode == 'snapshot': pass else: raise # Check general permission for this mode if mode_permissions is not None and not config.user.may("wato.seeall"): _ensure_mode_permissions(mode_permissions) mode = mode_class() # Do actions (might switch mode) action_message: Optional[str] = None if html.is_transaction(): try: config.user.need_permission("wato.edit") # Even if the user has seen this mode because auf "seeall", # he needs an explicit access permission for doing changes: if config.user.may("wato.seeall"): if mode_permissions: _ensure_mode_permissions(mode_permissions) if cmk.gui.watolib.read_only.is_enabled( ) and not cmk.gui.watolib.read_only.may_override(): raise MKUserError(None, cmk.gui.watolib.read_only.message()) result = mode.action() if isinstance(result, tuple): newmode, action_message = result else: newmode = result # We assume something has been modified and increase the config generation ID by one. update_config_generation() # If newmode is False, then we shall immediately abort. # This is e.g. the case, if the page outputted non-HTML # data, such as a tarball (in the export function). We must # be sure not to output *any* further data in that case. if newmode is False: return # if newmode is not None, then the mode has been changed if newmode is not None: assert not isinstance(newmode, bool) if newmode == "": # no further information: configuration dialog, etc. if action_message: html.show_message(action_message) wato_html_footer() return mode_permissions, mode_class = _get_mode_permission_and_class( newmode) current_mode = newmode mode = mode_class() html.request.set_var("mode", newmode) # will be used by makeuri # Check general permissions for the new mode if mode_permissions is not None and not config.user.may( "wato.seeall"): for pname in mode_permissions: if '.' not in pname: pname = "wato." + pname config.user.need_permission(pname) except MKUserError as e: action_message = "%s" % e html.add_user_error(e.varname, action_message) except MKAuthException as e: reason = e.args[0] action_message = reason html.add_user_error(None, reason) breadcrumb = make_main_menu_breadcrumb( mode.main_menu()) + mode.breadcrumb() page_menu = mode.page_menu(breadcrumb) wato_html_head(title=mode.title(), breadcrumb=breadcrumb, page_menu=page_menu, show_body_start=display_options.enabled(display_options.H), show_top_heading=display_options.enabled(display_options.T)) if not html.is_transaction() or (cmk.gui.watolib.read_only.is_enabled() and cmk.gui.watolib.read_only.may_override()): _show_read_only_warning() # Show outcome of action if html.has_user_errors(): html.show_user_errors() elif action_message: html.show_message(action_message) # Show content mode.handle_page() if is_sidebar_reload_needed(): html.reload_sidebar() if config.wato_use_git and html.is_transaction(): do_git_commit() wato_html_footer(show_footer=display_options.enabled(display_options.Z), show_body_end=display_options.enabled(display_options.H))
def _wato_page_handler(current_mode: str, mode_permissions: Optional[List[PermissionName]], mode_class: Type[WatoMode]) -> None: try: init_wato_datastructures(with_wato_lock=not html.is_transaction()) except Exception: # Snapshot must work in any case if current_mode == 'snapshot': pass else: raise # Check general permission for this mode if mode_permissions is not None and not config.user.may("wato.seeall"): _ensure_mode_permissions(mode_permissions) mode = mode_class() # Do actions (might switch mode) if html.is_transaction(): try: config.user.need_permission("wato.edit") # Even if the user has seen this mode because auf "seeall", # he needs an explicit access permission for doing changes: if config.user.may("wato.seeall"): if mode_permissions: _ensure_mode_permissions(mode_permissions) if cmk.gui.watolib.read_only.is_enabled( ) and not cmk.gui.watolib.read_only.may_override(): raise MKUserError(None, cmk.gui.watolib.read_only.message()) result = mode.action() if isinstance(result, (tuple, str, bool)): raise MKGeneralException( f"WatoMode \"{current_mode}\" returns unsupported return value: {result!r}" ) # We assume something has been modified and increase the config generation ID by one. update_config_generation() if config.wato_use_git: do_git_commit() # Handle two cases: # a) Don't render the page content after action # (a confirm dialog is displayed by the action, or a non-HTML content was sent) # b) Redirect to another page if isinstance(result, FinalizeRequest): raise result except MKUserError as e: html.add_user_error(e.varname, str(e)) except MKAuthException as e: reason = e.args[0] html.add_user_error(None, reason) breadcrumb = make_main_menu_breadcrumb( mode.main_menu()) + mode.breadcrumb() page_menu = mode.page_menu(breadcrumb) wato_html_head(title=mode.title(), breadcrumb=breadcrumb, page_menu=page_menu, show_body_start=display_options.enabled(display_options.H), show_top_heading=display_options.enabled(display_options.T)) if not html.is_transaction() or (cmk.gui.watolib.read_only.is_enabled() and cmk.gui.watolib.read_only.may_override()): _show_read_only_warning() # Show outcome of failed action on this page if html.has_user_errors(): html.show_user_errors() # Show outcome of previous page (that redirected to this one) for message in get_flashed_messages(): html.show_message(message) # Show content mode.handle_page() if is_sidebar_reload_needed(): html.reload_whole_page() wato_html_footer(show_body_end=display_options.enabled(display_options.H))
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