예제 #1
0
def add_expiration(
        session,  # type: Session
        expiration,  # type: datetime
        group_name,  # type: str
        member_name,  # type: str
        recipients,  # type: List[str]
        member_is_user,  # type: bool
):
    # type: (...) -> None
    async_key = _expiration_key(group_name, member_name)
    send_after = expiration - timedelta(settings().expiration_notice_days)
    email_context = {
        "expiration": expiration,
        "group_name": group_name,
        "member_name": member_name,
        "member_is_user": member_is_user,
    }  # type: Context

    send_async_email(
        session=session,
        recipients=recipients,
        subject="expiration warning for membership in group '{}'".format(
            group_name),
        template="expiration_warning",
        settings=settings(),
        context=email_context,
        send_after=send_after,
        async_key=async_key,
    )
예제 #2
0
def add_expiration(
    session,  # type: Session
    expiration,  # type: datetime
    group_name,  # type: str
    member_name,  # type: str
    recipients,  # type: List[str]
    member_is_user,  # type: bool
):
    # type: (...) -> None
    async_key = _expiration_key(group_name, member_name)
    send_after = expiration - timedelta(settings().expiration_notice_days)
    email_context = {
        "expiration": expiration,
        "group_name": group_name,
        "member_name": member_name,
        "member_is_user": member_is_user,
    }  # type: Context

    send_async_email(
        session=session,
        recipients=recipients,
        subject="expiration warning for membership in group '{}'".format(group_name),
        template="expiration_warning",
        settings=settings(),
        context=email_context,
        send_after=send_after,
        async_key=async_key,
    )
예제 #3
0
def expiring_graph(session, graph, users, groups, permissions):  # noqa: F811
    now = datetime.utcnow()
    note_exp_now = now + timedelta(settings().expiration_notice_days)
    week = timedelta(7)

    add_member(groups["team-sre"], users["*****@*****.**"], role="owner")
    add_member(groups["team-sre"], users["*****@*****.**"], role="owner")
    add_member(groups["team-sre"], users["*****@*****.**"], expiration=note_exp_now + week)
    add_member(groups["team-sre"], users["*****@*****.**"])
    add_member(
        groups["team-sre"], users["*****@*****.**"], role="owner", expiration=note_exp_now + week
    )
    revoke_member(groups["team-sre"], users["*****@*****.**"])
    grant_permission(groups["team-sre"], permissions["ssh"], argument="*")

    add_member(groups["serving-team"], users["*****@*****.**"], role="owner")
    add_member(groups["serving-team"], groups["team-sre"], expiration=note_exp_now + week)
    add_member(groups["serving-team"], groups["tech-ops"])
    grant_permission(groups["serving-team"], permissions["audited"])

    add_member(groups["tech-ops"], users["*****@*****.**"], role="owner")
    add_member(groups["tech-ops"], users["*****@*****.**"], expiration=note_exp_now + 2 * week)
    grant_permission(groups["tech-ops"], permissions["ssh"], argument="shell")

    return graph
예제 #4
0
def test_shell(session, users, http_client, base_url):  # noqa: F811
    settings().shell = [["/bin/bash", "bash"], ["/bin/zsh", "zsh"]]

    user = users["*****@*****.**"]
    assert not get_user_metadata_by_key(session, user.id,
                                        USER_METADATA_SHELL_KEY)

    user = User.get(session, name=user.username)
    fe_url = url(base_url, "/users/{}/shell".format(user.username))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({"shell": "/bin/bash"}),
        headers={"X-Grouper-User": user.username},
    )
    assert resp.code == 200

    user = User.get(session, name=user.username)

    assert (get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY)
            is not None), "The user should have shell metadata"
    assert (get_user_metadata_by_key(
        session, user.id, USER_METADATA_SHELL_KEY).data_value == "/bin/bash")

    fe_url = url(base_url, "/users/{}/shell".format(user.username))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({"shell": "/bin/fish"}),
        headers={"X-Grouper-User": user.username},
    )
    assert resp.code == 200

    user = User.get(session, name=user.username)

    assert (get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY)
            is not None), "The user should have shell metadata"
    assert (get_user_metadata_by_key(
        session, user.id, USER_METADATA_SHELL_KEY).data_value == "/bin/bash")

    fe_url = url(base_url, "/users/{}/shell".format(user.username))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({"shell": "/bin/zsh"}),
        headers={"X-Grouper-User": user.username},
    )
    assert resp.code == 200

    user = User.get(session, name=user.username)

    assert (get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY)
            is not None), "The user should have shell metadata"
    assert (get_user_metadata_by_key(
        session, user.id, USER_METADATA_SHELL_KEY).data_value == "/bin/zsh")
