def notify_nonauditor_promoted(settings, session, user, auditors_group, group_names): # type: (Settings, Session, User, Group, Set[str]) -> None """Send notification that a nonauditor has been promoted to be an auditor. Handles email notification and audit logging. Args: settings: Grouper Settings object for current run. session: Object for db session. user: The user that has been promoted. auditors_group: The auditors group group_names: The audited groups in which the user was previously a non-auditor approver. """ member_name = user.username recipients = [member_name] auditors_group_name = auditors_group.groupname audit_data = { "action": "nonauditor_promoted", "actor_id": user.id, "description": "Added {} to group {}".format(member_name, auditors_group_name), } AuditLog.log(session, on_user_id=user.id, on_group_id=auditors_group.id, **audit_data) email_context = {"auditors_group_name": auditors_group_name, "member_name": member_name} send_email( session=session, recipients=recipients, subject='Added as member to group "{}"'.format(auditors_group_name), template="nonauditor_promoted", settings=settings, context=email_context, )
def edit_member(self, requester, user_or_group, reason, **kwargs): """ Edit an existing member (User or Group) of a group. This takes the same parameters as add_member, except that we do not allow you to set a status: this only works on existing members. Any option that is not passed is not updated, and instead, the existing value for this user is kept. """ logging.debug( "Editing member (%s) in %s", user_or_group.name, self.groupname ) persist_group_member_changes( session=self.session, group=self, requester=requester, member=user_or_group, status="actioned", reason=reason, **kwargs ) member_type = user_or_group.member_type message = "Edit member {} {}: {}".format( OBJ_TYPES_IDX[member_type].lower(), user_or_group.name, reason) AuditLog.log(self.session, requester.id, 'edit_member', message, on_group_id=self.id)
def disable_permission_auditing(session, permission_name, actor_user_id): """Set a permission as audited. Args: session(models.base.session.Session): database session permission_name(str): name of permission in question actor_user_id(int): id of user who is disabling auditing """ permission = get_permission(session, permission_name) if not permission: raise NoSuchPermission(name=permission_name) permission.audited = False AuditLog.log( session, actor_user_id, "disable_auditing", "Disabled auditing.", on_permission_id=permission.id, ) Counter.incr(session, "updates") session.commit()
def create_service_account(session, actor, name, description, machine_set, owner): # type: (Session, User, str, str, str, Group) -> ServiceAccount """Creates a service account and its underlying user. Also adds the service account to the list of accounts managed by the owning group. Throws: BadMachineSet: if some plugin rejected the machine set DuplicateServiceAccount: if a user with the given name already exists """ user = User(username=name, is_service_account=True) service_account = ServiceAccount(user=user, description=description, machine_set=machine_set) if machine_set is not None: _check_machine_set(service_account, machine_set) try: user.add(session) service_account.add(session) session.flush() except IntegrityError: session.rollback() raise DuplicateServiceAccount("User {} already exists".format(name)) # Counter is updated here and the session is committed, so we don't need an additional update # or commit for the account creation. add_service_account(session, owner, service_account) AuditLog.log(session, actor.id, "create_service_account", "Created new service account.", on_group_id=owner.id, on_user_id=service_account.user_id) return service_account
def post(self, user_id=None, name=None): user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = UserEnableForm(self.request.arguments) if not form.validate(): # TODO: add error message return self.redirect("/users/{}?refresh=yes".format(user.name)) if user.role_user: enable_service_account(self.session, actor=self.current_user, preserve_membership=form.preserve_membership.data, user=user) else: enable_user(self.session, user, self.current_user, preserve_membership=form.preserve_membership.data) self.session.commit() AuditLog.log(self.session, self.current_user.id, 'enable_user', 'Enabled user.', on_user_id=user.id) return self.redirect("/users/{}?refresh=yes".format(user.name))
def post(self, user_id=None, name=None, key_id=None, tag_id=None): user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() try: key = get_public_key(self.session, user.id, key_id) except KeyNotFound: return self.notfound() tag = PublicKeyTag.get(self.session, id=tag_id) if not tag: return self.notfound() try: remove_tag_from_public_key(self.session, key, tag) except TagNotOnKey: return self.redirect("/users/{}?refresh=yes".format(user.name)) AuditLog.log(self.session, self.current_user.id, 'untag_public_key', 'Untagged public key: {}'.format(key.fingerprint), on_tag_id=tag.id, on_user_id=user.id) return self.redirect("/users/{}?refresh=yes".format(user.name))
def post(self, user_id=None, name=None): user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() try: if user.role_user: disable_role_user(self.session, user=user) else: disable_user(self.session, user) except PluginRejectedDisablingUser as e: alert = Alert("danger", str(e)) return self.redirect("/users/{}".format(user.name), alerts=[alert]) self.session.commit() AuditLog.log( self.session, self.current_user.id, "disable_user", "Disabled user.", on_user_id=user.id, ) return self.redirect("/users/{}?refresh=yes".format(user.name))
def post(self, user_id=None, name=None): user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = UserShellForm(self.request.arguments) form.shell.choices = settings.shell if not form.validate(): return self.render( "user-shell.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) user.set_metadata(USER_METADATA_SHELL_KEY, form.data["shell"]) user.add(self.session) self.session.commit() AuditLog.log(self.session, self.current_user.id, 'changed_shell', 'Changed shell: {}'.format(form.data["shell"]), on_user_id=user.id) return self.redirect("/users/{}?refresh=yes".format(user.name))
def post(self, user_id=None, name=None, key_id=None): user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() try: key = get_public_key(self.session, user.id, key_id) delete_public_key(self.session, user.id, key_id) except KeyNotFound: return self.notfound() AuditLog.log(self.session, self.current_user.id, 'delete_public_key', 'Deleted public key: {}'.format(key.fingerprint), on_user_id=user.id) email_context = { "actioner": self.current_user.name, "changed_user": user.name, "action": "removed", } send_email(self.session, [user.name], 'Public SSH key removed', 'ssh_keys_changed', settings, email_context) return self.redirect("/users/{}?refresh=yes".format(user.name))
def create_role_user(session, actor, name, description, canjoin): # type (Session, User, str, str, str) -> None """DEPRECATED: Do not use in production code Creates a service account with the given name, description, and canjoin status Args: session: the database session actor: the user creating the service account name: the name of the service account description: description of the service account canjoin: the canjoin status for management of the service account Throws: IntegrityError: if a user or group with the given name already exists """ user = User(username=name, role_user=True) group = Group(groupname=name, description=description, canjoin=canjoin) user.add(session) group.add(session) group.add_member(actor, actor, "Group Creator", "actioned", None, "np-owner") group.add_member(actor, user, "Service Account", "actioned", None, "member") session.commit() AuditLog.log( session, actor.id, "create_role_user", "Created new service account.", on_group_id=group.id, on_user_id=user.id, )
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None user_id = kwargs.get("user_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = UserGitHubForm(self.request.arguments) if not form.validate(): return self.render( "user-github.html", form=form, user=user, alerts=self.get_form_alerts(form.errors) ) new_username = form.data["username"] if new_username == "": new_username = None set_user_metadata(self.session, user.id, USER_METADATA_GITHUB_USERNAME_KEY, new_username) AuditLog.log( self.session, self.current_user.id, "changed_github_username", "Changed GitHub username: {}".format(form.data["username"]), on_user_id=user.id, ) return self.redirect("/users/{}?refresh=yes".format(user.name))
def post(self, tag_id=None, name=None): tag = PublicKeyTag.get(self.session, tag_id, name) if not tag: return self.notfound() if not user_has_permission(self.session, self.current_user, TAG_EDIT, tag.name): return self.forbidden() form = TagEditForm(self.request.arguments, obj=tag) if not form.validate(): return self.render( "tag-edit.html", tag=tag, form=form, alerts=self.get_form_alerts(form.errors) ) tag.description = form.data["description"] tag.enabled = form.data["enabled"] Counter.incr(self.session, "updates") try: self.session.commit() except IntegrityError: self.session.rollback() form.tagname.errors.append( "{} already exists".format(form.data["tagname"]) ) return self.render( "tag-edit.html", tag=tag, form=form, alerts=self.get_form_alerts(form.errors) ) AuditLog.log(self.session, self.current_user.id, 'edit_tag', 'Edited tag.', on_tag_id=tag.id) return self.redirect("/tags/{}".format(tag.name))
def test_group_logdump(session, tmpdir, users, groups): # noqa: F811 groupname = "team-sre" group_id = groups[groupname].id yesterday = date.today() - timedelta(days=1) fn = tmpdir.join("out.csv").strpath call_main( session, tmpdir, "group", "log_dump", groupname, yesterday.isoformat(), "--outfile", fn ) with open(fn, "r") as fh: out = fh.read() assert not out, "nothing yet" AuditLog.log( session, users["*****@*****.**"].id, "make_noise", "making some noise", on_group_id=group_id ) session.commit() call_main( session, tmpdir, "group", "log_dump", groupname, yesterday.isoformat(), "--outfile", fn ) with open(fn, "r") as fh: entries = [x for x in csv.reader(fh)] assert len(entries) == 1, "should capture our new audit log entry" log_time, actor, description, action, extra = entries[0] assert groupname in extra
def post(self, group_id=None, name=None): group = Group.get(self.session, group_id, name) if not group: return self.notfound() if not self.current_user.can_manage(group): return self.forbidden() form = GroupRemoveForm(self.request.arguments) if not form.validate(): return self.send_error(status_code=400) member_type, member_name = form.data["member_type"], form.data["member"] members = group.my_members() if not members.get((member_type.capitalize(), member_name), None): return self.notfound() removed_member = get_user_or_group(self.session, member_name, user_or_group=member_type) if self.current_user == removed_member: return self.send_error( status_code=400, reason="Can't remove yourself. Leave group instead." ) group.revoke_member(self.current_user, removed_member, "Removed by owner/np-owner/manager") AuditLog.log(self.session, self.current_user.id, 'remove_from_group', '{} was removed from the group.'.format(removed_member.name), on_group_id=group.id, on_user_id=removed_member.id) return self.redirect("/groups/{}?refresh=yes".format(group.name))
def post(self, name=None, mapping_id=None): grantable = self.current_user.my_grantable_permissions() if not grantable: return self.forbidden() mapping = PermissionMap.get(self.session, id=mapping_id) if not mapping: return self.notfound() allowed = False for perm in grantable: if perm[0].name == mapping.permission.name: if matches_glob(perm[1], mapping.argument): allowed = True if not allowed: return self.forbidden() permission = mapping.permission group = mapping.group mapping.delete(self.session) Counter.incr(self.session, "updates") self.session.commit() AuditLog.log(self.session, self.current_user.id, 'revoke_permission', 'Revoked permission with argument: {}'.format(mapping.argument), on_group_id=group.id, on_permission_id=permission.id) return self.redirect('/groups/{}?refresh=yes'.format(group.name))
def test_group_logdump(make_session, session, users, groups, tmpdir): make_session.return_value = session groupname = 'team-sre' group_id = groups[groupname].id yesterday = date.today() - timedelta(days=1) fn = tmpdir.join('out.csv').strpath call_main('group', 'log_dump', groupname, yesterday.isoformat(), '--outfile', fn) with open(fn, 'r') as fh: out = fh.read() assert not out, 'nothing yet' AuditLog.log(session, users['*****@*****.**'].id, 'make_noise', 'making some noise', on_group_id=group_id) session.commit() call_main('group', 'log_dump', groupname, yesterday.isoformat(), '--outfile', fn) with open(fn, 'r') as fh: entries = [x for x in csv.reader(fh)] assert len(entries) == 1, 'should capture our new audit log entry' log_time, actor, description, action, extra = entries[0] assert groupname in extra
def post(self, user_id=None, name=None): user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = UserPasswordForm(self.request.arguments) if not form.validate(): return self.render("user-password-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors)) pass_name = form.data["name"] password = form.data["password"] try: add_new_user_password(self.session, pass_name, password, user.id) except PasswordAlreadyExists: self.session.rollback() form.name.errors.append("Name already in use.") return self.render("user-password-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors)) AuditLog.log( self.session, self.current_user.id, "add_password", "Added password: {}".format(pass_name), on_user_id=user.id, ) email_context = {"actioner": self.current_user.name, "changed_user": user.name, "pass_name": pass_name} send_email(self.session, [user.name], "User password created", "user_password_created", settings, email_context) return self.redirect("/users/{}?refresh=yes".format(user.name))
def post(self, group_id=None, name=None): group = Group.get(self.session, group_id, name) if not group: return self.notfound() members = group.my_members() if not user_role(self.current_user, members) in ("owner", "np-owner"): return self.forbidden() # Enabling and disabling service accounts via the group endpoints is forbidden # because we need the preserve_membership data that is only available via the # UserEnable form. if is_role_user(self.session, group=group): return self.forbidden() group.disable() self.session.commit() AuditLog.log(self.session, self.current_user.id, 'disable_group', 'Disabled group.', on_group_id=group.id) if group.audit: # complete the audit group.audit.complete = True self.session.commit() AuditLog.log(self.session, self.current_user.id, 'complete_audit', 'Disabling group completes group audit.', on_group_id=group.id) return self.redirect("/groups/{}?refresh=yes".format(group.name))
def post(self): form = TagCreateForm(self.request.arguments) if not form.validate(): return self.render( "tag-create.html", form=form, alerts=self.get_form_alerts(form.errors) ) tag = PublicKeyTag( name=form.data["tagname"], description=form.data["description"], ) try: tag.add(self.session) self.session.flush() except IntegrityError: self.session.rollback() form.tagname.errors.append( "{} already exists".format(form.data["tagname"]) ) return self.render( "tag-create.html", form=form, alerts=self.get_form_alerts(form.errors) ) Counter.incr(self.session, "updates") self.session.commit() AuditLog.log(self.session, self.current_user.id, 'create_tag', 'Created new tag.', on_tag_id=tag.id) return self.redirect("/tags/{}?refresh=yes".format(tag.name))
def post(self, group_id=None, name=None, account_id=None, accountname=None, mapping_id=None): group = Group.get(self.session, group_id, name) if not group: return self.notfound() service_account = ServiceAccount.get(self.session, account_id, accountname) if not service_account: return self.notfound() if not self.check_access(self.session, self.current_user, service_account): return self.forbidden() mapping = ServiceAccountPermissionMap.get(self.session, mapping_id) if not mapping: return self.notfound() permission = mapping.permission argument = mapping.argument mapping.delete(self.session) Counter.incr(self.session, "updates") self.session.commit() AuditLog.log( self.session, self.current_user.id, "revoke_permission", "Revoked permission with argument: {}".format(argument), on_permission_id=permission.id, on_group_id=group.id, on_user_id=service_account.user.id, ) return self.redirect( "/groups/{}/service/{}?refresh=yes".format(group.name, service_account.user.username) )
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None user_id = kwargs.get("user_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = UserShellForm(self.request.arguments) form.shell.choices = settings().shell if not form.validate(): return self.render( "user-shell.html", form=form, user=user, alerts=self.get_form_alerts(form.errors) ) set_user_metadata(self.session, user.id, USER_METADATA_SHELL_KEY, form.data["shell"]) AuditLog.log( self.session, self.current_user.id, "changed_shell", "Changed shell: {}".format(form.data["shell"]), on_user_id=user.id, ) return self.redirect("/users/{}?refresh=yes".format(user.name))
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None user_id = kwargs.get("user_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = UserTokenForm(self.request.arguments) if not form.validate(): return self.render( "user-token-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) try: token, secret = add_new_user_token( self.session, UserToken(name=form.data["name"], user=user) ) self.session.commit() except IntegrityError: self.session.rollback() form.name.errors.append("Name already in use.") return self.render( "user-token-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) AuditLog.log( self.session, self.current_user.id, "add_token", "Added token: {}".format(token.name), on_user_id=user.id, ) email_context = { "actioner": self.current_user.name, "changed_user": user.name, "action": "added", } send_email( self.session, [user.name], "User token created", "user_tokens_changed", settings(), email_context, ) return self.render("user-token-created.html", token=token, secret=secret)
def post(self): can_create = user_creatable_permissions(self.session, self.current_user) if not can_create: return self.forbidden() form = PermissionCreateForm(self.request.arguments) if not form.validate(): return self.render( "permission-create.html", form=form, alerts=self.get_form_alerts(form.errors) ) # A user is allowed to create a permission if the name matches any of the globs that they # are given access to via PERMISSION_CREATE, as long as the permission does not match a # reserved name. (Unless specifically granted.) allowed = False for creatable in can_create: if matches_glob(creatable, form.data["name"]): allowed = True for failure_message in test_reserved_names(form.data["name"]): form.name.errors.append(failure_message) if not allowed: form.name.errors.append("Permission name does not match any of your allowed patterns.") if form.name.errors: return self.render( "permission-create.html", form=form, alerts=self.get_form_alerts(form.errors) ) try: permission = create_permission( self.session, form.data["name"], form.data["description"] ) self.session.flush() except IntegrityError: self.session.rollback() form.name.errors.append("Name already in use. Permissions must be unique.") return self.render( "permission-create.html", form=form, can_create=sorted(can_create), alerts=self.get_form_alerts(form.errors), ) self.session.commit() AuditLog.log( self.session, self.current_user.id, "create_permission", "Created permission.", on_permission_id=permission.id, ) # No explicit refresh because handler queries SQL. return self.redirect("/permissions/{}".format(permission.name))
def post(self, group_id=None, name=None, account_id=None, accountname=None): group = Group.get(self.session, group_id, name) if not group: return self.notfound() service_account = ServiceAccount.get(self.session, account_id, accountname) if not service_account: return self.notfound() user = service_account.user if not self.check_access(self.session, self.current_user, service_account): return self.forbidden() grantable = group.my_permissions() form = self.get_form(grantable) if not form.validate(): return self.render( "service-account-permission-grant.html", form=form, user=user, group=group, alerts=self.get_form_alerts(form.errors) ) permission = Permission.get(self.session, form.data["permission"]) if not permission: return self.notfound() allowed = False for perm in grantable: if perm[1] == permission.name: if matches_glob(perm[3], form.data["argument"]): allowed = True break if not allowed: form.argument.errors.append( "The group {} does not have that permission".format(group.name)) return self.render( "service-account-permission-grant.html", form=form, user=user, group=group, alerts=self.get_form_alerts(form.errors) ) try: grant_permission_to_service_account( self.session, service_account, permission, form.data["argument"]) except IntegrityError: self.session.rollback() return self.render( "service-account-permission-grant.html", form=form, user=user, alerts=self.get_form_alerts(form.errors) ) AuditLog.log(self.session, self.current_user.id, "grant_permission", "Granted permission with argument: {}".format(form.data["argument"]), on_permission_id=permission.id, on_group_id=group.id, on_user_id=service_account.user.id) return self.redirect("/groups/{}/service/{}?refresh=yes".format( group.name, service_account.user.username))
def post(self, group_id=None, name=None): group = Group.get(self.session, group_id, name) if not group: return self.notfound() if not user_can_manage_group(self.session, group, self.current_user): return self.forbidden() form = GroupRemoveForm(self.request.arguments) if not form.validate(): return self.send_error(status_code=400) member_type, member_name = form.data["member_type"], form.data["member"] members = group.my_members() if not members.get((member_type.capitalize(), member_name), None): return self.notfound() removed_member = get_user_or_group(self.session, member_name, user_or_group=member_type) if self.current_user == removed_member: return self.send_error( status_code=400, reason="Can't remove yourself. Leave group instead." ) role_user = is_role_user(self.session, group=group) if (role_user and get_role_user(self.session, group=group).user.name == removed_member.name): return self.send_error( status_code=400, reason="Can't remove a service account user from the service account group." ) try: group.revoke_member( self.current_user, removed_member, "Removed by owner/np-owner/manager" ) AuditLog.log(self.session, self.current_user.id, 'remove_from_group', '{} was removed from the group.'.format(removed_member.name), on_group_id=group.id, on_user_id=removed_member.id) except PluginRejectedGroupMembershipUpdate as e: alert = Alert("danger", str(e)) if role_user: return self.redirect("/service/{}".format(group.name), alerts=[alert]) else: return self.redirect("/groups/{}".format(group.name), alerts=[alert]) return self.redirect("/groups/{}?refresh=yes".format(group.name))
def enable_service_account(session, actor, service_account, owner): # type: (Session, User, ServiceAccount, Group) -> None """Enables a service account and sets a new owner.""" enable_user(session, service_account.user, actor, preserve_membership=False) add_service_account(session, owner, service_account) AuditLog.log(session, actor.id, "enable_service_account", "Enabled service account.", on_group_id=owner.id, on_user_id=service_account.user_id) Counter.incr(session, "updates") session.commit()
def post(self, user_id=None, name=None, token_id=None): user = User.get(self.session, user_id, name) if not user: return self.notfound() if (user.name != self.current_user.name) and not self.current_user.user_admin: return self.forbidden() token = UserToken.get(self.session, user=user, id=token_id) disable_user_token(self.session, token) AuditLog.log(self.session, self.current_user.id, 'disable_token', 'Disabled token: {}'.format(token.name), on_user_id=user.id) self.session.commit() return self.render("user-token-disabled.html", token=token)
def disable_service_account(session, actor, service_account): # type: (Session, User, ServiceAccount) -> None """Disables a service account and deletes the association with a Group.""" disable_user(session, service_account.user) owner_id = service_account.owner.group.id service_account.owner.delete(session) permissions = session.query(ServiceAccountPermissionMap).filter_by( service_account_id=service_account.id) for permission in permissions: permission.delete(session) AuditLog.log(session, actor.id, "disable_service_account", "Disabled service account.", on_group_id=owner_id, on_user_id=service_account.user_id) Counter.incr(session, "updates") session.commit()
def post(self, group_id=None, name=None): group = Group.get(self.session, group_id, name) if not group: return self.notfound() members = group.my_members() if not self.current_user.my_role(members) in ("owner", "np-owner"): return self.forbidden() group.enable() self.session.commit() AuditLog.log(self.session, self.current_user.id, 'enable_group', 'Enabled group.', on_group_id=group.id) return self.redirect("/groups/{}?refresh=yes".format(group.name))
def post(self, group_id=None, name=None): group = Group.get(self.session, group_id, name) if not group: return self.notfound() members = group.my_members() if not user_role(self.current_user, members): return self.forbidden() group.revoke_member(self.current_user, self.current_user, "User self-revoked.") AuditLog.log(self.session, self.current_user.id, 'leave_group', '{} left the group.'.format(self.current_user.name), on_group_id=group.id) return self.redirect("/groups/{}?refresh=yes".format(group.name))
def test_promote_nonauditors( mock_gagn, standard_graph, graph, users, groups, session, permissions # noqa: F811 ): """ Test expiration auditing and notification. """ assert graph.get_group_details("audited-team")["audited"] # # Ensure auditors promotion for all approvers # approver_roles = ["owner", "np-owner", "manager"] affected_users = set( ["*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**"]) for idx, role in enumerate(approver_roles): # Add non-auditor as an approver to an audited group add_member(groups["audited-team"], users["*****@*****.**"], role=role) graph.update_from_db(session) assert not affected_users.intersection(get_users(graph, "auditors")) # do the promotion logic settings = BackgroundSettings() set_global_settings(settings) background = BackgroundProcessor(settings, None) background.promote_nonauditors(session) # Check that the users now added to auditors group graph.update_from_db(session) assert affected_users.intersection(get_users( graph, "auditors")) == affected_users unsent_emails = _get_unsent_emails_and_send(session) assert any([ 'Subject: Added as member to group "auditors"' in email.body and "To: [email protected]" in email.body for email in unsent_emails ]) assert any([ 'Subject: Added as member to group "auditors"' in email.body and "To: [email protected]" in email.body for email in unsent_emails ]) assert any([ 'Subject: Added as member to group "auditors"' in email.body and "To: [email protected]" in email.body for email in unsent_emails ]) audits = AuditLog.get_entries(session, action="nonauditor_promoted") assert len(audits) == len(affected_users) * (idx + 1) # reset for next iteration revoke_member(groups["audited-team"], users["*****@*****.**"]) for username in affected_users: revoke_member(groups["auditors"], users[username]) # # Ensure nonauditor, nonapprovers in audited groups do not get promoted # # first, run a promotion to get any other promotion that we don't # care about out of the way background = BackgroundProcessor(settings, None) background.promote_nonauditors(session) prev_audit_log_count = len( AuditLog.get_entries(session, action="nonauditor_promoted")) member_roles = ["member"] for idx, role in enumerate(member_roles): # Add non-auditor as a non-approver to an audited group add_member(groups["audited-team"], users["*****@*****.**"], role=role) # do the promotion logic background = BackgroundProcessor(settings, None) background.promote_nonauditors(session) # Check that the user is not added to auditors group graph.update_from_db(session) assert "*****@*****.**" not in get_users(graph, "auditors") assert not any([ 'Subject: Added as member to group "auditors"' in email.body and "To: [email protected]" in email.body for email in _get_unsent_emails_and_send(session) ]) audits = AuditLog.get_entries(session, action="nonauditor_promoted") assert len(audits) == prev_audit_log_count revoke_member(groups["audited-team"], users["*****@*****.**"])
def notify_edge_expiration(settings, session, edge): """Send notification that an edge has expired. Handles email notification and audit logging. Args: settings (Settings): Grouper Settings object for current run. session (Session): Object for db session. edge (GroupEdge): The expiring edge. """ # TODO(herb): get around circular depdendencies; long term remove call to # send_async_email() from grouper.models from grouper.model_soup import Group from grouper.models.base.constants import OBJ_TYPES_IDX from grouper.models.user import User # TODO(rra): Arbitrarily use the first listed owner of the group from which membership expired # as the actor, since we have to provide an actor and we didn't record who set the expiration on # the edge originally. actor_id = next(edge.group.my_owners().itervalues()).id # Pull data about the edge and the affected user or group. group_name = edge.group.name if OBJ_TYPES_IDX[edge.member_type] == "User": user = User.get(session, pk=edge.member_pk) member_name = user.username recipients = [member_name] member_is_user = True else: subgroup = Group.get(session, pk=edge.member_pk) member_name = subgroup.groupname recipients = subgroup.my_owners_as_strings() member_is_user = False # Log to the audit log. How depends on whether a user's membership has expired or a group's # membership has expired. audit_data = { "action": "expired_from_group", "actor_id": actor_id, "description": "{} expired out of the group".format(member_name), } if member_is_user: AuditLog.log(session, on_user_id=user.id, on_group_id=edge.group_id, **audit_data) else: # Make an audit log entry for both the subgroup and the parent group so that it will show up # in the FE view for both groups. AuditLog.log(session, on_group_id=edge.group_id, **audit_data) AuditLog.log(session, on_group_id=subgroup.id, **audit_data) # Send email notification to the affected people. email_context = { "group_name": group_name, "member_name": member_name, "member_is_user": member_is_user, } send_email( session=session, recipients=recipients, subject="Membership in {} expired".format(group_name), template="expiration", settings=settings, context=email_context, )
def user_command(args): # type: (Namespace) -> None session = make_session() if args.subcommand == "create": for username in args.username: user = User.get(session, name=username) if not user: logging.info("{}: No such user, creating...".format(username)) user = User.get_or_create(session, username=username, role_user=args.role_user) session.commit() else: logging.info( "{}: Already exists. Doing nothing.".format(username)) return elif args.subcommand == "disable": for username in args.username: user = User.get(session, name=username) if not user: logging.info( "{}: No such user. Doing nothing.".format(username)) elif not user.enabled: logging.info( "{}: User already disabled. Doing nothing.".format( username)) else: logging.info("{}: User found, disabling...".format(username)) try: if user.role_user: disable_role_user(session, user) else: disable_user(session, user) AuditLog.log( session, user.id, "disable_user", "(Administrative) User disabled via grouper-ctl", on_user_id=user.id, ) session.commit() except PluginRejectedDisablingUser as e: logging.error(e.message) return elif args.subcommand == "enable": for username in args.username: user = User.get(session, name=username) if not user: logging.info( "{}: No such user. Doing nothing.".format(username)) elif user.enabled: logging.info( "{}: User not disabled. Doing nothing.".format(username)) else: logging.info("{}: User found, enabling...".format(username)) if user.role_user: enable_role_user( session, user, preserve_membership=args.preserve_membership, user=user) else: enable_user(session, user, user, preserve_membership=args.preserve_membership) AuditLog.log( session, user.id, "enable_user", "(Administrative) User enabled via grouper-ctl", on_user_id=user.id, ) session.commit() return # "add_public_key" and "set_metadata" user = User.get(session, name=args.username) if not user: logging.error("{}: No such user. Doing nothing.".format(args.username)) return # User must exist at this point. if args.subcommand == "set_metadata": logging.info("Setting %s metadata: %s=%s", args.username, args.metadata_key, args.metadata_value) if args.metadata_value == "": args.metadata_value = None set_user_metadata(session, user.id, args.metadata_key, args.metadata_value) session.commit() elif args.subcommand == "add_public_key": logging.info("Adding public key for user") try: pubkey = public_key.add_public_key(session, user, args.public_key) except public_key.DuplicateKey: logging.error("Key already in use") return except public_key.PublicKeyParseError: logging.error("Public key appears to be invalid") return AuditLog.log( session, user.id, "add_public_key", "(Administrative) Added public key: {}".format( pubkey.fingerprint_sha256), on_user_id=user.id, )
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None group_id = kwargs.get("group_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] group = Group.get(self.session, group_id, name) if not group or not group.enabled: return self.notfound() form = GroupJoinForm(self.request.arguments) form.member.choices = self._get_choices(group) if not form.validate(): return self.render("group-join.html", form=form, group=group, alerts=self.get_form_alerts(form.errors)) member = self._get_member(form.data["member"]) if not member: return self.render( "group-join.html", form=form, group=group, alerts=[ Alert( "danger", "Unknown user or group: {}".format( form.data["member"])) ], ) fail_message = "This join is denied with this role at this time." try: user_can_join = assert_can_join(group, member, role=form.data["role"]) except UserNotAuditor as e: user_can_join = False fail_message = str(e) if not user_can_join: return self.render( "group-join.html", form=form, group=group, alerts=[ Alert("danger", fail_message, "Audit Policy Enforcement") ], ) if group.canjoin == "nobody": fail_message = "This group cannot be joined at this time." return self.render("group-join.html", form=form, group=group, alerts=[Alert("danger", fail_message)]) if group.require_clickthru_tojoin: if not form.data["clickthru_agreement"]: return self.render( "group-join.html", form=form, group=group, alerts=[ Alert( "danger", "please accept review of the group's description", "Clickthru Enforcement", ) ], ) # We only use the default expiration time if no expiration time was given # This does mean that if a user wishes to join a group with no expiration # (even with an owner's permission) that has an auto expiration, they must # first be accepted to the group and then have the owner edit the user to # have no expiration. expiration = None if form.data["expiration"]: expiration = datetime.strptime(form.data["expiration"], "%m/%d/%Y") elif group.auto_expire: expiration = datetime.utcnow() + group.auto_expire request = group.add_member( requester=self.current_user, user_or_group=member, reason=form.data["reason"], status=GROUP_JOIN_CHOICES[group.canjoin], expiration=expiration, role=form.data["role"], ) self.session.commit() if group.canjoin == "canask": AuditLog.log( self.session, self.current_user.id, "join_group", "{} requested to join with role: {}".format( member.name, form.data["role"]), on_group_id=group.id, ) mail_to = [ user.name for user in group.my_users() if GROUP_EDGE_ROLES[user.role] in ("manager", "owner", "np-owner") ] email_context = { "requester": member.name, "requested_by": self.current_user.name, "request_id": request.id, "group_name": group.name, "reason": form.data["reason"], "expiration": expiration, "role": form.data["role"], "references_header": request.reference_id, } subj = self.render_template("email/pending_request_subj.tmpl", group=group.name, user=self.current_user.name) send_email(self.session, mail_to, subj, "pending_request", settings(), email_context) elif group.canjoin == "canjoin": AuditLog.log( self.session, self.current_user.id, "join_group", "{} auto-approved to join with role: {}".format( member.name, form.data["role"]), on_group_id=group.id, ) else: raise Exception("Need to update the GroupJoin.post audit logging") return self.redirect("/groups/{}?refresh=yes".format(group.name))
def post(self, name=None): grantable = user_grantable_permissions(self.session, self.current_user) if not grantable: return self.forbidden() group = Group.get(self.session, None, name) if not group: return self.notfound() form = PermissionGrantForm(self.request.arguments) form.permission.choices = [["", "(select one)"]] for perm in grantable: grantable_str = "{} ({})".format(perm[0].name, perm[1]) form.permission.choices.append([perm[0].name, grantable_str]) if not form.validate(): return self.render("permission-grant.html", form=form, group=group, alerts=self.get_form_alerts(form.errors)) permission = Permission.get(self.session, form.data["permission"]) if not permission: return self.notfound() # Shouldn't happen. allowed = False for perm in grantable: if perm[0].name == permission.name: if matches_glob(perm[1], form.data["argument"]): allowed = True break if not allowed: form.argument.errors.append( "You do not have grant authority over that permission/argument combination." ) return self.render( "permission-grant.html", form=form, group=group, alerts=self.get_form_alerts(form.errors), ) # If the permission is audited, then see if the subtree meets auditing requirements. if permission.audited: fail_message = ( "Permission is audited and this group (or a subgroup) contains " + "owners, np-owners, or managers who have not received audit training." ) try: permission_ok = assert_controllers_are_auditors(group) except UserNotAuditor as e: permission_ok = False fail_message = e if not permission_ok: form.permission.errors.append(fail_message) return self.render( "permission-grant.html", form=form, group=group, alerts=self.get_form_alerts(form.errors), ) try: grant_permission(self.session, group.id, permission.id, argument=form.data["argument"]) except IntegrityError: self.session.rollback() form.argument.errors.append( "Permission and Argument already mapped to this group.") return self.render( "permission-grant.html", form=form, group=group, alerts=self.get_form_alerts(form.errors), ) self.session.commit() AuditLog.log(self.session, self.current_user.id, 'grant_permission', 'Granted permission with argument: {}'.format( form.data["argument"]), on_permission_id=permission.id, on_group_id=group.id) return self.redirect("/groups/{}?refresh=yes".format(group.name))
def test_github(session, users, http_client, base_url, mocker): # noqa: F811 user = users["*****@*****.**"] assert get_user_metadata_by_key(session, user.id, USER_METADATA_GITHUB_USERNAME_KEY) is None user = User.get(session, name=user.username) fe_url = url(base_url, "/github/link_begin/{}".format(user.id)) mocker.patch.object(settings(), "github_app_client_id", "a-client-id") resp = yield http_client.fetch( fe_url, method="GET", headers={"X-Grouper-User": user.username}, follow_redirects=False, raise_error=False, ) assert resp.code == 302 redir_url = urlparse(resp.headers["Location"]) assert redir_url.netloc == "github.com" assert redir_url.path == "/login/oauth/authorize" query_params = parse_qs(redir_url.query) assert query_params["client_id"] == ["a-client-id"] state, = query_params["state"] assert "github-link-state={}".format(state) in resp.headers["Set-cookie"] assert query_params["redirect_uri"] == [ "http://127.0.0.1:8888/github/link_complete/{}".format(user.id) ] fe_url = url( base_url, "/github/link_complete/{}?code=tempcode&state={}".format( user.id, state)) with pytest.raises(HTTPError) as excinfo: yield http_client.fetch( fe_url, method="GET", headers={ "X-Grouper-User": user.username, "Cookie": "github-link-state=bogus-state" }, ) assert excinfo.value.code == 400 recorder = FakeGitHubHttpClient() proxy_plugin = PluginProxy([SecretPlugin()]) mocker.patch("grouper.fe.handlers.github._get_github_http_client", lambda: recorder) mocker.patch("grouper.fe.handlers.github.get_plugin_proxy", lambda: proxy_plugin) mocker.patch.object(settings(), "http_proxy_host", "proxy-server") mocker.patch.object(settings(), "http_proxy_port", 42) resp = yield http_client.fetch( fe_url, method="GET", headers={ "X-Grouper-User": user.username, "Cookie": "github-link-state=" + state }, ) authorize_request, user_request = recorder.requests assert authorize_request.proxy_host == "proxy-server" assert authorize_request.proxy_port == 42 assert user_request.proxy_host == "proxy-server" assert user_request.proxy_port == 42 authorize_params = parse_qs(authorize_request.body) assert authorize_params[b"code"] == [b"tempcode"] assert authorize_params[b"state"] == [state.encode("ascii")] assert authorize_params[b"client_id"] == [b"a-client-id"] assert authorize_params[b"client_secret"] == [b"client-secret"] assert user_request.headers["Authorization"] == "token a-access-token" assert (get_user_metadata_by_key(session, user.id, USER_METADATA_GITHUB_USERNAME_KEY) is not None) assert (get_user_metadata_by_key( session, user.id, USER_METADATA_GITHUB_USERNAME_KEY).data_value == "zorkian-on-gh") audit_entries = AuditLog.get_entries(session, on_user_id=user.id, action="changed_github_username") assert len(audit_entries) == 1 assert audit_entries[ 0].description == "Changed GitHub username: zorkian-on-gh" fe_url = url(base_url, "/users/{}/github/clear".format(user.username)) resp = yield http_client.fetch(fe_url, method="POST", headers={"X-Grouper-User": user.username}, body=b"") assert resp.code == 200 assert get_user_metadata_by_key(session, user.id, USER_METADATA_GITHUB_USERNAME_KEY) is None audit_entries = AuditLog.get_entries(session, on_user_id=user.id, action="changed_github_username") assert len(audit_entries) == 2 audit_entries.sort(key=operator.attrgetter("id")) assert audit_entries[1].description == "Cleared GitHub link"
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None form = AuditCreateForm(self.request.arguments) if not form.validate(): return self.render( "audit-create.html", form=form, alerts=self.get_form_alerts(form.errors) ) if not user_has_permission(self.session, self.current_user, AUDIT_MANAGER): return self.forbidden() # Step 1, detect if there are non-completed audits and fail if so. open_audits = self.session.query(Audit).filter(Audit.complete == False).all() if open_audits: raise Exception("Sorry, there are audits in progress.") ends_at = datetime.strptime(form.data["ends_at"], "%m/%d/%Y") # Step 2, find all audited groups and schedule audits for each. audited_groups = [] for groupname in self.graph.groups: if not self.graph.get_group_details(groupname)["audited"]: continue group = Group.get(self.session, name=groupname) audit = Audit(group_id=group.id, ends_at=ends_at) try: audit.add(self.session) self.session.flush() except IntegrityError: self.session.rollback() raise Exception("Failed to start the audit. Please try again.") # Update group with new audit audited_groups.append(group) group.audit_id = audit.id # Step 3, now get all members of this group and set up audit rows for those edges. for member in itervalues(group.my_members()): auditmember = AuditMember(audit_id=audit.id, edge_id=member.edge_id) try: auditmember.add(self.session) except IntegrityError: self.session.rollback() raise Exception("Failed to start the audit. Please try again.") self.session.commit() AuditLog.log( self.session, self.current_user.id, "start_audit", "Started global audit.", category=AuditLogCategory.audit, ) # Calculate schedule of emails, basically we send emails at various periods in advance # of the end of the audit period. schedule_times = [] not_before = datetime.utcnow() + timedelta(1) for days_prior in (28, 21, 14, 7, 3, 1): email_time = ends_at - timedelta(days_prior) email_time.replace(hour=17, minute=0, second=0) if email_time > not_before: schedule_times.append((days_prior, email_time)) # Now send some emails. We do this separately/later to ensure that the audits are all # created. Email notifications are sent multiple times if group audits are still # outstanding. for group in audited_groups: mail_to = [ member.name for member in group.my_users() if GROUP_EDGE_ROLES[member.role] in ("owner", "np-owner") ] send_email( self.session, mail_to, "Group Audit: {}".format(group.name), "audit_notice", settings(), {"group": group.name, "ends_at": ends_at}, ) for days_prior, email_time in schedule_times: send_async_email( self.session, mail_to, "Group Audit: {} - {} day(s) left".format(group.name, days_prior), "audit_notice_reminder", settings(), {"group": group.name, "ends_at": ends_at, "days_left": days_prior}, email_time, async_key="audit-{}".format(group.id), ) return self.redirect("/audits")
def my_log_entries(self): return AuditLog.get_entries(self.session, on_group_id=self.id, limit=20)
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None user_id = kwargs.get("user_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = PublicKeyForm(self.request.arguments) if not form.validate(): return self.render( "public-key-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) try: pubkey = public_key.add_public_key(self.session, user, form.data["public_key"]) except public_key.DuplicateKey: form.public_key.errors.append("Key already in use. Public keys must be unique.") except public_key.PublicKeyParseError: form.public_key.errors.append("Public key appears to be invalid.") except public_key.BadPublicKey as e: form.public_key.errors.append(str(e)) if form.public_key.errors: return self.render( "public-key-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) AuditLog.log( self.session, self.current_user.id, "add_public_key", "Added public key: {}".format(pubkey.fingerprint_sha256), on_user_id=user.id, ) email_context = { "actioner": self.current_user.name, "changed_user": user.name, "action": "added", } send_email( self.session, [user.name], "Public SSH key added", "ssh_keys_changed", settings(), email_context, ) return self.redirect("/users/{}?refresh=yes".format(user.name))
def update_request(session, request, user, new_status, comment): """Update a request. Args: session(sqlalchemy.orm.session.Session): database session request(models.PermissionRequest): request to update user(models.User): user making update new_status(models.base.constants.REQUEST_STATUS_CHOICES): new status comment(str): comment to include with status change Raises: grouper.audit.UserNotAuditor in case we're trying to add an audited permission to a group without auditors """ if request.status == new_status: # nothing to do return # make sure the grant can happen if new_status == "actioned": if request.permission.audited: # will raise UserNotAuditor if no auditors are owners of the group assert_controllers_are_auditors(request.group) # all rows we add have the same timestamp now = datetime.utcnow() # new status change row permission_status_change = PermissionRequestStatusChange( request=request, user_id=user.id, from_status=request.status, to_status=new_status, change_at=now, ).add(session) session.flush() # new comment Comment( obj_type=OBJ_TYPES_IDX.index("PermissionRequestStatusChange"), obj_pk=permission_status_change.id, user_id=user.id, comment=comment, created_on=now, ).add(session) # update permissionRequest status request.status = new_status session.commit() if new_status == "actioned": # actually grant permission try: grant_permission(session, request.group.id, request.permission.id, request.argument) except IntegrityError: session.rollback() # audit log AuditLog.log(session, user.id, "update_perm_request", "updated permission request to status: {}".format(new_status), on_group_id=request.group_id, on_user_id=request.requester_id) session.commit() # send notification if new_status == "actioned": subject = "Request for Permission Actioned" email_template = "permission_request_actioned" else: subject = "Request for Permission Cancelled" email_template = "permission_request_cancelled" email_context = { 'group_name': request.group.name, 'action_taken_by': user.name, 'reason': comment, 'permission_name': request.permission.name, 'argument': request.argument, } send_email(session, [request.requester.name], subject, email_template, settings, email_context)
def test_expire_nonauditors(standard_graph, users, groups, session, permissions): """ Test expiration auditing and notification. """ graph = standard_graph # noqa # Test audit autoexpiration for all approvers approver_roles = ["owner", "np-owner", "manager"] for role in approver_roles: # Add non-auditor as an owner to an audited group add_member(groups["audited-team"], users["*****@*****.**"], role=role) session.commit() graph.update_from_db(session) group_md = graph.get_group_details("audited-team") assert group_md.get('audited', False) # Expire the edges. background = BackgroundThread(settings, None) background.expire_nonauditors(session) # Check that the edges are now marked as inactive. edge = session.query(GroupEdge).filter_by( group_id=groups["audited-team"].id, member_pk=users["*****@*****.**"].id).scalar() assert edge.expiration is not None assert edge.expiration < datetime.utcnow() + timedelta( days=settings.nonauditor_expiration_days) assert edge.expiration > datetime.utcnow() + timedelta( days=settings.nonauditor_expiration_days - 1) assert any([ "Subject: Membership in audited-team set to expire" in email.body and "To: [email protected]" in email.body for email in _get_unsent_emails_and_send(session) ]) audits = AuditLog.get_entries(session, action="nonauditor_flagged") assert len(audits) == 3 + 1 * (approver_roles.index(role) + 1) revoke_member(groups["audited-team"], users["*****@*****.**"]) # Ensure nonauditor, nonapprovers in audited groups do not get set to expired member_roles = ["member"] for role in member_roles: # Add non-auditor as an owner to an audited group add_member(groups["audited-team"], users["*****@*****.**"], role=role) session.commit() graph.update_from_db(session) group_md = graph.get_group_details("audited-team") assert group_md.get('audited', False) # Expire the edges. background = BackgroundThread(settings, None) background.expire_nonauditors(session) # Check that the edges are now marked as inactive. edge = session.query(GroupEdge).filter_by( group_id=groups["audited-team"].id, member_pk=users["*****@*****.**"].id).scalar() assert edge.expiration is None assert not any([ "Subject: Membership in audited-team set to expire" in email.body and "To: [email protected]" in email.body for email in _get_unsent_emails_and_send(session) ]) audits = AuditLog.get_entries(session, action="nonauditor_flagged") assert len(audits) == 3 + 1 * len(approver_roles) revoke_member(groups["audited-team"], users["*****@*****.**"])
def post(self, audit_id): if not user_has_permission(self.session, self.current_user, PERMISSION_AUDITOR): return self.forbidden() audit = self.session.query(Audit).filter(Audit.id == audit_id).one() # only owners can complete owner_ids = {member.id for member in audit.group.my_owners().values()} if self.current_user.id not in owner_ids: return self.forbidden() if audit.complete: return self.redirect("/groups/{}".format(audit.group.name)) edges = {} for argument in self.request.arguments: if argument.startswith("audit_"): edges[int(argument.split("_") [1])] = self.request.arguments[argument][0].decode() for audit_member_info in get_group_audit_members_infos( self.session, audit.group): if audit_member_info.audit_member_obj.id in edges: # You can only approve yourself (otherwise you can remove yourself # from the group and leave it ownerless) if audit_member_info.member_obj.id == self.current_user.id: audit_member_info.audit_member_obj.status = "approved" elif edges[audit_member_info.audit_member_obj. id] in AUDIT_STATUS_CHOICES: audit_member_info.audit_member_obj.status = edges[ audit_member_info.audit_member_obj.id] self.session.commit() # If there are still pending statuses, then redirect to the group page. if group_has_pending_audit_members(self.session, audit.group): return self.redirect("/groups/{}".format(audit.group.name)) # Complete audits have to be "enacted" now. This means anybody marked as remove has to # be removed from the group now. try: for audit_member_info in get_group_audit_members_infos( self.session, audit.group): member_obj = audit_member_info.member_obj if audit_member_info.audit_member_obj.status == "remove": audit.group.revoke_member(self.current_user, member_obj, "Revoked as part of audit.") AuditLog.log( self.session, self.current_user.id, "remove_member", "Removed membership in audit: {}".format( member_obj.name), on_group_id=audit.group.id, on_user_id=member_obj.id, category=AuditLogCategory.audit, ) except PluginRejectedGroupMembershipUpdate as e: alert = Alert("danger", str(e)) return self.redirect("/groups/{}".format(audit.group.name), alerts=[alert]) audit.complete = True self.session.commit() # Now cancel pending emails cancel_async_emails(self.session, "audit-{}".format(audit.group.id)) AuditLog.log( self.session, self.current_user.id, "complete_audit", "Completed group audit.", on_group_id=audit.group.id, category=AuditLogCategory.audit, ) # check if all audits are complete if get_audits(self.session, only_open=True).count() == 0: AuditLog.log( self.session, self.current_user.id, "complete_global_audit", "last open audit have been completed", category=AuditLogCategory.audit, ) return self.redirect("/groups/{}".format(audit.group.name))
def test_audit_end_to_end(session, users, groups, http_client, base_url, graph): # noqa: F811 """ Tests an end-to-end audit cycle. """ groupname = "audited-team" gary_id = users["*****@*****.**"].id # make everyone an auditor or global audit will have issues add_member(groups["auditors"], users["*****@*****.**"]) add_member(groups["auditors"], users["*****@*****.**"]) add_member(groups["auditors"], users["*****@*****.**"]) add_member(groups["auditors"], users["*****@*****.**"]) # add some users to test removal add_member(groups[groupname], users["*****@*****.**"]) add_member(groups[groupname], users["*****@*****.**"]) graph.update_from_db(session) # start the audit end_at_str = (datetime.now() + timedelta(days=10)).strftime("%m/%d/%Y") fe_url = url(base_url, "/audits/create") resp = yield http_client.fetch( fe_url, method="POST", body=urlencode({"ends_at": end_at_str}), headers={"X-Grouper-User": "******"}, ) assert resp.code == 200 open_audits = get_audits(session, only_open=True).all() assert len(open_audits) == 4, "audits created" assert groupname in [x.group.name for x in open_audits ], "group we expect also gets audit" # pull all the info we need to resolve audits, avoids detached sqlalchemy sessions # (DetachedInstanceError) MyAuditMemberInfo = NamedTuple("MyAuditMemberInfo", [("am_id", int), ("edge_type", int), ("edge_id", int)]) Audit = NamedTuple( "Audit", [ ("audit_id", int), ("owner_name", str), ("group_name", str), ("audit_members_infos", List[MyAuditMemberInfo]), ], ) all_group_ids = [x.group.id for x in open_audits] open_audits = [ Audit( x.id, next(iter(x.group.my_owners())), x.group.name, [ MyAuditMemberInfo( ami.audit_member_obj.id, ami.audit_member_obj.edge.member_type, ami.audit_member_obj.edge_id, ) for ami in get_group_audit_members_infos(session, x.group) ], ) for x in open_audits ] # approve everything but the one we added members to for one_audit in open_audits: fe_url = url(base_url, "/audits/{}/complete".format(one_audit.audit_id)) if one_audit.group_name == groupname: continue # blanket approval body = urlencode({ "audit_{}".format(ami.am_id): "approved" for ami in one_audit.audit_members_infos }) resp = yield http_client.fetch( fe_url, method="POST", body=body, headers={"X-Grouper-User": one_audit.owner_name}) assert resp.code == 200 open_audits = get_audits(session, only_open=True).all() assert len(open_audits) == 1, "only our test group remaining" one_audit = open_audits[0] one_audit.id body_dict = {} for ami in get_group_audit_members_infos(session, one_audit.group): if gary_id == ami.member_obj.id: # deny body_dict["audit_{}".format(ami.audit_member_obj.id)] = "remove" else: # approve body_dict["audit_{}".format(ami.audit_member_obj.id)] = "approved" owner_name = next(iter(one_audit.group.my_owners())) fe_url = url(base_url, "/audits/{}/complete".format(one_audit.id)) resp = yield http_client.fetch(fe_url, method="POST", body=urlencode(body_dict), headers={"X-Grouper-User": owner_name}) assert resp.code == 200 # check all the logs assert len(AuditLog.get_entries( session, action="start_audit")) == 1, "global start is logged" assert (len(AuditLog.get_entries( session, action="complete_global_audit")) == 1), "global complete is logged" for group_id in all_group_ids: assert (len( AuditLog.get_entries( session, on_group_id=group_id, action="complete_audit", category=AuditLogCategory.audit, )) == 1), "complete entry for each group" assert (len( AuditLog.get_entries(session, on_user_id=gary_id, category=AuditLogCategory.audit)) == 1 ), "removal AuditLog entry on user"
def post(self, group_id=None, name=None): group = Group.get(self.session, group_id, name) if not group: return self.notfound() if not user_can_manage_group(self.session, group, self.current_user): return self.forbidden() members = group.my_members() my_role = user_role(self.current_user, members) form = self.get_form(role=my_role) if not form.validate(): return self.render( "group-add.html", form=form, group=group, alerts=self.get_form_alerts(form.errors) ) member = get_user_or_group(self.session, form.data["member"]) if member.type == "User" and is_service_account(self.session, member): # For service accounts, we want to always add the group to other groups, not the user member = get_service_account(self.session, user=member).group if not member: form.member.errors.append("User or group not found.") elif (member.type, member.name) in group.my_members(): form.member.errors.append("User or group is already a member of this group.") elif group.name == member.name: form.member.errors.append("By definition, this group is a member of itself already.") # Ensure this doesn't violate auditing constraints fail_message = 'This join is denied with this role at this time.' try: user_can_join = assert_can_join(group, member, role=form.data["role"]) except UserNotAuditor as e: user_can_join = False fail_message = e if not user_can_join: form.member.errors.append(fail_message) if form.member.errors: return self.render( "group-add.html", form=form, group=group, alerts=self.get_form_alerts(form.errors) ) expiration = None if form.data["expiration"]: expiration = datetime.strptime(form.data["expiration"], "%m/%d/%Y") try: group.add_member( requester=self.current_user, user_or_group=member, reason=form.data["reason"], status='actioned', expiration=expiration, role=form.data["role"] ) except InvalidRoleForMember as e: return self.render( "group-add.html", form=form, group=group, alerts=[ Alert('danger', e.message) ] ) self.session.commit() on_user_id = member.id if member.type == "User" else None AuditLog.log(self.session, self.current_user.id, 'join_group', '{} added to group with role: {}'.format( member.name, form.data["role"]), on_group_id=group.id, on_user_id=on_user_id) if member.type == "User": send_email( self.session, [member.name], 'Added to group: {}'.format(group.name), 'request_actioned', settings, { 'group_name': group.name, 'actioned_by': self.current_user.name, 'reason': form.data['reason'], 'expiration': expiration, 'role': form.data['role'], } ) return self.redirect("/groups/{}?refresh=yes".format(group.name))
def post(self, audit_id): user = self.get_current_user() if not user_has_permission(self.session, user, PERMISSION_AUDITOR): return self.forbidden() audit = self.session.query(Audit).filter(Audit.id == audit_id).one() # only owners can complete owner_ids = {member.id for member in audit.group.my_owners().values()} if user.id not in owner_ids: return self.forbidden() if audit.complete: return self.redirect("/groups/{}".format(audit.group.name)) edges = {} for argument in self.request.arguments: if argument.startswith('audit_'): edges[int(argument.split('_')[1])] = self.request.arguments[argument][0] for member in audit.my_members(): if member.id in edges: # You can only approve yourself (otherwise you can remove yourself # from the group and leave it ownerless) if member.member.id == user.id: member.status = "approved" elif edges[member.id] in AUDIT_STATUS_CHOICES: member.status = edges[member.id] self.session.commit() # Now if it's completable (no pendings) then mark it complete, else redirect them # to the group page. if not audit.completable: return self.redirect('/groups/{}'.format(audit.group.name)) # Complete audits have to be "enacted" now. This means anybody marked as remove has to # be removed from the group now. try: for member in audit.my_members(): if member.status == "remove": audit.group.revoke_member(self.current_user, member.member, "Revoked as part of audit.") AuditLog.log(self.session, self.current_user.id, 'remove_member', 'Removed membership in audit: {}'.format(member.member.name), on_group_id=audit.group.id, on_user_id=member.member.id, category=AuditLogCategory.audit) except PluginRejectedGroupMembershipUpdate as e: alert = Alert("danger", str(e)) return self.redirect('/groups/{}'.format(audit.group.name), alerts=[alert]) audit.complete = True self.session.commit() # Now cancel pending emails cancel_async_emails(self.session, 'audit-{}'.format(audit.group.id)) AuditLog.log(self.session, self.current_user.id, 'complete_audit', 'Completed group audit.', on_group_id=audit.group.id, category=AuditLogCategory.audit) # check if all audits are complete if get_audits(self.session, only_open=True).count() == 0: AuditLog.log(self.session, self.current_user.id, 'complete_global_audit', 'last open audit have been completed', category=AuditLogCategory.audit) return self.redirect('/groups/{}'.format(audit.group.name))
def post(self, request_id, group_id=None, name=None): group = Group.get(self.session, group_id, name) if not group: return self.notfound() members = group.my_members() my_role = user_role(self.current_user, members) if my_role not in ("manager", "owner", "np-owner"): return self.forbidden() request = self.session.query(Request).filter_by(id=request_id).scalar() if not request: return self.notfound() form = GroupRequestModifyForm(self.request.arguments) form.status.choices = self._get_choices(request.status) updates = request.my_status_updates() if not form.status.choices: alerts = (Alert("info", "Request has already been processed"),) return self.render( "group-request-update.html", group=group, request=request, members=members, form=form, alerts=alerts, statuses=REQUEST_STATUS_CHOICES, updates=updates ) if not form.validate(): return self.render( "group-request-update.html", group=group, request=request, members=members, form=form, alerts=self.get_form_alerts(form.errors), statuses=REQUEST_STATUS_CHOICES, updates=updates ) # We have to test this here, too, to ensure that someone can't sneak in with a pending # request that used to be allowed. if form.data["status"] != "cancelled": fail_message = 'This join is denied with this role at this time.' try: user_can_join = assert_can_join(request.requesting, request.get_on_behalf(), role=request.edge.role) except UserNotAuditor as e: user_can_join = False fail_message = e if not user_can_join: return self.render( "group-request-update.html", group=group, request=request, members=members, form=form, statuses=REQUEST_STATUS_CHOICES, updates=updates, alerts=[ Alert('danger', fail_message, 'Audit Policy Enforcement') ] ) request.update_status( self.current_user, form.data["status"], form.data["reason"] ) self.session.commit() AuditLog.log(self.session, self.current_user.id, 'update_request', 'Updated request to status: {}'.format(form.data["status"]), on_group_id=group.id, on_user_id=request.requester.id) edge = self.session.query(GroupEdge).filter_by( id=request.edge_id ).one() approver_mail_to = [ user.name for user in group.my_approver_users() if user.name != self.current_user.name and user.name != request.requester.username ] subj = "Re: " + self.render_template( 'email/pending_request_subj.tmpl', group=group.name, user=request.requester.username ) send_email( self.session, approver_mail_to, subj, "approver_request_updated", settings, { 'group_name': group.name, 'requester': request.requester.username, 'changed_by': self.current_user.name, 'status': form.data['status'], 'role': edge.role, 'reason': form.data['reason'], 'references_header': request.reference_id, }, ) if form.data['status'] == 'actioned': send_email( self.session, [request.requester.name], 'Added to group: {}'.format(group.groupname), 'request_actioned', settings, { 'group_name': group.name, 'actioned_by': self.current_user.name, 'reason': form.data['reason'], 'expiration': edge.expiration, 'role': edge.role, } ) elif form.data['status'] == 'cancelled': send_email( self.session, [request.requester.name], 'Request to join cancelled: {}'.format(group.groupname), 'request_cancelled', settings, { 'group_name': group.name, 'cancelled_by': self.current_user.name, 'reason': form.data['reason'], 'expiration': edge.expiration, 'role': edge.role, } ) # No explicit refresh because handler queries SQL. if form.data['redirect_aggregate']: return self.redirect("/user/requests") else: return self.redirect("/groups/{}/requests".format(group.name))