Beispiel #1
0
 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
Beispiel #2
0
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})
Beispiel #3
0
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]
Beispiel #4
0
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()
    )
Beispiel #5
0
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,
            ])
Beispiel #6
0
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
Beispiel #7
0
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()
Beispiel #8
0
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
Beispiel #9
0
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
        )
    ]
Beispiel #10
0
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()
Beispiel #11
0
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())
Beispiel #12
0
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()
Beispiel #13
0
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)
Beispiel #14
0
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
Beispiel #15
0
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
Beispiel #16
0
 def get(session: Session, name: str) -> Permission:
     return session.query(Permission).filter_by(name=name).scalar()