Beispiel #1
0
def _create_auth_file(callee, users=None):
    if users is None:
        users = userdb.load_users()

    contactgroups = load_contact_group_information()
    groups = {}
    for gid, group in contactgroups.items():
        if "nagvis_maps" in group and group["nagvis_maps"]:
            groups[gid] = group["nagvis_maps"]

    _create_php_file(callee, users, get_role_permissions(), groups)
Beispiel #2
0
def list_users(params):
    """Show all users"""
    user.need_permission("wato.users")
    users = []
    for user_id, attrs in userdb.load_users(False).items():
        user_attributes = _internal_to_api_format(attrs)
        users.append(
            serialize_user(user_id, complement_customer(user_attributes)))

    return constructors.serve_json(
        constructors.collection_object(domain_type="user_config", value=users))
Beispiel #3
0
    def execute(self, request):
        user_profiles = request.user_profiles

        if not user_profiles:
            raise MKGeneralException(_('Invalid call: No profiles set.'))

        users = userdb.load_users(lock=True)
        for user_id, profile in user_profiles.items():
            users[user_id] = profile
        userdb.save_users(users)
        return True
Beispiel #4
0
def _find_usages_of_contact_group_in_users(name):
    """Is the contactgroup assigned to a user?"""
    used_in = []
    users = userdb.load_users()
    entries = users.items()
    for userid, user in sorted(entries, key=lambda x: x[1].get("alias", x[0])):
        cgs = user.get("contactgroups", [])
        if name in cgs:
            used_in.append(('%s: %s' % (_('User'), user.get('alias', userid)),
                            folder_preserving_link([('mode', 'edit_user'), ('edit', userid)])))
    return used_in
Beispiel #5
0
    def execute(self, api_request):
        user_profiles = api_request.user_profiles

        if not user_profiles:
            raise MKGeneralException(_("Invalid call: No profiles set."))

        users = userdb.load_users(lock=True)
        for user_id, profile in user_profiles.items():
            users[user_id] = profile
        userdb.save_users(users, datetime.now())
        return True
Beispiel #6
0
    def execute(self) -> Iterator[ACResult]:
        users = userdb.load_users()
        num_users = len(users)
        user_warn_threshold = 500

        if num_users <= user_warn_threshold:
            yield ACResultOK(_("You have %d users configured") % num_users)
        else:
            yield ACResultWARN(
                _("You have %d users configured. Please review the number of "
                  "users you have configured in Check_MK.") % num_users)
Beispiel #7
0
    def _show_form(self, profile_changed: bool) -> None:
        assert config.user.id is not None

        users = userdb.load_users()

        if profile_changed:
            flash(_("Successfully updated user profile."))
            # Ensure theme changes are applied without additional user interaction
            html.reload_whole_page()

        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

        html.begin_form("profile", method="POST")
        html.prevent_password_auto_completion()
        html.open_div(class_="wato")
        forms.header(_("Personal settings"))

        forms.section(_("Name"), simple=True)
        html.write_text(user.get("alias", config.user.id))

        select_language(user)

        # Let the user configure how he wants to be notified
        rulebased_notifications = rulebased_notifications_enabled()
        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'):
            custom_user_attr_topics = get_user_attributes_by_topic()
            _show_custom_user_attr(user,
                                   custom_user_attr_topics.get("personal", []))
            forms.header(_("User interface settings"))
            _show_custom_user_attr(
                user, custom_user_attr_topics.get("interface", []))

        forms.end()
        html.close_div()
        html.hidden_fields()
        html.end_form()
        html.footer()
Beispiel #8
0
    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 _set_user_scheme_serial(self):
        """Set attribute to detect with what cmk version the user was created.
        We start that with 2.0"""
        users = load_users(lock=True)
        for user_id in users:
            # pre 2.0 user
            if users[user_id].get("user_scheme_serial") is None:
                _set_show_mode(users, user_id)
            # here you could set attributes based on the current scheme

            users[user_id]["user_scheme_serial"] = USER_SCHEME_SERIAL
        save_users(users)
Beispiel #10
0
    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
Beispiel #11
0
    def _bulk_delete_users_after_confirm(self):
        selected_users = []
        users = userdb.load_users()
        for varname, _value in html.request.itervars(prefix="_c_user_"):
            if html.get_checkbox(varname):
                user = base64.b64decode(
                    varname.split("_c_user_")[-1].encode("utf-8")).decode("utf-8")
                if user in users:
                    selected_users.append(user)

        if selected_users:
            delete_users(selected_users)
