def get(session: Session, pk: Optional[int] = None, name: Optional[str] = None) -> Optional[Group]: if pk is not None: return session.query(Group).filter_by(id=pk).scalar() if name is not None: return session.query(Group).filter_by(groupname=name).scalar() return None
def cancel_async_emails(session: Session, async_key: str) -> None: """Cancel pending async emails by key If you scheduled an asynchronous email with an async_key previously, this method can be used to cancel any unsent emails. Args: async_key: The async_key previously provided for your emails. """ session.query(AsyncNotification).filter( AsyncNotification.key == async_key, AsyncNotification.sent == False).update({"sent": True})
def get_changes_by_request_id( session: Session, request_id: int ) -> List[Tuple[PermissionRequestStatusChange, Comment]]: status_changes = (session.query(PermissionRequestStatusChange).filter( PermissionRequestStatusChange.request_id == request_id).all()) comments = (session.query(Comment).filter( Comment.obj_type == OBJ_TYPES_IDX.index( "PermissionRequestStatusChange"), Comment.obj_pk.in_([s.id for s in status_changes]), ).all()) comment_by_status_change_id = {c.obj_pk: c for c in comments} return [(sc, comment_by_status_change_id[sc.id]) for sc in status_changes]
def get_pending_request_by_group(session: Session, group: Group) -> List[PermissionRequest]: """Load pending request for a particular group.""" return ( session.query(PermissionRequest) .filter(PermissionRequest.status == "pending", PermissionRequest.group_id == group.id) .all() )
def logdump_group_command(session: Session, group: Group, args: Namespace) -> None: log_entries = session.query(AuditLog).filter( AuditLog.on_group_id == group.id, AuditLog.log_time > args.start_date) if args.end_date: log_entries = log_entries.filter(AuditLog.log_time <= args.end_date) with open_file_or_stdout_for_write(args.outfile) as fh: csv_w = csv.writer(fh) for log_entry in log_entries: if log_entry.on_user: extra = "user: {}".format(log_entry.on_user.username) elif log_entry.on_group: extra = "group: {}".format(log_entry.on_group.groupname) else: extra = "" csv_w.writerow([ log_entry.log_time, log_entry.actor, log_entry.description, log_entry.action, extra, ])
def process_async_emails( settings: Settings, session: Session, now_ts: datetime, dry_run: bool = False ) -> int: """Send emails due before now This method finds and immediately sends any emails that have been scheduled to be sent before the now_ts. Meant to be called from the background processing thread. Args: settings: The current Settings object for this application. session: Object for db session. now_ts: The time to use as the cutoff (send emails before this point). dry_run: If True, do not actually send any email, just generate and return how many emails would have been sent. Returns: Number of emails that were sent. """ emails = ( session.query(AsyncNotification) .filter(AsyncNotification.sent == False, AsyncNotification.send_after < now_ts) .all() ) sent_ct = 0 for email in emails: # For atomicity, attempt to set the sent flag on this email to true if # and only if it's still false. update_ct = ( session.query(AsyncNotification) .filter(AsyncNotification.id == email.id, AsyncNotification.sent == False) .update({"sent": True}) ) # If it's 0, someone else won the race. Bail. if update_ct == 0: continue try: if not dry_run: send_email_raw(settings, email.email, email.body) email.sent = True sent_ct += 1 except smtplib.SMTPException: # Any sort of error with sending the email and we want to move on to # the next email. This email will be retried later. pass return sent_ct
def group_has_pending_audit_members(session: Session, group: Group) -> bool: """Check if a group still has memberships with "pending" audit status.""" members_edge_ids = {member.edge_id for member in group.my_members().values()} audit_members_statuses = session.query(AuditMember.status).filter( AuditMember.audit_id == group.audit_id, AuditMember.status == "pending", # only those members who have not left the group after the audit started AuditMember.edge_id.in_(members_edge_ids), ) return audit_members_statuses.count()
def get_audits(session: Session, only_open: bool) -> Query: """Return audits in the system. Args: session: Database session only_open: Whether to filter by open audits """ query = session.query(Audit).order_by(Audit.started_at) if only_open: query = query.filter(Audit.complete == False) return query
def get_group_audit_members_infos(session: Session, group: Group) -> List[AuditMemberInfo]: """Get audit information about the members of a group. Note that only current members of the group are relevant, i.e., members of the group at the time the current audit was started but are no longer part of the group are excluded, as are members of the group added after the audit was started. """ members_edge_ids = {member.edge_id for member in group.my_members().values()} user_members = ( session.query(AuditMember, GroupEdge._role, User) .filter( AuditMember.audit_id == group.audit_id, AuditMember.edge_id == GroupEdge.id, GroupEdge.member_type == OBJ_TYPES["User"], GroupEdge.member_pk == User.id, # only those members who have not left the group after the audit started AuditMember.edge_id.in_(members_edge_ids), ) .all() ) group_members = ( session.query(AuditMember, GroupEdge._role, Group) .filter( AuditMember.audit_id == group.audit_id, AuditMember.edge_id == GroupEdge.id, GroupEdge.member_type == OBJ_TYPES["Group"], GroupEdge.member_pk == Group.id, # only those members who have not left the group after the audit started AuditMember.edge_id.in_(members_edge_ids), ) .all() ) return [ AuditMemberInfo(audit_member, audit_member_role, member_obj) for audit_member, audit_member_role, member_obj in itertools.chain( user_members, group_members ) ]
def get_all_permissions(session: Session, include_disabled: bool = False) -> List[Permission]: """Get permissions that exist in the database. Can retrieve either only enabled permissions, or both enabled and disabled ones. Args: session: Database session include_disabled: True to include disabled permissions (make sure you really want this) """ query = session.query(Permission) if not include_disabled: query = query.filter(Permission.enabled == True) return query.order_by(asc(Permission.name)).all()
def get_groups_by_permission( session: Session, permission: Permission) -> List[Tuple[Group, str]]: """Return the groups granted a permission and their associated arguments. For an enabled permission, return the groups and associated arguments that have that permission. If the permission is disabled, return empty list. Returns: List of 2-tuple of the form (Group, argument). """ if not permission.enabled: return [] return (session.query(Group.groupname, PermissionMap.argument, PermissionMap.granted_on).filter( Group.id == PermissionMap.group_id, PermissionMap.permission_id == permission.id, Group.enabled == True, ).all())
def get_request_by_id(session: Session, request_id: int) -> Optional[PermissionRequest]: """Get a single request by the request ID.""" return session.query(PermissionRequest).filter( PermissionRequest.id == request_id).one()
def get_requests( session: Session, status: str, limit: int, offset: int, owner: Optional[User] = None, requester: Optional[User] = None, owners_by_arg_by_perm: Optional[Dict[object, Dict[str, List[Group]]]] = None, ) -> Tuple[Requests, int]: """Load requests using the given filters. Args: session: Database session status: If not None, filter by particular status limit: how many results to return offset: the offset into the result set that should be applied owner: If not None, filter by requests that the owner can action requester: If not None, filter by requests that the requester made owners_by_arg_by_perm: List of groups that can grant a given permission, argument pair in the format of {perm_name: {argument: [group1, group2, ...], ...}, ...} This is for convenience/caching if the value has already been fetched. Returns: 2-tuple of (Requests, total) where total is total result size and Requests is the data transfer object with requests and associated comments/changes. """ # get all requests all_requests = session.query(PermissionRequest) if status: all_requests = all_requests.filter(PermissionRequest.status == status) if requester: all_requests = all_requests.filter( PermissionRequest.requester_id == requester.id) all_requests = all_requests.order_by( PermissionRequest.requested_at.desc()).all() if owners_by_arg_by_perm is None: owners_by_arg_by_perm = get_owners_by_grantable_permission(session) if owner: group_ids = {g.id for g, _ in get_groups_by_user(session, owner)} requests = [ request for request in all_requests if can_approve_request( session, request, owner, group_ids=group_ids, owners_by_arg_by_perm=owners_by_arg_by_perm, ) ] else: requests = all_requests total = len(requests) requests = requests[offset:limit] status_change_by_request_id: Dict[ int, List[PermissionRequestStatusChange]] = defaultdict(list) if not requests: comment_by_status_change_id: Dict[int, Comment] = {} else: status_changes = (session.query(PermissionRequestStatusChange).filter( PermissionRequestStatusChange.request_id.in_( [r.id for r in requests])).all()) for sc in status_changes: status_change_by_request_id[sc.request_id].append(sc) comments = (session.query(Comment).filter( Comment.obj_type == OBJ_TYPES_IDX.index( "PermissionRequestStatusChange"), Comment.obj_pk.in_([s.id for s in status_changes]), ).all()) comment_by_status_change_id = {c.obj_pk: c for c in comments} return (Requests(requests, status_change_by_request_id, comment_by_status_change_id), total)
def create_request(session: Session, user: User, group: Group, permission: Permission, argument: str, reason: str) -> PermissionRequest: """Creates an permission request and sends notification to the responsible approvers. Args: session: Database session user: User requesting permission group: Group requested permission would be applied to permission: Permission in question to request argument: argument for the given permission reason: reason the permission should be granted Raises: RequestAlreadyExists: Trying to create a request that is already pending NoOwnersAvailable: No owner is available for the requested perm + arg. grouper.audit.UserNotAuditor: The group has owners that are not auditors """ # check if group already has perm + arg pair for _, existing_perm_name, _, existing_perm_argument, _ in group.my_permissions( ): if permission.name == existing_perm_name and argument == existing_perm_argument: raise RequestAlreadyGranted() # check if request already pending for this perm + arg pair existing_count = (session.query(PermissionRequest).filter( PermissionRequest.group_id == group.id, PermissionRequest.permission_id == permission.id, PermissionRequest.argument == argument, PermissionRequest.status == "pending", ).count()) if existing_count > 0: raise RequestAlreadyExists() # determine owner(s) owners_by_arg_by_perm = get_owners_by_grantable_permission( session, separate_global=True) owner_arg_list = get_owner_arg_list( session, permission, argument, owners_by_arg_by_perm=owners_by_arg_by_perm) if not owner_arg_list: raise NoOwnersAvailable() if permission.audited: # will raise UserNotAuditor if any owner of the group is not an auditor assert_controllers_are_auditors(group) pending_status = "pending" now = datetime.utcnow() # multiple steps to create the request request = PermissionRequest( requester_id=user.id, group_id=group.id, permission_id=permission.id, argument=argument, status=pending_status, requested_at=now, ).add(session) session.flush() request_status_change = PermissionRequestStatusChange( request=request, user=user, to_status=pending_status, change_at=now).add(session) session.flush() Comment( obj_type=OBJ_TYPES_IDX.index("PermissionRequestStatusChange"), obj_pk=request_status_change.id, user_id=user.id, comment=reason, created_on=now, ).add(session) # send notification email_context = { "user_name": user.name, "group_name": group.name, "permission_name": permission.name, "argument": argument, "reason": reason, "request_id": request.id, "references_header": request.reference_id, } # TODO: would be nicer if it told you which group you're an approver of # that's causing this notification mail_to = [] global_owners = owners_by_arg_by_perm[GLOBAL_OWNERS]["*"] non_wildcard_owners = [ grant for grant in owner_arg_list if grant[1] != "*" ] non_global_owners = [ grant for grant in owner_arg_list if grant[0] not in global_owners ] if any(non_wildcard_owners): # non-wildcard owners should get all the notifications mailto_owner_arg_list = non_wildcard_owners elif any(non_global_owners): mailto_owner_arg_list = non_global_owners else: # only the wildcards so they get the notifications mailto_owner_arg_list = owner_arg_list for owner, arg in mailto_owner_arg_list: if owner.email_address: mail_to.append(owner.email_address) else: mail_to.extend([u for t, u in owner.my_members() if t == "User"]) template_engine = EmailTemplateEngine(settings()) subject_template = template_engine.get_template( "email/pending_permission_request_subj.tmpl") subject = subject_template.render(permission=permission.name, group=group.name) send_email(session, set(mail_to), subject, "pending_permission_request", settings(), email_context) return request
def get_owners_by_grantable_permission( session: Session, separate_global: bool = False) -> Dict[object, Dict[str, List[Group]]]: """Returns all known permission arguments with owners. This consolidates permission grants supported by grouper itself as well as any grants governed by plugins. Args: session: Database session separate_global: Whether to construct a specific entry for GLOBAL_OWNER in the output map Returns: A map of permission to argument to owners of the form: {permission: {argument: [owner1, ...], }, } where owners are Group objects. argument can be '*' which means anything. """ all_permissions = { permission.name: permission for permission in get_all_permissions(session) } all_groups = session.query(Group).filter(Group.enabled == True).all() owners_by_arg_by_perm: Dict[object, Dict[str, List[Group]]] = defaultdict( lambda: defaultdict(list)) all_group_permissions = (session.query( Permission.name, PermissionMap.argument, PermissionMap.granted_on, Group).filter(PermissionMap.group_id == Group.id, Permission.id == PermissionMap.permission_id).all()) grants_by_group: Dict[str, List[Any]] = defaultdict(list) for grant in all_group_permissions: grants_by_group[grant.Group.id].append(grant) for group in all_groups: # special case permission admins group_permissions = grants_by_group[group.id] if any([g.name == PERMISSION_ADMIN for g in group_permissions]): for perm_name in all_permissions: owners_by_arg_by_perm[perm_name]["*"].append(group) if separate_global: owners_by_arg_by_perm[GLOBAL_OWNERS]["*"].append(group) continue grants = [ gp for gp in group_permissions if gp.name == PERMISSION_GRANT ] for perm, arg in filter_grantable_permissions( session, grants, all_permissions=all_permissions): owners_by_arg_by_perm[perm.name][arg].append(group) # merge in plugin results for res in get_plugin_proxy().get_owner_by_arg_by_perm(session): for permission_name, owners_by_arg in res.items(): for arg, owners in owners_by_arg.items(): owners_by_arg_by_perm[permission_name][arg] += owners return owners_by_arg_by_perm
def get(session: Session, name: str) -> Permission: return session.query(Permission).filter_by(name=name).scalar()