예제 #5
0
def test_metadata(session, users, http_client, base_url):  # noqa: F811
    settings().metadata_options = {
        "favorite_food": [["pizza", "pizza"], ["kale", "kale"]]
    }

    user = users["*****@*****.**"]
    assert not get_user_metadata_by_key(session, user.id, "favorite_food")

    user = User.get(session, name=user.username)
    set_user_metadata(session, user.id, "favorite_food", "default")
    fe_url = url(base_url,
                 "/users/{}/metadata/favorite_food".format(user.username))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({"value": "pizza"}),
        headers={"X-Grouper-User": user.username},
    )
    assert resp.code == 200

    assert get_user_metadata_by_key(session, user.id,
                                    "favorite_food").data_value == "pizza"

    fe_url = url(base_url,
                 "/users/{}/metadata/favorite_food".format(user.username))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({"value": "kale"}),
        headers={"X-Grouper-User": user.username},
    )

    assert get_user_metadata_by_key(session, user.id,
                                    "favorite_food").data_value == "kale"

    fe_url = url(base_url,
                 "/users/{}/metadata/favorite_food".format(user.username))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({"value": "donuts"}),
        headers={"X-Grouper-User": user.username},
    )

    assert get_user_metadata_by_key(session, user.id,
                                    "favorite_food").data_value == "kale"