Beispiel #12
0
    def _synchronize_profile(self, site_id, site, user_id):
        users = userdb.load_users(lock=False)
        if user_id not in users:
            raise MKUserError(None, _('The requested user does not exist'))

        start = time.time()
        result = push_user_profiles_to_site_transitional_wrapper(site, {user_id: users[user_id]})

        duration = time.time() - start
        watolib.ActivateChanges().update_activation_time(site_id, ACTIVATION_TIME_PROFILE_SYNC,
                                                         duration)
        return result
Beispiel #13
0
    def _show_form(self, profile_changed: bool) -> None:
        assert config.user.id is not None

        users = userdb.load_users()

        change_reason = html.request.get_ascii_input('reason')

        if change_reason == 'expired':
            html.p(_('Your password is too old, you need to choose a new password.'))
        elif change_reason == 'enforced':
            html.p(_('You are required to change your password before proceeding.'))

        if profile_changed:
            html.show_message(_("Your password has been changed."))
            if change_reason:
                raise HTTPRedirect(html.request.get_str_input_mandatory('_origtarget', 'index.py'))

        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

        locked_attributes = userdb.locked_attributes(user.get('connector'))
        if "password" in locked_attributes:
            raise MKUserError(
                "cur_password",
                _("You can not change your password, because it is "
                  "managed by another system."))

        html.begin_form("profile", method="POST")
        html.prevent_password_auto_completion()
        html.open_div(class_="wato")
        forms.header(self._page_title())

        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")

        forms.end()
        html.close_div()
        html.hidden_fields()
        html.end_form()
        html.footer()
Beispiel #14
0
    def page(self):
        with table_element("roles") as table:

            users = userdb.load_users()
            for rid, role in sorted(self._roles.items(), key=lambda a: (a[1]["alias"], a[0])):
                table.row()

                # Actions
                table.cell(_("Actions"), css=["buttons"])
                edit_url = folder_preserving_link([("mode", "edit_role"), ("edit", rid)])
                clone_url = make_action_link([("mode", "roles"), ("_clone", rid)])
                delete_url = make_confirm_link(
                    url=make_action_link([("mode", "roles"), ("_delete", rid)]),
                    message=_("Do you really want to delete the role %s?") % rid,
                )
                html.icon_button(edit_url, _("Properties"), "edit")
                html.icon_button(clone_url, _("Clone"), "clone")
                if not role.get("builtin"):
                    html.icon_button(delete_url, _("Delete this role"), "delete")

                # ID
                table.cell(_("Name"), rid)

                # Alias
                table.cell(_("Alias"), role["alias"])

                # Type
                table.cell(_("Type"), _("builtin") if role.get("builtin") else _("custom"))

                # Modifications
                table.cell(
                    _("Modifications"),
                    HTMLWriter.render_span(
                        str(len(role["permissions"])),
                        title=_("That many permissions do not use the factory defaults."),
                    ),
                )

                # Users
                table.cell(
                    _("Users"),
                    HTML(", ").join(
                        [
                            HTMLWriter.render_a(
                                user.get("alias", user_id),
                                folder_preserving_link([("mode", "edit_user"), ("edit", user_id)]),
                            )
                            for (user_id, user) in users.items()
                            if rid in user["roles"]
                        ]
                    ),
                )
Beispiel #15
0
    def _find_usages_in_users(self, tpname):
        used_in = []
        for userid, user in userdb.load_users().items():
            tp = user.get("notification_period")
            if tp == tpname:
                used_in.append(("%s: %s" % (_("User"), userid),
                                watolib.folder_preserving_link([("mode", "edit_user"),
                                                                ("edit", userid)])))

            for index, rule in enumerate(user.get("notification_rules", [])):
                used_in += self._find_usages_in_notification_rule(
                    tpname, index, rule, user_id=userid)
        return used_in
Beispiel #16
0
def _create_auth_file(callee, users=None):
    if users is None:
        users = userdb.load_users()

    store.mkdir(_auth_base_dir)

    contactgroups = load_contact_group_information()
    groups = {}
    for gid, group in contactgroups.items():
        if 'nagvis_maps' in group and group['nagvis_maps']:
            groups[gid] = group['nagvis_maps']

    _create_php_file(callee, users, config.get_role_permissions(), groups)
