def _parse_auth_options( auth_details: Dict[str, Union[str, bool]], enforce_pw_change: bool = False) -> Dict[str, Union[int, str, bool]]: """Format the authentication information to be Checkmk compatible Args: auth_details: user provided authentication details Returns: formatted authentication details for Checkmk user_attrs Example: >>> _parse_auth_options({"auth_type": "automation", "secret": "TNBJCkwane3$cfn0XLf6p6a"}) # doctest:+ELLIPSIS {'automation_secret': 'TNBJCkwane3$cfn0XLf6p6a', 'password': ...} >>> _parse_auth_options({"auth_type": "password", "password": "******"}, enforce_pw_change=True) # doctest:+ELLIPSIS {'password': ..., 'last_pw_change': ..., 'enforce_pw_change': True} """ auth_options: Dict[str, Union[str, bool, int]] = {} if auth_details: if auth_details["auth_type"] == "automation": secret = auth_details["secret"] auth_options["automation_secret"] = secret auth_options["password"] = htpasswd.hash_password(secret) else: # password auth_options["password"] = htpasswd.hash_password( auth_details["password"]) auth_options["last_pw_change"] = int(time.time()) # this can be configured in WATO but we enforce this here if enforce_pw_change: auth_options["enforce_pw_change"] = True return auth_options
def _edit_users(self, request): # A dictionary with the userid as key # Each value is a {"set_attributes": {"alias": "test"}, # "unset_attributes": ["pager", "email"]} user_settings = request.get("users") all_users = userdb.load_users() edit_user_objects = {} for user_id, settings in user_settings.items(): if user_id not in all_users: raise MKUserError(None, _("Unknown user: %s") % user_id) if all_users[user_id].get("connector", "htpasswd") != "htpasswd": raise MKUserError( None, _("This user is not a htpasswd user: %s") % user_id) user_attrs = copy.deepcopy(all_users[user_id]) user_attrs.update(settings.get("set_attributes", {})) for entry in settings.get("unset_attributes", []): if entry not in user_attrs: continue del user_attrs[entry] new_password = settings.get("set_attributes", {}).get("password") if new_password: user_attrs["password"] = hash_password(new_password) user_attrs["serial"] = user_attrs.get("serial", 0) + 1 edit_user_objects[user_id] = { "attributes": user_attrs, "is_new_user": False } cmk.gui.watolib.users.edit_users(edit_user_objects)
def _action(self) -> None: assert user.id is not None users = userdb.load_users(lock=True) user_spec = users[user.id] cur_password = request.get_str_input_mandatory("cur_password") password = request.get_str_input_mandatory("password") password2 = request.get_str_input_mandatory("password2", "") # Force change pw mode if not cur_password: raise MKUserError("cur_password", _("You need to provide your current password.")) if not password: raise MKUserError("password", _("You need to change your password.")) if cur_password == password: raise MKUserError("password", _("The new password must differ from your current one.")) if userdb.check_credentials(user.id, cur_password) is False: raise MKUserError("cur_password", _("Your old password is wrong.")) if password2 and password != password2: raise MKUserError("password2", _("The both new passwords do not match.")) watolib.verify_password_policy(password) user_spec["password"] = hash_password(password) user_spec["last_pw_change"] = int(time.time()) # In case the user was enforced to change it's password, remove the flag try: del user_spec["enforce_pw_change"] except KeyError: pass # Increase serial to invalidate old authentication cookies if "serial" not in user_spec: user_spec["serial"] = 1 else: user_spec["serial"] += 1 userdb.save_users(users) flash(_("Successfully changed password.")) # Set the new cookie to prevent logout for the current user login.update_auth_cookie(user.id) # In distributed setups with remote sites where the user can login, start the # user profile replication now which will redirect the user to the destination # page after completion. Otherwise directly open up the destination page. origtarget = request.get_str_input_mandatory("_origtarget", "user_change_pw.py") if user.authorized_login_sites(): raise redirect( makeuri_contextless( request, [("back", origtarget)], filename="user_profile_replicate.py" ) ) raise redirect(origtarget)
def test_hash_password() -> None: # Suppress this warning from passlib code. We can not do anything about this and it clutters our # unit test log # tests/unit/cmk/gui/test_userdb_htpasswd_connector.py::test_hash_password # (...)/handlers/bcrypt.py:378: DeprecationWarning: NotImplemented should not be used in a boolean context # if not result: with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) hashed_pw = htpasswd.hash_password("blä") assert bcrypt.verify("blä", hashed_pw)
def _add_users(self, request): users_from_request = request.get("users") new_user_objects = {} for user_id, values in users_from_request.items(): user_template = userdb.new_user_template("htpasswd") if "password" in values: values["password"] = hash_password(values["password"]) values["serial"] = 1 user_template.update(values) new_user_objects[user_id] = {"attributes": user_template, "is_new_user": True} cmk.gui.watolib.users.edit_users(new_user_objects)
def _action(self) -> bool: assert config.user.id is not None users = userdb.load_users(lock=True) user = users[config.user.id] cur_password = html.request.get_str_input_mandatory('cur_password') password = html.request.get_str_input_mandatory('password') password2 = html.request.get_str_input_mandatory('password2', '') # Force change pw mode if not cur_password: raise MKUserError("cur_password", _("You need to provide your current password.")) if not password: raise MKUserError("password", _("You need to change your password.")) if cur_password == password: raise MKUserError( "password", _("The new password must differ from your current one.")) if userdb.check_credentials(config.user.id, cur_password) is False: raise MKUserError("cur_password", _("Your old password is wrong.")) if password2 and password != password2: raise MKUserError("password2", _("The both new passwords do not match.")) watolib.verify_password_policy(password) user['password'] = hash_password(password) user['last_pw_change'] = int(time.time()) # In case the user was enforced to change it's password, remove the flag try: del user['enforce_pw_change'] except KeyError: pass # Increase serial to invalidate old authentication cookies if 'serial' not in user: user['serial'] = 1 else: user['serial'] += 1 userdb.save_users(users) # Set the new cookie to prevent logout for the current user login.update_auth_cookie(config.user.id) return True
def _edit_users(self, request): # A dictionary with the userid as key # Each value is a {"set_attributes": {"alias": "test"}, # "unset_attributes": ["pager", "email"]} user_settings = request.get("users") all_users = userdb.load_users() locked_attributes_by_connection = {} edit_user_objects = {} for user_id, settings in user_settings.items(): if user_id not in all_users: raise MKUserError(None, _("Unknown user: %s") % user_id) user = all_users[user_id] connector_id = user.get("connector") set_attributes = settings.get("set_attributes", {}) unset_attributes = settings.get("unset_attributes", []) if connector_id not in locked_attributes_by_connection: locked_attributes_by_connection[connector_id] = userdb.locked_attributes( connector_id ) locked_attributes = locked_attributes_by_connection[connector_id] for attr in list(set_attributes.keys()) + unset_attributes: if attr in locked_attributes: raise MKUserError( None, _( 'Attribute "%s" of user "%s" can not be changed, ' "because it is locked by the user connection." ) % (attr, user_id), ) user_attrs = copy.deepcopy(user) user_attrs.update(set_attributes) for entry in unset_attributes: if entry not in user_attrs: continue # TODO: Dynamically fiddling around with a TypedDict is a bit questionable del user_attrs[entry] # type: ignore[misc] new_password = set_attributes.get("password") if new_password: user_attrs["password"] = hash_password(new_password) user_attrs["serial"] = user_attrs.get("serial", 0) + 1 edit_user_objects[user_id] = {"attributes": user_attrs, "is_new_user": False} cmk.gui.watolib.users.edit_users(edit_user_objects)
def _auth_options_to_internal_format( auth_details: AuthOptions, new_user: bool = False) -> Dict[str, Union[int, str, bool]]: """Format the authentication information to be Checkmk compatible Args: auth_details: user provided authentication details Returns: formatted authentication details for Checkmk user_attrs Example: >>> _auth_options_to_internal_format({"auth_type": "automation", "secret": "TNBJCkwane3$cfn0XLf6p6a"}) # doctest:+ELLIPSIS {'automation_secret': 'TNBJCkwane3$cfn0XLf6p6a', 'password': ...} >>> _auth_options_to_internal_format({"auth_type": "password", "password": "******"}) # doctest:+ELLIPSIS {'password': ..., 'last_pw_change': ...} """ internal_options: Dict[str, Union[str, bool, int]] = {} if not auth_details: return internal_options if auth_details["auth_type"] == "automation": secret = auth_details["secret"] internal_options["automation_secret"] = secret internal_options["password"] = htpasswd.hash_password(secret) else: # password if new_user or "password" in auth_details: internal_options["password"] = htpasswd.hash_password( auth_details["password"]) internal_options["last_pw_change"] = int(time.time()) if "enforce_password_change" in auth_details: internal_options["enforce_pw_change"] = auth_details[ "enforce_password_change"] return internal_options
def _mk_user_obj(username, password, automation=False): user = { username: { 'attributes': { 'alias': username, 'email': '*****@*****.**', 'password': htpasswd.hash_password(password), 'notification_method': 'email', 'roles': ['admin'], 'serial': 0 }, 'is_new_user': True, } } # type: dict if automation: user[username]['attributes'].update(automation_secret=password, ) return user
def test_check_credentials_local_user_create_htpasswd_user_ad_hoc() -> None: user_id = UserId("someuser") assert userdb.user_exists(user_id) is False assert userdb._user_exists_according_to_profile(user_id) is False assert user_id not in _load_users_uncached(lock=False) htpasswd.Htpasswd(Path(cmk.utils.paths.htpasswd_file)).save( {user_id: htpasswd.hash_password("cmk")}) # Once a user exists in the htpasswd, the GUI treats the user as existing user and will # automatically initialize the missing data structures assert userdb.user_exists(user_id) is True assert userdb._user_exists_according_to_profile(user_id) is False assert str(user_id) in _load_users_uncached(lock=False) assert userdb.check_credentials(user_id, "cmk") == user_id # Nothing changes during regular access assert userdb.user_exists(user_id) is True assert userdb._user_exists_according_to_profile(user_id) is False assert str(user_id) in _load_users_uncached(lock=False)
def test_hash_password(): hashed_pw = htpasswd.hash_password("blä") assert sha256_crypt.verify("blä", hashed_pw) hashed_pw = htpasswd.hash_password("blä") assert sha256_crypt.verify("blä", hashed_pw)
def _show_page_user_profile(change_pw): start_async_replication = False if not config.user.id: raise MKUserError(None, _('Not logged in.')) if not config.user.may('general.edit_profile') and not config.user.may( 'general.change_password'): raise MKAuthException( _("You are not allowed to edit your user profile.")) if not config.wato_enabled: raise MKAuthException( _('User profiles can not be edited (WATO is disabled).')) success = None if html.request.has_var('_save') and html.check_transaction(): users = userdb.load_users(lock=True) try: # Profile edit (user options like language etc.) if config.user.may('general.edit_profile'): if not change_pw: set_lang = html.get_checkbox('_set_lang') language = html.request.var('language') # Set the users language if requested if set_lang: if language == '': language = None # Set custom language users[config.user.id]['language'] = language config.user.language = language html.set_language_cookie(language) else: # Remove the customized language if 'language' in users[config.user.id]: del users[config.user.id]['language'] config.user.reset_language() # load the new language cmk.gui.i18n.localize(config.user.language) user = users.get(config.user.id) if config.user.may('general.edit_notifications' ) and user.get("notifications_enabled"): value = forms.get_input( watolib.get_vs_flexible_notifications(), "notification_method") users[config.user.id]["notification_method"] = value # Custom attributes if config.user.may('general.edit_user_attributes'): for name, attr in userdb.get_user_attributes(): if attr.user_editable(): if not attr.permission() or config.user.may( attr.permission()): vs = attr.valuespec() value = vs.from_html_vars('ua_' + name) vs.validate_value(value, "ua_" + name) users[config.user.id][name] = value # Change the password if requested password_changed = False if config.user.may('general.change_password'): cur_password = html.request.var('cur_password') password = html.request.var('password') password2 = html.request.var('password2', '') if change_pw: # Force change pw mode if not cur_password: raise MKUserError( "cur_password", _("You need to provide your current password.")) if not password: raise MKUserError( "password", _("You need to change your password.")) if cur_password == password: raise MKUserError( "password", _("The new password must differ from your current one." )) if cur_password and password: if userdb.hook_login(config.user.id, cur_password) is False: raise MKUserError("cur_password", _("Your old password is wrong.")) if password2 and password != password2: raise MKUserError( "password2", _("The both new passwords do not match.")) watolib.verify_password_policy(password) users[config.user.id]['password'] = hash_password(password) users[config.user.id]['last_pw_change'] = int(time.time()) if change_pw: # Has been changed, remove enforcement flag del users[config.user.id]['enforce_pw_change'] # Increase serial to invalidate old cookies if 'serial' not in users[config.user.id]: users[config.user.id]['serial'] = 1 else: users[config.user.id]['serial'] += 1 password_changed = True # Now, if in distributed environment where users can login to remote sites, # set the trigger for pushing the new auth information to the slave sites # asynchronous if config.user.authorized_login_sites(): start_async_replication = True userdb.save_users(users) if password_changed: # Set the new cookie to prevent logout for the current user login.set_auth_cookie(config.user.id) success = True except MKUserError as e: html.add_user_error(e.varname, e) else: users = userdb.load_users() watolib.init_wato_datastructures(with_wato_lock=True) # When in distributed setup, display the replication dialog instead of the normal # profile edit dialog after changing the password. if start_async_replication: user_profile_async_replication_page() return if change_pw: title = _("Change Password") else: title = _("Edit User Profile") html.header(title) # Rule based notifications: The user currently cannot simply call the according # WATO module due to WATO permission issues. So we cannot show this button # right now. if not change_pw: rulebased_notifications = watolib.load_configuration_settings().get( "enable_rulebased_notifications") if rulebased_notifications and config.user.may( 'general.edit_notifications'): html.begin_context_buttons() url = "wato.py?mode=user_notifications_p" html.context_button(_("Notifications"), url, "notifications") html.end_context_buttons() else: reason = html.request.var('reason') if reason == 'expired': html.p( _('Your password is too old, you need to choose a new password.' )) else: html.p( _('You are required to change your password before proceeding.' )) if success: html.reload_sidebar() if change_pw: html.show_message(_("Your password has been changed.")) raise HTTPRedirect(html.request.var('_origtarget', 'index.py')) else: html.show_message(_("Successfully updated user profile.")) # Ensure theme changes are applied without additional user interaction html.immediate_browser_redirect(0.5, html.makeuri([])) if html.has_user_errors(): html.show_user_errors() user = users.get(config.user.id) if user is None: html.show_warning(_("Sorry, your user account does not exist.")) html.footer() return # Returns true if an attribute is locked and should be read only. Is only # checked when modifying an existing user locked_attributes = userdb.locked_attributes(user.get('connector')) def is_locked(attr): return attr in locked_attributes html.begin_form("profile", method="POST") html.prevent_password_auto_completion() html.open_div(class_="wato") forms.header(_("Personal Settings")) if not change_pw: forms.section(_("Name"), simple=True) html.write_text(user.get("alias", config.user.id)) if config.user.may( 'general.change_password') and not is_locked('password'): forms.section(_("Current Password")) html.password_input('cur_password', autocomplete="new-password") forms.section(_("New Password")) html.password_input('password', autocomplete="new-password") forms.section(_("New Password Confirmation")) html.password_input('password2', autocomplete="new-password") if not change_pw and config.user.may('general.edit_profile'): select_language(user) # Let the user configure how he wants to be notified if not rulebased_notifications \ and config.user.may('general.edit_notifications') \ and user.get("notifications_enabled"): forms.section(_("Notifications")) html.help( _("Here you can configure how you want to be notified about host and service problems and " "other monitoring events.")) watolib.get_vs_flexible_notifications().render_input( "notification_method", user.get("notification_method")) if config.user.may('general.edit_user_attributes'): for name, attr in userdb.get_user_attributes(): if attr.user_editable(): vs = attr.valuespec() forms.section(_u(vs.title())) value = user.get(name, vs.default_value()) if not attr.permission() or config.user.may( attr.permission()): vs.render_input("ua_" + name, value) html.help(_u(vs.help())) else: html.write(vs.value_to_text(value)) # Save button forms.end() html.button("_save", _("Save")) html.close_div() html.hidden_fields() html.end_form() html.footer()
def action(self): if not html.check_transaction(): return "users" if self._is_new_user: self._user_id = UserID(allow_empty=False).from_html_vars("user_id") user_attrs = {} else: self._user_id = html.get_unicode_input("edit").strip() user_attrs = self._users[self._user_id] # Full name user_attrs["alias"] = html.get_unicode_input("alias").strip() # Locking user_attrs["locked"] = html.get_checkbox("locked") increase_serial = False if self._user_id in self._users and self._users[ self._user_id]["locked"] != user_attrs["locked"] and user_attrs["locked"]: increase_serial = True # when user is being locked now, increase the auth serial # Authentication: Password or Secret auth_method = html.request.var("authmethod") if auth_method == "secret": secret = html.request.var("_auth_secret", "").strip() user_attrs["automation_secret"] = secret user_attrs["password"] = hash_password(secret) increase_serial = True # password changed, reflect in auth serial else: password = html.request.var("_password_" + self._pw_suffix(), '').strip() password2 = html.request.var("_password2_" + self._pw_suffix(), '').strip() # We compare both passwords only, if the user has supplied # the repeation! We are so nice to our power users... # Note: this validation is done before the main-validiation later on # It doesn't make any sense to put this block into the main validation function if password2 and password != password2: raise MKUserError("_password2", _("The both passwords do not match.")) # Detect switch back from automation to password if "automation_secret" in user_attrs: del user_attrs["automation_secret"] if "password" in user_attrs: del user_attrs["password"] # which was the encrypted automation password! if password: user_attrs["password"] = hash_password(password) user_attrs["last_pw_change"] = int(time.time()) increase_serial = True # password changed, reflect in auth serial # PW change enforcement user_attrs["enforce_pw_change"] = html.get_checkbox("enforce_pw_change") if user_attrs["enforce_pw_change"]: increase_serial = True # invalidate all existing user sessions, enforce relogon # Increase serial (if needed) if increase_serial: user_attrs["serial"] = user_attrs.get("serial", 0) + 1 # Email address user_attrs["email"] = EmailAddressUnicode().from_html_vars("email") idle_timeout = watolib.get_vs_user_idle_timeout().from_html_vars("idle_timeout") user_attrs["idle_timeout"] = idle_timeout if idle_timeout is not None: user_attrs["idle_timeout"] = idle_timeout elif idle_timeout is None and "idle_timeout" in user_attrs: del user_attrs["idle_timeout"] # Pager user_attrs["pager"] = html.request.var("pager", '').strip() if cmk.is_managed_edition(): customer = self._vs_customer.from_html_vars("customer") self._vs_customer.validate_value(customer, "customer") if customer != managed.default_customer_id(): user_attrs["customer"] = customer elif "customer" in user_attrs: del user_attrs["customer"] vs_sites = self._vs_sites() authorized_sites = vs_sites.from_html_vars("authorized_sites") vs_sites.validate_value(authorized_sites, "authorized_sites") if authorized_sites is not None: user_attrs["authorized_sites"] = authorized_sites elif "authorized_sites" in user_attrs: del user_attrs["authorized_sites"] # Roles user_attrs["roles"] = [ role for role in self._roles.keys() if html.get_checkbox("role_" + role) ] # Language configuration set_lang = html.get_checkbox("_set_lang") language = html.request.var("language") if set_lang: if language == "": language = None user_attrs["language"] = language elif not set_lang and "language" in user_attrs: del user_attrs["language"] # Contact groups cgs = [] for c in self._contact_groups: if html.get_checkbox("cg_" + c): cgs.append(c) user_attrs["contactgroups"] = cgs # Notification settings are only active if we do *not* have # rule based notifications! if not self._rbn_enabled(): # Notifications user_attrs["notifications_enabled"] = html.get_checkbox("notifications_enabled") ntp = html.request.var("notification_period") if ntp not in self._timeperiods: ntp = "24X7" user_attrs["notification_period"] = ntp for what, opts in [("host", "durfs"), ("service", "wucrfs")]: user_attrs[what + "_notification_options"] = "".join( [opt for opt in opts if html.get_checkbox(what + "_" + opt)]) value = watolib.get_vs_flexible_notifications().from_html_vars("notification_method") user_attrs["notification_method"] = value else: user_attrs["fallback_contact"] = html.get_checkbox("fallback_contact") # Custom user attributes for name, attr in userdb.get_user_attributes(): value = attr.valuespec().from_html_vars('ua_' + name) user_attrs[name] = value # Generate user "object" to update user_object = {self._user_id: {"attributes": user_attrs, "is_new_user": self._is_new_user}} # The following call validates and updated the users edit_users(user_object) return "users"