def load(cls): cls.clear_instances() # First load builtin pages. Set username to '' for name, page_dict in cls.builtin_pages().items(): page_dict["owner"] = UserId(u'') # might have been forgotten on copy action page_dict["public"] = True page_dict["name"] = name new_page = cls(page_dict) cls.add_instance(("", name), new_page) # Now scan users subdirs for files "user_$type_name.mk" for user_dir in os.listdir(config.config_dir): user = UserId(ensure_unicode(user_dir)) try: path = "%s/%s/user_%ss.mk" % (config.config_dir, six.ensure_str(user), cls.type_name()) if not os.path.exists(path): continue if not userdb.user_exists(user): continue user_pages = store.load_object_from_file(path, default={}) for name, page_dict in user_pages.items(): page_dict["owner"] = user page_dict["name"] = name cls.add_instance((user, name), cls(page_dict)) except SyntaxError as e: raise MKGeneralException( _("Cannot load %s from %s: %s") % (cls.type_name(), path, e)) cls._load() cls._declare_instance_permissions()
def _from_vars(self): # TODO: Should we turn the both fields below into Optional[UserId]? self._user_id = html.request.get_unicode_input( "edit") # missing -> new user # This is needed for the breadcrumb computation: # When linking from user notification rules page the request variable is "user" # instead of "edit". We should also change that variable to "user" on this page, # then we can simply use self._user_id. if not self._user_id and html.request.has_var("user"): self._user_id = html.request.get_str_input_mandatory("user") self._cloneid = html.request.get_unicode_input( "clone") # Only needed in 'new' mode # TODO: Nuke the field below? It effectively hides facts about _user_id for mypy. self._is_new_user = self._user_id is None self._users = userdb.load_users(lock=html.is_transaction()) new_user = userdb.new_user_template('htpasswd') if self._user_id is not None: self._user = self._users.get(UserId(self._user_id), new_user) elif self._cloneid: self._user = self._users.get(UserId(self._cloneid), new_user) else: self._user = new_user self._locked_attributes = userdb.locked_attributes( self._user.get('connector'))
def __init__(self, user_id): self.id = UserId(user_id) self._load_confdir() self._load_roles() self._load_attributes() self._load_permissions() self._load_site_config() self._button_counts = None
def fixture_test_config(tmp_path): file_path = tmp_path / "htpasswd" htpwd = htpasswd.Htpasswd(file_path) htpwd.save({ UserId("non-unicode"): "non-unicode", UserId("abcä"): "bbbä", }) return htpwd
def _verify_user(environ: WSGIEnvironment, now: datetime) -> RFC7662: verified: List[RFC7662] = [] auth_header = environ.get("HTTP_AUTHORIZATION", "") basic_user = None if auth_header: auth_type, _ = auth_header.split(None, 1) if auth_type == "Bearer": user_id, secret = user_from_bearer_header(auth_header) automation_user = automation_auth(user_id, secret) if automation_user: verified.append(automation_user) else: # GUI user and Automation users are mutually exclusive. Checking only once is less # work for the system. gui_user = gui_user_auth(user_id, secret, now) if gui_user: verified.append(gui_user) elif auth_type == "Basic": # We store this for sanity checking below, once we get a REMOTE_USER key. # If we don't get a REMOTE_USER key, this value will be ignored. basic_user = user_from_basic_header(auth_header) else: raise MKAuthException(f"Unsupported Auth Type: {auth_type}") remote_user = environ.get("REMOTE_USER", "") if remote_user and userdb.user_exists(UserId(remote_user)): if basic_user and basic_user[0] != remote_user: raise MKAuthException("Mismatch in authentication headers.") verified.append(rfc7662_subject(UserId(remote_user), "web_server")) cookie = Request(environ).cookies.get(f"auth_{omd_site()}") if cookie: user_id, session_id, cookie_hash = user_from_cookie(cookie) check_parsed_auth_cookie(user_id, session_id, cookie_hash) verified.append(rfc7662_subject(user_id, "cookie")) if not verified: raise MKAuthException( "You need to be authenticated to use the REST API.") # We pick the first successful authentication method, which means the precedence is the same # as the order in the code. final_candidate = verified[0] user_id = final_candidate["sub"] if not userdb.is_customer_user_allowed_to_login(user_id): raise MKAuthException(f"{user_id} may not log in here.") if userdb.user_locked(user_id): raise MKAuthException(f"{user_id} not authorized.") if change_reason := userdb.need_to_change_pw(user_id, now): raise MKAuthException( f"{user_id} needs to change the password ({change_reason}).")
def _livestatus_auth_user(user: LoggedInUser, force_authuser: Optional[UserId]) -> Optional[UserId]: if not user.may("general.see_all"): return user.id if force_authuser == UserId("1"): return user.id if force_authuser == UserId("0"): return None if force_authuser: return force_authuser # set a different user if user.get_attribute("force_authuser"): return user.id return None
def _check_auth_automation() -> UserId: secret = html.request.get_str_input_mandatory("_secret", "").strip() user_id = html.request.get_unicode_input_mandatory("_username", "") user_id = UserId(user_id.strip()) html.del_var_from_env('_username') html.del_var_from_env('_secret') if verify_automation_secret(user_id, secret): # Auth with automation secret succeeded - mark transid as unneeded in this case html.transaction_manager.ignore() set_auth_type("automation") return user_id raise MKAuthException(_("Invalid automation secret for user %s") % user_id)
def test_save(tmp_path): file_path = tmp_path / "htpasswd" htpwd = htpasswd.Htpasswd(file_path) htpwd.save({ UserId("non-unicode"): "non-unicode", UserId("abcä"): "bbbä", }) loaded = htpwd.load() assert loaded == { UserId("non-unicode"): "non-unicode", UserId("abcä"): "bbbä", }
def _from_vars(self): # TODO: Should we turn the both fields below into Optional[UserId]? self._user_id = html.request.get_unicode_input("edit") # missing -> new user self._cloneid = html.request.get_unicode_input("clone") # Only needed in 'new' mode # TODO: Nuke the field below? It effectively hides facts about _user_id for mypy. self._is_new_user = self._user_id is None self._users = userdb.load_users(lock=html.is_transaction()) new_user = userdb.new_user_template('htpasswd') if self._user_id is not None: self._user = self._users.get(UserId(self._user_id), new_user) elif self._cloneid: self._user = self._users.get(UserId(self._cloneid), new_user) else: self._user = new_user self._locked_attributes = userdb.locked_attributes(self._user.get('connector'))
def _verify_user(environ) -> RFC7662: verified: List[RFC7662] = [] auth_header = environ.get('HTTP_AUTHORIZATION', '') basic_user = None if auth_header: auth_type, _ = auth_header.split(None, 1) if auth_type == 'Bearer': user_id, secret = user_from_bearer_header(auth_header) automation_user = automation_auth(user_id, secret) if automation_user: verified.append(automation_user) gui_user = gui_user_auth(user_id, secret) if gui_user: verified.append(gui_user) elif auth_type == 'Basic': # We store this for sanity checking below, once we get a REMOTE_USER key. # If we don't get a REMOTE_USER key, this value will be ignored. basic_user = user_from_basic_header(auth_header) else: raise MKAuthException(f"Unsupported Auth Type: {auth_type}") remote_user = environ.get('REMOTE_USER', '') if remote_user and userdb.user_exists(UserId(remote_user)): if basic_user and basic_user[0] != remote_user: raise MKAuthException("Mismatch in authentication headers.") verified.append(rfc7662_subject(UserId(remote_user), 'webserver')) cookie = Request(environ).cookies.get(f"auth_{omd_site()}") if cookie: user_id, session_id, cookie_hash = user_from_cookie(cookie) check_parsed_auth_cookie(user_id, session_id, cookie_hash) verified.append(rfc7662_subject(user_id, 'cookie')) if not verified: raise MKAuthException("You need to be authenticated to use the REST API.") # We pick the first successful authentication method, which means the precedence is the same # as the oder in the code. final_candidate = verified[0] if not userdb.is_customer_user_allowed_to_login(final_candidate['sub']): raise MKAuthException(f"{final_candidate['sub']} may not log in here.") if userdb.user_locked(final_candidate['sub']): raise MKAuthException(f"{final_candidate['sub']} not authorized.") return final_candidate
def bearer_auth(auth_header: str) -> RFC7662: try: _, token = auth_header.split("Bearer", 1) except ValueError: raise MKAuthException(None, "Not a valid Bearer token.") try: user_id, secret = token.strip().split(' ', 1) except ValueError: raise MKAuthException("No user/password combination in Bearer token.") if not secret: raise MKAuthException("Empty password not allowed.") if not user_id: raise MKAuthException("Empty user not allowed.") if "/" in user_id: raise MKAuthException("No slashes / allowed in username.") if not verify_automation_secret(UserId(ensure_str(user_id)), secret): raise MKAuthException("Not authenticated.") # Auth with automation secret succeeded - mark transid as unneeded in this case return rfc7662_subject(user_id, 'automation')
def add_change( action_name: str, text: LogMessage, object_ref: Optional[ObjectRef] = None, diff_text: Optional[str] = None, add_user: bool = True, need_sync: Optional[bool] = None, need_restart: Optional[bool] = None, domains: Optional[List[Type[ABCConfigDomain]]] = None, sites: Optional[List[SiteId]] = None, domain_settings: Optional[DomainSettings] = None, ) -> None: log_audit( action=action_name, message=text, object_ref=object_ref, user_id=user.id if add_user else UserId(""), diff_text=diff_text, ) cmk.gui.watolib.sidebar_reload.need_sidebar_reload() search.update_index_background(action_name) ActivateChangesWriter().add_change( action_name, text, object_ref, add_user, need_sync, need_restart, domains, sites, domain_settings, )
def test_managed_global_internal(monkeypatch): # this test uses the internal mechanics of the user endpoint monkeypatch.setattr( "cmk.gui.watolib.global_settings.rulebased_notifications_enabled", lambda: True) edit_users({ "user": { "attributes": { "ui_theme": None, "ui_sidebar_position": None, "nav_hide_icons_title": None, "icons_per_item": None, "show_mode": None, "start_url": None, "force_authuser": False, "enforce_pw_change": False, "alias": "User Name", "locked": False, "pager": "", "roles": [], "contactgroups": [], "customer": None, # None represents global internally "email": "", "fallback_contact": False, "disable_notifications": {}, }, "is_new_user": True, } }) user_internal = _load_user(UserId("user")) user_endpoint_attrs = complement_customer( _internal_to_api_format(user_internal)) assert user_endpoint_attrs["customer"] == "global"
def test_log_audit_with_object_diff(request_context): old = { "a": "b", "b": "c", } new = { "b": "c", } with on_time("2018-04-15 16:50", "CET"): log_audit( object_ref=None, action="bla", message="Message", user_id=UserId("calvin"), diff_text=make_diff_text(old, new), ) store = AuditLogStore(AuditLogStore.make_path()) assert store.read() == [ AuditLogStore.Entry( time=1523811000, object_ref=None, user_id="calvin", action="bla", text="Message", diff_text='Attribute "a" with value "b" removed.', ), ]
def _automation_push_profile(self): site_id = html.request.var("siteid") if not site_id: raise MKGeneralException(_("Missing variable siteid")) user_id = html.request.var("user_id") if not user_id: raise MKGeneralException(_("Missing variable user_id")) our_id = config.omd_site() if our_id is not None and our_id != site_id: raise MKGeneralException( _("Site ID mismatch. Our ID is '%s', but you are saying we are '%s'." ) % (our_id, site_id)) profile = html.request.var("profile") if not profile: raise MKGeneralException( _('Invalid call: The profile is missing.')) users = userdb.load_users(lock=True) users[UserId(user_id)] = watolib.mk_eval(profile) userdb.save_users(users) return True
def _do_login(self) -> None: """handle the sent login form""" if not html.request.var('_login'): return try: if not config.user_login: raise MKUserError(None, _('Login is not allowed on this site.')) username_var = html.request.get_unicode_input('_username', '') assert username_var is not None username = UserId(username_var.rstrip()) if not username: raise MKUserError('_username', _('No username given.')) password = html.request.var('_password', '') if not password: raise MKUserError('_password', _('No password given.')) default_origtarget = config.url_prefix() + "check_mk/" origtarget = html.get_url_input("_origtarget", default_origtarget) # Disallow redirections to: # - logout.py: Happens after login # - side.py: Happens when invalid login is detected during sidebar refresh if "logout.py" in origtarget or 'side.py' in origtarget: origtarget = default_origtarget result = userdb.check_credentials(username, password) if result: # use the username provided by the successful login function, this function # might have transformed the username provided by the user. e.g. switched # from mixed case to lower case. username = result session_id = userdb.on_succeeded_login(username) # The login succeeded! Now: # a) Set the auth cookie # b) Unset the login vars in further processing # c) Redirect to really requested page _create_auth_session(username, session_id) # Never use inplace redirect handling anymore as used in the past. This results # in some unexpected situations. We simpy use 302 redirects now. So we have a # clear situation. # userdb.need_to_change_pw returns either False or the reason description why the # password needs to be changed change_pw_result = userdb.need_to_change_pw(username) if change_pw_result: raise HTTPRedirect( 'user_change_pw.py?_origtarget=%s&reason=%s' % (html.urlencode(origtarget), change_pw_result)) raise HTTPRedirect(origtarget) userdb.on_failed_login(username) raise MKUserError(None, _('Invalid credentials.')) except MKUserError as e: html.add_user_error(e.varname, e)
def _ensure_connected(user: Optional[LoggedInUser], force_authuser: Optional[UserId]) -> None: """Build up a connection to livestatus to either a single site or multiple sites.""" if "live" in g: return if user is None: user = global_user if force_authuser is None: request_force_authuser = request.get_str_input("force_authuser") force_authuser = UserId( request_force_authuser) if request_force_authuser else None logger.debug( "Initializing livestatus connections as user %s (forced auth user: %s)", user.id, force_authuser, ) g.site_status = {} _connect_multiple_sites(user) _set_livestatus_auth(user, force_authuser) logger.debug("Site states: %r", g.site_status)
def test_parse_auth_cookie_allow_current(current_cookie, with_user, session_id): assert login.user_from_cookie(login._fetch_cookie(current_cookie)) == ( UserId(with_user[0]), session_id, login._generate_auth_hash(with_user[0], session_id), )
def user_from_bearer_header(auth_header: str) -> Tuple[UserId, str]: """ Examples: >>> user_from_bearer_header("Bearer username password") ('username', 'password') Args: auth_header: Returns: """ try: _, token = auth_header.split("Bearer ", 1) except ValueError: raise MKAuthException(f"Not a valid Bearer token: {auth_header}") try: user_id, secret = token.strip().split(" ", 1) except ValueError: raise MKAuthException("No user/password combination in Bearer token.") if not secret: raise MKAuthException("Empty password not allowed.") if not user_id: raise MKAuthException("Empty user not allowed.") if "/" in user_id: raise MKAuthException("No slashes / allowed in username.") return UserId(user_id), secret
def add_change(action_name: str, text: LogMessage, object_ref: Optional[ObjectRef] = None, diff_text: Optional[str] = None, add_user: bool = True, need_sync: Optional[bool] = None, need_restart: Optional[bool] = None, domains: Optional[List[Type[ABCConfigDomain]]] = None, sites: Optional[List[SiteId]] = None) -> None: log_audit(action=action_name, message=text, object_ref=object_ref, user_id=config.user.id if add_user else UserId(''), diff_text=diff_text) cmk.gui.watolib.sidebar_reload.need_sidebar_reload() search.update_index_background(action_name) # On each change to the Checkmk configuration mark the agents to be rebuild # TODO: Really? Why? #if has_agent_bakery(): # import cmk.gui.cee.agent_bakery as agent_bakery # agent_bakery.mark_need_to_bake_agents() ActivateChangesWriter().add_change(action_name, text, object_ref, add_user, need_sync, need_restart, domains, sites)
def _parse_auth_cookie(cookie_name: str) -> Tuple[UserId, float, str]: raw_cookie = html.request.cookie(cookie_name, "::") assert raw_cookie is not None raw_value = ensure_str(raw_cookie) username, issue_time, cookie_hash = raw_value.split(':', 2) return UserId(username), float( issue_time) if issue_time else 0.0, ensure_str(cookie_hash)
def _create_host_info(row: Mapping[str, Any]) -> HostInfo: return HostInfo( name=row['name'], alias=row['alias'], address=row['address'], custom_variables=row['custom_variables'], contacts={UserId(c) for c in row['contacts']}, contact_groups=set(row['contact_groups']), )
def execute(self) -> Iterator[ACResult]: if (cmk.gui.plugins.userdb.htpasswd.HtpasswdUserConnector( {}).check_credentials(UserId("omdadmin"), "omd") == "omdadmin"): yield ACResultCRIT( _("Found <tt>omdadmin</tt> with default password. " "It is highly recommended to change this password.")) else: yield ACResultOK( _("Found <tt>omdadmin</tt> using custom password."))
def _create_host_info(row: Mapping[str, Any]) -> HostInfo: return HostInfo( name=row["name"], alias=row["alias"], address=row["address"], custom_variables=row["custom_variables"], contacts={UserId(c) for c in row["contacts"]}, contact_groups=set(row["contact_groups"]), )
def check_auth_web_server(request: Request) -> UserId: """Try to get the authenticated user from the HTTP request The user may have configured (basic) authentication by the web server. In case a user is provided, we trust that user. """ user = request.remote_user if user is not None: set_auth_type("web_server") return UserId(ensure_str(user))
def test_key_mgmt_create_key(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: monkeypatch.setattr(time, "time", lambda: 123) key = key_mgmt.generate_key("älias", "passphra$e", UserId("dingdöng"), SiteId("test-site")) assert isinstance(key, key_mgmt.Key) assert key.alias == "älias" assert key.date == 123 assert key.owner == "dingdöng" assert key.certificate.startswith("-----BEGIN CERTIFICATE---") assert key.private_key.startswith("-----BEGIN ENCRYPTED PRIVATE KEY---")
def _check_auth_http_header(auth_by_http_header: str) -> Optional[UserId]: """When http header auth is enabled, try to read the user_id from the var""" user_id = request.get_request_header(auth_by_http_header) if not user_id: return None user_id = UserId(user_id) set_auth_type("http_header") return user_id
def _verify_user(environ) -> RFC7662: verified: List[RFC7662] = [] auth_header = environ.get('HTTP_AUTHORIZATION', '') if auth_header: user_id, secret = user_from_bearer_header(auth_header) automation_user = automation_auth(user_id, secret) gui_user = gui_user_auth(user_id, secret) if not (automation_user or gui_user): raise MKAuthException(f"{user_id} not authorized.") if automation_user: verified.append(automation_user) if gui_user: verified.append(gui_user) remote_user = environ.get('REMOTE_USER', '') if remote_user and userdb.user_exists(UserId(remote_user)): verified.append(rfc7662_subject(UserId(remote_user), 'webserver')) cookie = Request(environ).cookies.get(f"auth_{omd_site()}") if cookie: user_id, session_id, cookie_hash = user_from_cookie(cookie) check_parsed_auth_cookie(user_id, session_id, cookie_hash) verified.append(rfc7662_subject(user_id, 'cookie')) if not verified: raise MKAuthException( "You need to be authenticated to use the REST API.") # We pick the first successful authentication method, which means the precedence is the same # as the oder in the code. final_candidate = verified[0] if not userdb.is_customer_user_allowed_to_login(final_candidate['sub']): raise MKAuthException(f"{final_candidate['sub']} may not log in here.") if userdb.user_locked(final_candidate['sub']): raise MKAuthException(f"{final_candidate['sub']} not authorized.") return final_candidate
def query_contactgroups_members(group_names: Iterable[ContactgroupName]) -> set[UserId]: query = "GET contactgroups\nColumns: members" num_group_names = 0 for group_name in group_names: query += f"\nFilter: name = {group_name}" num_group_names += 1 query += f"\nOr: {num_group_names}" contact_lists: list[list[str]] = ( LocalConnection().query_column(query) if num_group_names else [] ) return {UserId(contact) for contact_list in contact_lists for contact in contact_list}
def _check_auth_http_header() -> Optional[UserId]: """When http header auth is enabled, try to read the user_id from the var""" assert isinstance(config.auth_by_http_header, str) user_id = html.request.get_request_header(config.auth_by_http_header) if not user_id: return None user_id = UserId(ensure_str(user_id)) set_auth_type("http_header") return user_id