Beispiel #17
0
    def page(self):
        with table_element("roles") as table:

            users = userdb.load_users()
            for rid, role in sorted(self._roles.items(),
                                    key=lambda a: (a[1]["alias"], a[0])):
                table.row()

                # Actions
                table.cell(_("Actions"), css="buttons")
                edit_url = watolib.folder_preserving_link([("mode",
                                                            "edit_role"),
                                                           ("edit", rid)])
                clone_url = make_action_link([("mode", "roles"),
                                              ("_clone", rid)])
                delete_url = make_action_link([("mode", "roles"),
                                               ("_delete", rid)])
                html.icon_button(edit_url, _("Properties"), "edit")
                html.icon_button(clone_url, _("Clone"), "clone")
                if not role.get("builtin"):
                    html.icon_button(delete_url, _("Delete this role"),
                                     "delete")

                # ID
                table.text_cell(_("Name"), rid)

                # Alias
                table.text_cell(_("Alias"), role["alias"])

                # Type
                table.cell(
                    _("Type"),
                    _("builtin") if role.get("builtin") else _("custom"))

                # Modifications
                table.cell(
                    _("Modifications"), "<span title='%s'>%s</span>" %
                    (_("That many permissions do not use the factory defaults."
                       ), len(role["permissions"])))

                # Users
                table.cell(
                    _("Users"),
                    HTML(", ").join([
                        html.render_a(
                            user.get("alias", user_id),
                            watolib.folder_preserving_link([
                                ("mode", "edit_user"), ("edit", user_id)
                            ])) for (user_id, user) in users.items()
                        if rid in user["roles"]
                    ]))
Beispiel #18
0
    def _show_form(self) -> None:
        assert user.id is not None

        users = userdb.load_users()

        user_spec: Optional[UserSpec] = users.get(user.id)
        if user_spec is None:
            html.show_warning(_("Sorry, your user account does not exist."))
            html.footer()
            return

        html.begin_form("profile", method="POST")
        html.prevent_password_auto_completion()
        html.open_div(class_="wato")
        forms.header(_("Personal settings"))

        forms.section(_("Name"), simple=True)
        html.write_text(user_spec.get("alias", user.id))

        select_language(user_spec)

        # Let the user configure how he wants to be notified
        rulebased_notifications = rulebased_notifications_enabled()
        if (
            not rulebased_notifications
            and user.may("general.edit_notifications")
            and user_spec.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_spec.get("notification_method")
            )

        if user.may("general.edit_user_attributes"):
            custom_user_attr_topics = get_user_attributes_by_topic()
            _show_custom_user_attr(user_spec, custom_user_attr_topics.get("personal", []))
            forms.header(_("User interface settings"))
            _show_custom_user_attr(user_spec, custom_user_attr_topics.get("interface", []))

        forms.end()
        html.close_div()
        html.hidden_fields()
        html.end_form()
        html.footer()
Beispiel #19
0
    def _show_form(self) -> None:
        assert user.id is not None

        users = userdb.load_users()

        change_reason = request.get_ascii_input("reason")

        if change_reason == "expired":
            html.p(
                _("Your password is too old, you need to choose a new password."
                  ))
        elif change_reason == "enforced":
            html.p(
                _("You are required to change your password before proceeding."
                  ))

        user_spec = users.get(user.id)
        if user_spec is None:
            html.show_warning(_("Sorry, your user account does not exist."))
            html.footer()
            return

        locked_attributes = userdb.locked_attributes(
            user_spec.get("connector"))
        if "password" in locked_attributes:
            raise MKUserError(
                "cur_password",
                _("You can not change your password, because it is "
                  "managed by another system."),
            )

        html.begin_form("profile", method="POST")
        html.prevent_password_auto_completion()
        html.open_div(class_="wato")
        forms.header(self._page_title())

        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")

        forms.end()
        html.close_div()
        html.hidden_fields()
        html.end_form()
        html.footer()
