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 = GroupEditForm(self.request.arguments, obj=group) if not form.validate(): return self.render("group-edit.html", group=group, form=form, alerts=self.get_form_alerts(form.errors)) group.groupname = form.data["groupname"] group.description = form.data["description"] group.canjoin = form.data["canjoin"] Counter.incr(self.session, "updates") try: self.session.commit() except IntegrityError: self.session.rollback() form.groupname.errors.append("{} already exists".format(form.data["groupname"])) return self.render("group-edit.html", group=group, form=form, alerts=self.get_form_alerts(form.errors)) AuditLog.log(self.session, self.current_user.id, "edit_group", "Edited group.", on_group_id=group.id) return self.redirect("/groups/{}".format(group.name))
def post(self): form = GroupCreateForm(self.request.arguments) if not form.validate(): return self.render( "group-create.html", form=form, alerts=self.get_form_alerts(form.errors) ) user = self.get_current_user() group = Group( groupname=form.data["groupname"], description=form.data["description"], canjoin=form.data["canjoin"] ) try: group.add(self.session) self.session.flush() except IntegrityError: self.session.rollback() form.groupname.errors.append( "{} already exists".format(form.data["groupname"]) ) return self.render( "group-create.html", form=form, alerts=self.get_form_alerts(form.errors) ) group.add_member(user, user, "Group Creator", "actioned", None, form.data["creatorrole"]) self.session.commit() AuditLog.log(self.session, self.current_user.id, 'create_group', 'Created new group.', on_group_id=group.id) return self.redirect("/groups/{}?refresh=yes".format(group.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 (user.name != self.current_user.name) and not self.current_user.user_admin: return self.forbidden() key = self.session.query(PublicKey).filter_by(id=key_id, user_id=user.id).scalar() if not key: return self.notfound() key.delete(self.session) self.session.commit() 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 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) 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 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.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): can_create = self.current_user.my_creatable_permissions() 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), ) permission = Permission(name=form.data["name"], description=form.data["description"]) try: permission.add(self.session) 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=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, 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.current_user, user): return self.forbidden() user.disable() 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 test_expire_edges(expired_graph, session): # noqa """ Test expiration auditing and notification. """ email = session.query(AsyncNotification).all() assert email == [] for edge in session.query(GroupEdge).all(): assert edge.active == True # Expire the edges. background = BackgroundThread(settings, None) background.expire_edges(session) # Check that the edges are now marked as inactive. edges = ( session.query(GroupEdge) .filter(GroupEdge.group_id == Group.id, Group.enabled == True, GroupEdge.expiration != None) .all() ) for edge in edges: assert edge.active == False # Check that we have two queued email messages. # # TODO(rra): It would be nice to check the contents as well. email = session.query(AsyncNotification).all() assert len(email) == 2 # Check that we have three audit log entries: one for the expired user and # two for both "sides" of the expired group membership. audits = AuditLog.get_entries(session, action="expired_from_group") assert len(audits) == 3
def post(self, name=None): if not self.current_user.permission_admin: return self.forbidden() permission = Permission.get(self.session, name) if not permission: return self.notfound() permission.enable_auditing() self.session.commit() AuditLog.log(self.session, self.current_user.id, 'enable_auditing', 'Enabled auditing.', on_permission_id=permission.id) # No explicit refresh because handler queries SQL. return self.redirect("/permissions/{}".format(permission.name))
def get(self): user = self.get_current_user() if not (user.has_permission(AUDIT_VIEWER) or user.has_permission(AUDIT_MANAGER)): return self.forbidden() offset = int(self.get_argument("offset", 0)) limit = int(self.get_argument("limit", 50)) if limit > 200: limit = 200 open_filter = self.get_argument("filter", "Open Audits") audits = get_audits(self.session, only_open=(open_filter == "Open Audits")) open_audits = any([not audit.complete for audit in audits]) total = audits.count() audits = audits.offset(offset).limit(limit).all() open_audits = self.session.query(Audit).filter( Audit.complete == False).all() can_start = user.has_permission(AUDIT_MANAGER) # FIXME(herb): make limit selected from ui audit_log_entries = AuditLog.get_entries(self.session, category=AuditLogCategory.audit, limit=100) self.render( "audits.html", audits=audits, open_filter=open_filter, can_start=can_start, offset=offset, limit=limit, total=total, open_audits=open_audits, audit_log_entries=audit_log_entries, )
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): 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 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, user_id=None, name=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() 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.PublicKeyParseError: form.public_key.errors.append( "Key failed to parse and is invalid." ) return self.render( "public-key-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) except public_key.DuplicateKey: form.public_key.errors.append( "Key already in use. Public keys must be unique." ) 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), 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 user_command(args): 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 # "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": print "Setting %s metadata: %s=%s" % (args.username, args.metadata_key, args.metadata_value) if args.metadata_value == "": args.metadata_value = None user.set_metadata(args.metadata_key, args.metadata_value) session.commit() elif args.subcommand == "add_public_key": print "Adding public key for user..." try: pubkey = public_key.add_public_key(session, user, args.public_key) except public_key.DuplicateKey: print "Key already in use." return except public_key.PublicKeyParseError: print "Public key appears to be invalid." return AuditLog.log(session, user.id, 'add_public_key', '(Administrative) Added public key: {}'.format(pubkey.fingerprint), on_user_id=user.id)
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) token.disable() 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 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.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)) user.enable(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 group_command(args): session = make_session() group = session.query(Group).filter_by(groupname=args.groupname).scalar() if not group: logging.error("No such group %s".format(args.groupname)) return for username in args.username: user = User.get(session, name=username) if not user: logging.error("no such user '{}'".format(username)) return if args.subcommand == "add_member": if args.member: role = 'member' elif args.owner: role = 'owner' elif args.np_owner: role = 'np-owner' elif args.manager: role = 'manager' assert role logging.info("Adding {} as {} to group {}".format(username, role, args.groupname)) group.add_member(user, user, "grouper-ctl join", status="actioned", role=role) AuditLog.log( session, user.id, 'join_group', '{} manually joined via grouper-ctl'.format(username), on_group_id=group.id) session.commit() elif args.subcommand == "remove_member": logging.info("Removing {} from group {}".format(username, args.groupname)) group.revoke_member(user, user, "grouper-ctl remove") AuditLog.log( session, user.id, 'leave_group', '{} manually left via grouper-ctl'.format(username), on_group_id=group.id) session.commit()
def post(self, user_id=None, name=None): user = User.get(self.session, user_id, name) if not user: return self.notfound() if user.name != self.current_user.name: 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 = UserToken(name=form.data["name"], user=user).add(self.session) 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, 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 = self.current_user.my_role(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.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() if form.data['status'] == 'actioned': send_email( self.session, [request.requester.name], 'Added to group: {}'.format(group.groupname), 'request_actioned', settings, { 'group': 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': 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))
def post(self, group_id=None, name=None): group = Group.get(self.session, group_id, name) if not group: 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"]) 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: 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) ] ) expiration = None if form.data["expiration"]: expiration = datetime.strptime(form.data["expiration"], "%m/%d/%Y") 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, "requested": group.name, "reason": form.data["reason"], "expiration": expiration, "role": form.data["role"], } send_email(self.session, mail_to, 'Request to join: {}'.format(group.name), '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): form = AuditCreateForm(self.request.arguments) if not form.validate(): return self.render( "audit-create.html", form=form, alerts=self.get_form_alerts(form.errors) ) user = self.get_current_user() if not user.has_permission(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 group.my_members().values(): 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 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.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.flush() if new_status == "actioned": # actually grant permission request.group.grant_permission(request.permission, request.argument) Counter.incr(session, "updates") # 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) # 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 post(self, audit_id): user = self.get_current_user() if not user.has_permission(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. 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) 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): # noqa """ Tests an end-to-end audit cycle. """ groupname = 'audited-team' zay_id = users["*****@*****.**"].id 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["*****@*****.**"]) # 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 AuditMember = namedtuple('AuditMember', 'am_id, edge_type, edge_id') Audit = namedtuple('Audit', 'audit_id, owner_name, group_name, audit_members') all_group_ids = [x.group.id for x in open_audits] open_audits = [Audit(x.id, x.group.my_owners().iterkeys().next(), x.group.name, [AuditMember(am.id, am.edge.member_type, am.edge_id) for am in x.my_members()]) 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(am.am_id): "approved" for am in one_audit.audit_members}) 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 am in one_audit.my_members(): if gary_id == am.member.id: # deny body_dict["audit_{}".format(am.id)] = "remove" else: # approve body_dict["audit_{}".format(am.id)] = "approved" owner_name = one_audit.group.my_owners().iterkeys().next() 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, name=None): grantable = self.current_user.my_grantable_permissions() 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 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: group.grant_permission(permission, argument=form.data["argument"]) except IntegrityError: 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 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.models import AuditLog, Group, OBJ_TYPES_IDX, 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, )