예제 #6
0
def update_request(session: Session, request: PermissionRequest, user: User,
                   new_status: str, comment: str) -> None:
    """Update a request.

    Args:
        session: Database session
        request: Request to update
        user: User making update
        new_status: New status
        comment: 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,
        on_permission_id=request.permission.id,
    )

    session.commit()

    # send notification

    template_engine = EmailTemplateEngine(settings())
    subject_template = template_engine.get_template(
        "email/pending_permission_request_subj.tmpl")
    subject = "Re: " + subject_template.render(
        permission=request.permission.name, group=request.group.name)

    if new_status == "actioned":
        email_template = "permission_request_actioned"
    else:
        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)
예제 #7
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
예제 #8
0
 def reference_id(self):
     # type: () -> str
     return reference_id(settings(), "permission", self)
예제 #9
0
def test_expiration_notifications(
    expiring_graph, session, users, groups, permissions  # noqa: F811
):
    now = datetime.utcnow()
    note_exp_now = now + timedelta(settings().expiration_notice_days)
    day = timedelta(1)
    week = timedelta(7)

    # What expirations are coming up in the next day?  Next week?
    upcoming_expirations = _get_unsent_expirations(session, now + day)
    assert upcoming_expirations == []

    upcoming_expirations = _get_unsent_expirations(session, now + week)
    assert sorted(upcoming_expirations) == [
        # Group, subgroup, subgroup owners.
        ("serving-team", "team-sre", "*****@*****.**"),
        ("serving-team", "team-sre", "*****@*****.**"),
        # Group, user, user.
        ("team-sre", "*****@*****.**", "*****@*****.**"),
    ]

    # Make someone expire a week from now.
    edit_member(groups["team-sre"], users["*****@*****.**"], expiration=note_exp_now + week)
    upcoming_expirations = _get_unsent_expirations(session, now + week)
    assert sorted(upcoming_expirations) == [
        # Group, subgroup, subgroup owners.
        ("serving-team", "team-sre", "*****@*****.**"),
        ("serving-team", "team-sre", "*****@*****.**"),
        # Group, user, user.
        ("team-sre", "*****@*****.**", "*****@*****.**"),
        ("team-sre", "*****@*****.**", "*****@*****.**"),
    ]

    # Now cancel that expiration.
    edit_member(groups["team-sre"], users["*****@*****.**"], expiration=None)
    upcoming_expirations = _get_unsent_expirations(session, now + week)
    assert sorted(upcoming_expirations) == [
        # Group, subgroup, subgroup owners.
        ("serving-team", "team-sre", "*****@*****.**"),
        ("serving-team", "team-sre", "*****@*****.**"),
        # Group, user, user.
        ("team-sre", "*****@*****.**", "*****@*****.**"),
    ]

    # Make an ordinary member an owner.
    edit_member(groups["team-sre"], users["*****@*****.**"], role="owner")
    upcoming_expirations = _get_unsent_expirations(session, now + week)
    assert sorted(upcoming_expirations) == [
        # Group, subgroup, subgroup owners.
        ("serving-team", "team-sre", "*****@*****.**"),
        ("serving-team", "team-sre", "*****@*****.**"),
        ("serving-team", "team-sre", "*****@*****.**"),
        # Group, user, user.
        ("team-sre", "*****@*****.**", "*****@*****.**"),
    ]

    # Make an owner an ordinary member.
    edit_member(groups["team-sre"], users["*****@*****.**"], role="member")
    upcoming_expirations = _get_unsent_expirations(session, now + week)
    assert sorted(upcoming_expirations) == [
        # Group, subgroup, subgroup owners.
        ("serving-team", "team-sre", "*****@*****.**"),
        ("serving-team", "team-sre", "*****@*****.**"),
        # Group, user, user.
        ("team-sre", "*****@*****.**", "*****@*****.**"),
    ]

    # Send notices about expirations coming up in the next day, next week.
    notices_sent = process_async_emails(settings(), session, now + day, dry_run=True)
    assert notices_sent == 0

    notices_sent = process_async_emails(settings(), session, now + week, dry_run=True)
    assert notices_sent == 3
    # ("serving-team", "team-sre", "*****@*****.**")
    # ("serving-team", "team-sre", "*****@*****.**")
    # ("team-sre", "*****@*****.**", "*****@*****.**")

    # Notices in the upcoming week have already been sent, but there's another
    # two weeks from now.
    upcoming_expirations = _get_unsent_expirations(session, now + week)
    assert upcoming_expirations == []

    upcoming_expirations = _get_unsent_expirations(session, now + 2 * week)
    assert upcoming_expirations == [("tech-ops", "*****@*****.**", "*****@*****.**")]

    # We already sent these notices.
    notices_sent = process_async_emails(settings(), session, now + week, dry_run=True)
    assert notices_sent == 0

    # Extend gary's membership to beyond worth mentioning expiration in two weeks.
    add_member(groups["tech-ops"], users["*****@*****.**"], expiration=note_exp_now + 3 * week)

    upcoming_expirations = _get_unsent_expirations(session, now + 2 * week)
    assert upcoming_expirations == []

    notices_sent = process_async_emails(settings(), session, now + 2 * week, dry_run=True)
    assert notices_sent == 0
예제 #10
0
def test_shell(session, users, http_client, base_url):  # noqa: F811
    settings().shell = [["/bin/bash", "bash"], ["/bin/zsh", "zsh"]]

    user = users["*****@*****.**"]
    assert not get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY)

    user = User.get(session, name=user.username)
    fe_url = url(base_url, "/users/{}/shell".format(user.username))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({"shell": "/bin/bash"}),
        headers={"X-Grouper-User": user.username},
    )
    assert resp.code == 200

    user = User.get(session, name=user.username)

    assert (
        get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY) is not None
    ), "The user should have shell metadata"
    assert (
        get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY).data_value
        == "/bin/bash"
    )

    fe_url = url(base_url, "/users/{}/shell".format(user.username))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({"shell": "/bin/fish"}),
        headers={"X-Grouper-User": user.username},
    )
    assert resp.code == 200

    user = User.get(session, name=user.username)

    assert (
        get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY) is not None
    ), "The user should have shell metadata"
    assert (
        get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY).data_value
        == "/bin/bash"
    )

    fe_url = url(base_url, "/users/{}/shell".format(user.username))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({"shell": "/bin/zsh"}),
        headers={"X-Grouper-User": user.username},
    )
    assert resp.code == 200

    user = User.get(session, name=user.username)

    assert (
        get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY) is not None
    ), "The user should have shell metadata"
    assert (
        get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY).data_value
        == "/bin/zsh"
    )
예제 #11
0
파일: request.py 프로젝트: yasaswyk/merou
 def reference_id(self):
     # type: () -> str
     return reference_id(settings(), "group", self)
예제 #12
0
def update_request(
    session,  # type: Session
    request,  # type: PermissionRequest
    user,  # type: User
    new_status,  # type: str
    comment,  # type: str
):
    # type: (...) -> None
    """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,
        on_permission_id=request.permission.id,
    )

    session.commit()

    # send notification

    template_engine = EmailTemplateEngine(settings())
    subject_template = template_engine.get_template("email/pending_permission_request_subj.tmpl")
    subject = "Re: " + subject_template.render(
        permission=request.permission.name, group=request.group.name
    )

    if new_status == "actioned":
        email_template = "permission_request_actioned"
    else:
        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
    )
예제 #13
0
def create_request(session, user, group, permission, argument, reason):
    # type: (Session, User, Group, Permission, str, str) -> PermissionRequest
    """
    Creates an permission request and sends notification to the responsible approvers.

    Args:
        session(sqlalchemy.orm.session.Session): database session
        user(models.User): user requesting permission
        group(models.Group): group requested permission would be applied to
        permission(models.Permission): permission in question to request
        argument(str): argument for the given permission
        reason(str): reason the permission should be granted

    Raises:
        RequestAlreadyExists if trying to create a request that is already pending
        NoOwnersAvailable if no owner is available for the requested perm + arg.
        grouper.audit.UserNotAuditor if 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