Beispiel #20
0
def _find_usages_of_contact_group_in_users(name: GroupName) -> List[Tuple[str, str]]:
    """Is the contactgroup assigned to a user?"""
    used_in = []
    users = userdb.load_users()
    for userid, user_spec in sorted(users.items(), key=lambda x: x[1].get("alias", x[0])):
        cgs = user_spec.get("contactgroups", [])
        if name in cgs:
            used_in.append(
                (
                    "%s: %s" % (_("User"), user_spec.get("alias", userid)),
                    folder_preserving_link([("mode", "edit_user"), ("edit", userid)]),
                )
            )
    return used_in
Beispiel #21
0
 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'))
Beispiel #22
0
def create_auth_file(callee, users=None):
    from cmk.gui.watolib.groups import load_contact_group_information
    import cmk.gui.userdb as userdb  # TODO: Cleanup
    if users is None:
        users = userdb.load_users()

    store.mkdir(g_auth_base_dir)

    contactgroups = load_contact_group_information()
    groups = {}
    for gid, group in contactgroups.items():
        if 'nagvis_maps' in group and group['nagvis_maps']:
            groups[gid] = group['nagvis_maps']

    create_php_file(callee, users, config.get_role_permissions(), groups)
Beispiel #23
0
def list_users(params):
    """Show all users"""
    user_collection = {
        "id": "user",
        "domainType": "user_config",
        "value": [
            constructors.collection_item(
                domain_type="user_config",
                title=attrs["alias"],
                identifier=user_id,
            )
            for user_id, attrs in userdb.load_users(False).items()
        ],
        "links": [constructors.link_rel("self", constructors.collection_href("user_config"))],
    }
    return constructors.serve_json(user_collection)
Beispiel #24
0
    def _from_vars(self):
        self._user_id = html.get_unicode_input("edit")  # missing -> new user
        self._cloneid = html.get_unicode_input("clone")  # Only needed in 'new' mode
        self._is_new_user = self._user_id is None

        self._users = userdb.load_users(lock=html.is_transaction())

        if self._is_new_user:
            if self._cloneid:
                self._user = self._users.get(self._cloneid, userdb.new_user_template('htpasswd'))
            else:
                self._user = userdb.new_user_template('htpasswd')
        else:
            self._user = self._users.get(self._user_id, userdb.new_user_template('htpasswd'))

        self._locked_attributes = userdb.locked_attributes(self._user.get('connector'))
Beispiel #25
0
    def _bulk_delete_users_after_confirm(self):
        selected_users = []
        users = userdb.load_users()
        for varname, _value in html.request.itervars(prefix="_c_user_"):
            if html.get_checkbox(varname):
                user = base64.b64decode(varname.split("_c_user_")[-1]).decode("utf-8")
                if user in users:
                    selected_users.append(user)

        if selected_users:
            c = wato_confirm(
                _("Confirm deletion of %d users") % len(selected_users),
                _("Do you really want to delete %d users?") % len(selected_users))
            if c:
                delete_users(selected_users)
            elif c is False:
                return ""
Beispiel #26
0
def find_usages_of_contact_group(name):
    # Part 1: Rules
    used_in = _find_usages_of_group_in_rules(
        name, ['host_contactgroups', 'service_contactgroups'])

    # Is the contactgroup assigned to a user?
    users = userdb.load_users()
    entries = users.items()
    for userid, user in sorted(entries, key=lambda x: x[1].get("alias", x[0])):
        cgs = user.get("contactgroups", [])
        if name in cgs:
            used_in.append(('%s: %s' % (_('User'), user.get('alias', userid)),
                            folder_preserving_link([('mode', 'edit_user'),
                                                    ('edit', userid)])))

    global_config = load_configuration_settings()

    # Used in default_user_profile?
    config_variable = config_variable_registry['default_user_profile']()
    domain = config_variable.domain()
    configured = global_config.get('default_user_profile', {})
    default_value = domain().default_globals()["default_user_profile"]
    if (configured and name in configured['contactgroups']) \
       or name in  default_value['contactgroups']:
        used_in.append(
            ('%s' % (_('Default User Profile')),
             folder_preserving_link([('mode', 'edit_configvar'),
                                     ('varname', 'default_user_profile')])))

    # Is the contactgroup used in mkeventd notify (if available)?
    if 'mkeventd_notify_contactgroup' in config_variable_registry:
        config_variable = config_variable_registry[
            'mkeventd_notify_contactgroup']()
        domain = config_variable.domain()
        configured = global_config.get('mkeventd_notify_contactgroup')
        default_value = domain().default_globals(
        )["mkeventd_notify_contactgroup"]
        if (configured and name == configured) \
           or name == default_value:
            used_in.append(('%s' % (config_variable.valuespec().title()),
                            folder_preserving_link([
                                ('mode', 'edit_configvar'),
                                ('varname', 'mkeventd_notify_contactgroup')
                            ])))

    return used_in
Beispiel #27
0
def delete_users(users_to_delete):
    if config.user.id in users_to_delete:
        raise MKUserError(None, _("You cannot delete your own account!"))

    all_users = userdb.load_users(lock=True)

    deleted_users = []
    for entry in users_to_delete:
        if entry in all_users:  # Silently ignore not existing users
            deleted_users.append(entry)
            del all_users[entry]
        else:
            raise MKUserError(None, _("Unknown user: %s") % entry)

    if deleted_users:
        add_change("edit-users", _("Deleted user: %s") % ", ".join(deleted_users))
        userdb.save_users(all_users)
Beispiel #28
0
    def _action(self) -> bool:
        assert config.user.id is not None

        users = userdb.load_users(lock=True)
        user = users[config.user.id]

        language = html.request.get_ascii_input_mandatory('language', "")
        # Set the users language if requested to set it explicitly
        if language != "_default_":
            user['language'] = language
            config.user.language = language
            html.set_language_cookie(language)

        else:
            if 'language' in user:
                del user['language']
            config.user.reset_language()

        # load the new language
        cmk.gui.i18n.localize(config.user.language)

        if config.user.may('general.edit_notifications') and user.get(
                "notifications_enabled"):
            value = forms.get_input(watolib.get_vs_flexible_notifications(),
                                    "notification_method")
            user["notification_method"] = value

        # Custom attributes
        if config.user.may('general.edit_user_attributes'):
            for name, attr in userdb.get_user_attributes():
                if not attr.user_editable():
                    continue

                if attr.permission() and not config.user.may(
                        attr.permission()):
                    continue

                vs = attr.valuespec()
                value = vs.from_html_vars('ua_' + name)
                vs.validate_value(value, "ua_" + name)
                user[name] = value

        userdb.save_users(users)

        return True
Beispiel #29
0
def _rename_host_in_event_rules(oldname, newname):
    actions = []

    def rename_in_event_rules(rules):
        num_changed = 0
        for rule in rules:
            for key in ["match_hosts", "match_exclude_hosts"]:
                if rule.get(key):
                    if rename_host_in_list(rule[key], oldname, newname):
                        num_changed += 1
        return num_changed

    users = userdb.load_users(lock=True)
    some_user_changed = False
    for user in users.values():
        if unrules := user.get("notification_rules"):
            if num_changed := rename_in_event_rules(unrules):
                actions += ["notify_user"] * num_changed
                some_user_changed = True
Beispiel #30
0
def edit_users(changed_users):
    if user:
        user.need_permission("wato.users")
        user.need_permission("wato.edit")
    all_users = userdb.load_users(lock=True)
    new_users_info = []
    modified_users_info = []
    for user_id, settings in changed_users.items():
        user_attrs = settings.get("attributes")
        is_new_user = settings.get("is_new_user", True)
        _validate_user_attributes(all_users,
                                  user_id,
                                  user_attrs,
                                  is_new_user=is_new_user)
        if is_new_user:
            new_users_info.append(user_id)
        else:
            modified_users_info.append(user_id)

        if is_new_user:
            add_internal_attributes(user_attrs)

        old_object = make_user_audit_log_object(all_users.get(user_id, {}))
        log_audit(
            action="edit-user",
            message=(_("Created new user: %s") %
                     user_id if is_new_user else _("Modified user: %s") %
                     user_id),
            diff_text=make_diff_text(old_object,
                                     make_user_audit_log_object(user_attrs)),
            object_ref=make_user_object_ref(user_id),
        )

        all_users[user_id] = user_attrs

    if new_users_info:
        add_change("edit-users",
                   _("Created new users: %s") % ", ".join(new_users_info))
    if modified_users_info:
        add_change("edit-users",
                   _("Modified users: %s") % ", ".join(modified_users_info))

    userdb.save_users(all_users, datetime.now())