示例#1
0
def get_user_view_template_vars(session, actor, user, graph):
    # type: (Session, User, User, GroupGraph) -> Dict[str, Any]
    # TODO(cbguder): get around circular dependencies
    from grouper.fe.handlers.user_disable import UserDisable
    from grouper.fe.handlers.user_enable import UserEnable

    ret = {}  # type: Dict[str, Any]
    if user.is_service_account:
        ret["can_control"] = can_manage_service_account(
            session, user.service_account, actor
        ) or user_is_user_admin(session, actor)
        ret["can_disable"] = ret["can_control"]
        ret["can_enable"] = user_is_user_admin(session, actor)
        ret["can_enable_preserving_membership"] = user_is_user_admin(session, actor)
        ret["account"] = user.service_account
    else:
        ret["can_control"] = user.name == actor.name or user_is_user_admin(session, actor)
        ret["can_disable"] = UserDisable.check_access(session, actor, user)
        ret["can_enable_preserving_membership"] = UserEnable.check_access(session, actor, user)
        ret["can_enable"] = UserEnable.check_access_without_membership(session, actor, user)

    if user.id == actor.id:
        ret["num_pending_group_requests"] = user_requests_aggregate(session, actor).count()
        _, ret["num_pending_perm_requests"] = get_requests(
            session, status="pending", limit=1, offset=0, owner=actor
        )
    else:
        ret["num_pending_group_requests"] = None
        ret["num_pending_perm_requests"] = None

    try:
        user_md = graph.get_user_details(user.name)
    except NoSuchUser:
        # Either user is probably very new, so they have no metadata yet, or
        # they're disabled, so we've excluded them from the in-memory graph.
        user_md = {}

    shell_metadata = get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY)
    ret["shell"] = shell_metadata.data_value if shell_metadata else "No shell configured"
    github_username = get_user_metadata_by_key(session, user.id, USER_METADATA_GITHUB_USERNAME_KEY)
    ret["github_username"] = github_username.data_value if github_username else "(Unset)"
    ret["open_audits"] = user_open_audits(session, user)
    group_edge_list = get_groups_by_user(session, user) if user.enabled else []
    ret["groups"] = [
        {"name": g.name, "type": "Group", "role": ge._role} for g, ge in group_edge_list
    ]
    ret["passwords"] = user_passwords(session, user)
    ret["public_keys"] = get_public_keys_of_user(session, user.id)
    ret["log_entries"] = get_log_entries_by_user(session, user)
    ret["user_tokens"] = user.tokens

    if user.is_service_account:
        service_account = user.service_account
        ret["permissions"] = service_account_permissions(session, service_account)
    else:
        ret["permissions"] = user_md.get("permissions", [])
        for permission in ret["permissions"]:
            permission["granted_on"] = datetime.fromtimestamp(permission["granted_on"])

    return ret
    def post(self, request_id):
        # check for request existence
        request = permissions.get_request_by_id(self.session, request_id)
        if not request:
            return self.notfound()

        # check that this user should be actioning this request
        user_requests, total = permissions.get_requests(
            self.session,
            status="pending",
            limit=None,
            offset=0,
            owner=self.current_user)
        user_request_ids = [ur.id for ur in user_requests.requests]
        if request.id not in user_request_ids:
            return self.forbidden()

        form = PermissionRequestUpdateForm(self.request.arguments)
        form.status.choices = self._get_choices(request.status)
        if not form.validate():
            change_comment_list = [
                (sc, user_requests.comment_by_status_change_id[sc.id])
                for sc in user_requests.status_change_by_request_id[request.id]
            ]

            return self.render(
                "permission-request-update.html",
                form=form,
                request=request,
                change_comment_list=change_comment_list,
                statuses=REQUEST_STATUS_CHOICES,
                alerts=self.get_form_alerts(form.errors),
            )

        try:
            permissions.update_request(self.session, request,
                                       self.current_user, form.status.data,
                                       form.reason.data)
        except UserNotAuditor as e:
            alerts = [Alert("danger", str(e))]

            change_comment_list = [
                (sc, user_requests.comment_by_status_change_id[sc.id])
                for sc in user_requests.status_change_by_request_id[request.id]
            ]

            return self.render(
                "permission-request-update.html",
                form=form,
                request=request,
                change_comment_list=change_comment_list,
                statuses=REQUEST_STATUS_CHOICES,
                alerts=alerts,
            )

        return self.redirect("/permissions/requests?status=pending")
    def post(self, request_id):
        # check for request existence
        request = permissions.get_request_by_id(self.session, request_id)
        if not request:
            return self.notfound()

        # check that this user should be actioning this request
        user_requests, total = permissions.get_requests(
            self.session, status="pending", limit=None, offset=0, owner=self.current_user
        )
        user_request_ids = [ur.id for ur in user_requests.requests]
        if request.id not in user_request_ids:
            return self.forbidden()

        form = PermissionRequestUpdateForm(self.request.arguments)
        form.status.choices = self._get_choices(request.status)
        if not form.validate():
            change_comment_list = [
                (sc, user_requests.comment_by_status_change_id[sc.id])
                for sc in user_requests.status_change_by_request_id[request.id]
            ]

            return self.render(
                "permission-request-update.html",
                form=form,
                request=request,
                change_comment_list=change_comment_list,
                statuses=REQUEST_STATUS_CHOICES,
                alerts=self.get_form_alerts(form.errors),
            )

        try:
            permissions.update_request(
                self.session, request, self.current_user, form.status.data, form.reason.data
            )
        except UserNotAuditor as e:
            alerts = [Alert("danger", str(e))]

            change_comment_list = [
                (sc, user_requests.comment_by_status_change_id[sc.id])
                for sc in user_requests.status_change_by_request_id[request.id]
            ]

            return self.render(
                "permission-request-update.html",
                form=form,
                request=request,
                change_comment_list=change_comment_list,
                statuses=REQUEST_STATUS_CHOICES,
                alerts=alerts,
            )

        return self.redirect("/permissions/requests?status=pending")
示例#4
0
    def get(self):
        form = PermissionRequestsForm(self.request.arguments)
        form.status.choices = [("", "")] + [(k, k)
                                            for k in REQUEST_STATUS_CHOICES]

        if not form.validate():
            alerts = self.get_form_alerts(form.errors)
            request_tuple = None
            total = 0
            granters_by_arg_by_perm = None
        else:
            alerts = []
            owners_by_arg_by_perm = permissions.get_owners_by_grantable_permission(
                self.session)
            if form.direction.data == "Waiting my approval":
                owner = self.current_user
                requester = None
            else:  # "Requested by me"
                owner = None
                requester = self.current_user

            request_tuple, total = permissions.get_requests(
                self.session,
                status=form.status.data,
                limit=form.limit.data,
                offset=form.offset.data,
                owner=owner,
                requester=requester,
                owners_by_arg_by_perm=owners_by_arg_by_perm,
            )
            granters_by_arg_by_perm = defaultdict(dict)
            for request in request_tuple.requests:
                owners = permissions.get_owner_arg_list(
                    self.session,
                    request.permission,
                    request.argument,
                    owners_by_arg_by_perm=owners_by_arg_by_perm,
                )
                granters = [owner_pair[0].name for owner_pair in owners]
                granters_by_arg_by_perm[request.permission.name][
                    request.argument] = granters

        return self.render(
            "permission-requests.html",
            form=form,
            request_tuple=request_tuple,
            granters=granters_by_arg_by_perm,
            alerts=alerts,
            total=total,
            statuses=REQUEST_STATUS_CHOICES,
        )
示例#5
0
    def get(self):
        form = PermissionRequestsForm(self.request.arguments)
        form.status.choices = [("", "")] + [(k, k) for k in REQUEST_STATUS_CHOICES]

        if not form.validate():
            alerts = self.get_form_alerts(form.errors)
            request_tuple = None
            total = 0
            granters_by_arg_by_perm = None
        else:
            alerts = []
            owners_by_arg_by_perm = permissions.get_owners_by_grantable_permission(self.session)
            if form.direction.data == "Waiting my approval":
                owner = self.current_user
                requester = None
            else:  # "Requested by me"
                owner = None
                requester = self.current_user

            request_tuple, total = permissions.get_requests(
                self.session,
                status=form.status.data,
                limit=form.limit.data,
                offset=form.offset.data,
                owner=owner,
                requester=requester,
                owners_by_arg_by_perm=owners_by_arg_by_perm,
            )
            granters_by_arg_by_perm = defaultdict(dict)
            for request in request_tuple.requests:
                owners = permissions.get_owner_arg_list(
                    self.session,
                    request.permission,
                    request.argument,
                    owners_by_arg_by_perm=owners_by_arg_by_perm,
                )
                granters = [owner_pair[0].name for owner_pair in owners]
                granters_by_arg_by_perm[request.permission.name][request.argument] = granters

        return self.render(
            "permission-requests.html",
            form=form,
            request_tuple=request_tuple,
            granters=granters_by_arg_by_perm,
            alerts=alerts,
            total=total,
            statuses=REQUEST_STATUS_CHOICES,
        )
示例#6
0
def test_permission_request_flow(
        session,
        standard_graph,
        groups,
        grantable_permissions,
        http_client,
        base_url  # noqa: F811
):
    """Test that a permission request gets into the system correctly and
    notifications are sent correctly."""
    perm_grant, _, perm1, perm2 = grantable_permissions
    grant_permission(groups["all-teams"], perm_grant, argument="grantable.*")
    grant_permission(groups["security-team"],
                     perm_grant,
                     argument="grantable.one")
    grant_permission(groups["tech-ops"], perm_grant, argument="grantable.two")

    # REQUEST: permission with an invalid argument
    groupname = "serving-team"
    username = "******"
    fe_url = url(base_url, "/groups/{}/permission/request".format(groupname))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({
            "permission_name": "grantable.one",
            "argument": "some argument?",
            "reason": "blah blah black sheep",
            "argument_type": "text",
        }),
        headers={"X-Grouper-User": username},
    )
    assert resp.code == 200
    assert b"Field must match" in resp.body
    emails = _get_unsent_and_mark_as_sent_emails(session)
    assert len(emails) == 0, "no emails queued"

    # REQUEST: 'grantable.one', 'some argument' for 'serving-team'
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({
            "permission_name": "grantable.one",
            "argument": "some argument",
            "reason": "blah blah black sheep",
            "argument_type": "text",
        }),
        headers={"X-Grouper-User": username},
    )
    assert resp.code == 200

    emails = _get_unsent_and_mark_as_sent_emails(session)
    assert_same_recipients(emails, ["*****@*****.**", "*****@*****.**"])

    perms = _load_permissions_by_group_name(session, "serving-team")
    assert len(perms) == 1
    assert "grantable.one" not in perms, "requested permission shouldn't be granted immediately"

    user = User.get(session, name="*****@*****.**")
    request_tuple, total = get_requests(session, "pending", 10, 0, owner=user)
    assert len(
        request_tuple.requests) == 0, "random user shouldn't have a request"

    user = User.get(session, name="*****@*****.**")
    request_tuple, total = get_requests(session, "pending", 10, 0, owner=user)
    assert len(request_tuple.requests
               ) == 1, "user in group with grant should have a request"

    # APPROVE grant: have '*****@*****.**' action this request as owner of
    # 'all-teams' which has the grant permission for the requested permission
    request_id = request_tuple.requests[0].id
    fe_url = url(base_url, "/permissions/requests/{}".format(request_id))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({
            "status": "actioned",
            "reason": "lgtm"
        }),
        headers={"X-Grouper-User": user.name},
    )
    assert resp.code == 200

    perms = _load_permissions_by_group_name(session, "serving-team")
    assert len(perms) == 2
    assert "grantable.one" in perms, "requested permission shouldn't be granted immediately"

    emails = _get_unsent_and_mark_as_sent_emails(session)
    assert_same_recipients(emails, ["*****@*****.**"])

    # (re)REQUEST: 'grantable.one', 'some argument' for 'serving-team'
    groupname = "serving-team"
    username = "******"
    fe_url = url(base_url, "/groups/{}/permission/request".format(groupname))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({
            "permission_name": "grantable.one",
            "argument": "some argument",
            "reason": "blah blah black sheep",
            "argument_type": "text",
        }),
        headers={"X-Grouper-User": username},
    )
    assert resp.code == 200

    user = User.get(session, name="*****@*****.**")
    request_tuple, total = get_requests(session, "pending", 10, 0, owner=user)
    assert len(
        request_tuple.requests) == 0, "request for existing perm should fail"

    # REQUEST: 'grantable.two', 'some argument' for 'serving-team'
    groupname = "serving-team"
    username = "******"
    fe_url = url(base_url, "/groups/{}/permission/request".format(groupname))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({
            "permission_name": "grantable.two",
            "argument": "some argument",
            "reason": "blah blah black sheep",
            "argument_type": "text",
        }),
        headers={"X-Grouper-User": username},
    )
    assert resp.code == 200

    emails = _get_unsent_and_mark_as_sent_emails(session)
    # because tech-ops team doesn't have an email, all of its members should get emailed instead
    assert_same_recipients(
        emails, ["*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**"])

    perms = _load_permissions_by_group_name(session, "serving-team")
    assert len(perms) == 2
    assert "grantable.two" not in perms, "requested permission shouldn't be granted immediately"

    user = User.get(session, name="*****@*****.**")
    request_tuple, total = get_requests(session, "pending", 10, 0, owner=user)
    assert len(
        request_tuple.requests) == 0, "random user shouldn't have a request"

    user = User.get(session, name="*****@*****.**")
    request_tuple, total = get_requests(session, "pending", 10, 0, owner=user)
    assert len(request_tuple.requests
               ) == 1, "user in group with grant should have a request"

    # CANCEL request: have '*****@*****.**' cancel this request
    request_id = request_tuple.requests[0].id
    fe_url = url(base_url, "/permissions/requests/{}".format(request_id))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({
            "status": "cancelled",
            "reason": "heck no"
        }),
        headers={"X-Grouper-User": user.name},
    )
    assert resp.code == 200

    emails = _get_unsent_and_mark_as_sent_emails(session)
    assert_same_recipients(emails, ["*****@*****.**"])

    perms = _load_permissions_by_group_name(session, "serving-team")
    assert len(perms) == 2
    assert "grantable.two" not in perms, "no new permissions should be granted for this"
示例#7
0
def get_user_view_template_vars(session, actor, user, graph):
    # type: (Session, User, User, GroupGraph) -> Dict[str, Any]
    # TODO(cbguder): get around circular dependencies
    from grouper.fe.handlers.user_disable import UserDisable
    from grouper.fe.handlers.user_enable import UserEnable

    ret = {}  # type: Dict[str, Any]
    if user.is_service_account:
        ret["can_control"] = can_manage_service_account(
            session, user.service_account, actor
        ) or user_is_user_admin(session, actor)
        ret["can_disable"] = ret["can_control"]
        ret["can_enable"] = user_is_user_admin(session, actor)
        ret["can_enable_preserving_membership"] = user_is_user_admin(session, actor)
        ret["account"] = user.service_account
    else:
        ret["can_control"] = user.name == actor.name or user_is_user_admin(session, actor)
        ret["can_disable"] = UserDisable.check_access(session, actor, user)
        ret["can_enable_preserving_membership"] = UserEnable.check_access(session, actor, user)
        ret["can_enable"] = UserEnable.check_access_without_membership(session, actor, user)

    if user.id == actor.id:
        ret["num_pending_group_requests"] = user_requests_aggregate(session, actor).count()
        _, ret["num_pending_perm_requests"] = get_requests(
            session, status="pending", limit=1, offset=0, owner=actor
        )
    else:
        ret["num_pending_group_requests"] = None
        ret["num_pending_perm_requests"] = None

    try:
        user_md = graph.get_user_details(user.name)
    except NoSuchUser:
        # Either user is probably very new, so they have no metadata yet, or
        # they're disabled, so we've excluded them from the in-memory graph.
        user_md = {}

    shell = (
        get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY).data_value
        if get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY)
        else "No shell configured"
    )
    ret["shell"] = shell
    github_username = get_user_metadata_by_key(session, user.id, USER_METADATA_GITHUB_USERNAME_KEY)
    ret["github_username"] = github_username.data_value if github_username else "(Unset)"
    ret["open_audits"] = user_open_audits(session, user)
    group_edge_list = get_groups_by_user(session, user) if user.enabled else []
    ret["groups"] = [
        {"name": g.name, "type": "Group", "role": ge._role} for g, ge in group_edge_list
    ]
    ret["passwords"] = user_passwords(session, user)
    ret["public_keys"] = get_public_keys_of_user(session, user.id)
    ret["log_entries"] = get_log_entries_by_user(session, user)
    ret["user_tokens"] = user.tokens

    if user.is_service_account:
        service_account = user.service_account
        ret["permissions"] = service_account_permissions(session, service_account)
    else:
        ret["permissions"] = user_md.get("permissions", [])
        for permission in ret["permissions"]:
            permission["granted_on"] = datetime.fromtimestamp(permission["granted_on"])

    return ret
示例#8
0
def test_permission_request_flow(
    session, standard_graph, groups, grantable_permissions, http_client, base_url  # noqa: F811
):
    """Test that a permission request gets into the system correctly and
    notifications are sent correctly."""
    perm_grant, _, perm1, perm2 = grantable_permissions
    grant_permission(groups["all-teams"], perm_grant, argument="grantable.*")
    grant_permission(groups["security-team"], perm_grant, argument="grantable.one")
    grant_permission(groups["tech-ops"], perm_grant, argument="grantable.two")

    # REQUEST: permission with an invalid argument
    groupname = "serving-team"
    username = "******"
    fe_url = url(base_url, "/groups/{}/permission/request".format(groupname))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode(
            {
                "permission_name": "grantable.one",
                "argument": "some argument?",
                "reason": "blah blah black sheep",
                "argument_type": "text",
            }
        ),
        headers={"X-Grouper-User": username},
    )
    assert resp.code == 200
    assert b"Field must match" in resp.body
    emails = _get_unsent_and_mark_as_sent_emails(session)
    assert len(emails) == 0, "no emails queued"

    # REQUEST: 'grantable.one', 'some argument' for 'serving-team'
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode(
            {
                "permission_name": "grantable.one",
                "argument": "some argument",
                "reason": "blah blah black sheep",
                "argument_type": "text",
            }
        ),
        headers={"X-Grouper-User": username},
    )
    assert resp.code == 200

    emails = _get_unsent_and_mark_as_sent_emails(session)
    assert_same_recipients(emails, [u"*****@*****.**", u"*****@*****.**"])

    perms = _load_permissions_by_group_name(session, "serving-team")
    assert len(perms) == 1
    assert "grantable.one" not in perms, "requested permission shouldn't be granted immediately"

    user = User.get(session, name="*****@*****.**")
    request_tuple, total = get_requests(session, "pending", 10, 0, owner=user)
    assert len(request_tuple.requests) == 0, "random user shouldn't have a request"

    user = User.get(session, name="*****@*****.**")
    request_tuple, total = get_requests(session, "pending", 10, 0, owner=user)
    assert len(request_tuple.requests) == 1, "user in group with grant should have a request"

    # APPROVE grant: have '*****@*****.**' action this request as owner of
    # 'all-teams' which has the grant permission for the requested permission
    request_id = request_tuple.requests[0].id
    fe_url = url(base_url, "/permissions/requests/{}".format(request_id))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({"status": "actioned", "reason": "lgtm"}),
        headers={"X-Grouper-User": user.name},
    )
    assert resp.code == 200

    perms = _load_permissions_by_group_name(session, "serving-team")
    assert len(perms) == 2
    assert "grantable.one" in perms, "requested permission shouldn't be granted immediately"

    emails = _get_unsent_and_mark_as_sent_emails(session)
    assert_same_recipients(emails, [u"*****@*****.**"])

    # (re)REQUEST: 'grantable.one', 'some argument' for 'serving-team'
    groupname = "serving-team"
    username = "******"
    fe_url = url(base_url, "/groups/{}/permission/request".format(groupname))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode(
            {
                "permission_name": "grantable.one",
                "argument": "some argument",
                "reason": "blah blah black sheep",
                "argument_type": "text",
            }
        ),
        headers={"X-Grouper-User": username},
    )
    assert resp.code == 200

    user = User.get(session, name="*****@*****.**")
    request_tuple, total = get_requests(session, "pending", 10, 0, owner=user)
    assert len(request_tuple.requests) == 0, "request for existing perm should fail"

    # REQUEST: 'grantable.two', 'some argument' for 'serving-team'
    groupname = "serving-team"
    username = "******"
    fe_url = url(base_url, "/groups/{}/permission/request".format(groupname))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode(
            {
                "permission_name": "grantable.two",
                "argument": "some argument",
                "reason": "blah blah black sheep",
                "argument_type": "text",
            }
        ),
        headers={"X-Grouper-User": username},
    )
    assert resp.code == 200

    emails = _get_unsent_and_mark_as_sent_emails(session)
    # because tech-ops team doesn't have an email, all of its members should get emailed instead
    assert_same_recipients(
        emails, [u"*****@*****.**", u"*****@*****.**", u"*****@*****.**", u"*****@*****.**"]
    )

    perms = _load_permissions_by_group_name(session, "serving-team")
    assert len(perms) == 2
    assert "grantable.two" not in perms, "requested permission shouldn't be granted immediately"

    user = User.get(session, name="*****@*****.**")
    request_tuple, total = get_requests(session, "pending", 10, 0, owner=user)
    assert len(request_tuple.requests) == 0, "random user shouldn't have a request"

    user = User.get(session, name="*****@*****.**")
    request_tuple, total = get_requests(session, "pending", 10, 0, owner=user)
    assert len(request_tuple.requests) == 1, "user in group with grant should have a request"

    # CANCEL request: have '*****@*****.**' cancel this request
    request_id = request_tuple.requests[0].id
    fe_url = url(base_url, "/permissions/requests/{}".format(request_id))
    resp = yield http_client.fetch(
        fe_url,
        method="POST",
        body=urlencode({"status": "cancelled", "reason": "heck no"}),
        headers={"X-Grouper-User": user.name},
    )
    assert resp.code == 200

    emails = _get_unsent_and_mark_as_sent_emails(session)
    assert_same_recipients(emails, [u"*****@*****.**"])

    perms = _load_permissions_by_group_name(session, "serving-team")
    assert len(perms) == 2
    assert "grantable.two" not in perms, "no new permissions should be granted for this"
def get_user_view_template_vars(session, actor, user, graph):
    # TODO(cbguder): get around circular dependencies
    from grouper.fe.handlers.user_disable import UserDisable
    from grouper.fe.handlers.user_enable import UserEnable

    ret = {}
    if user.is_service_account:
        ret["can_control"] = can_manage_service_account(
            session, user.service_account, actor
        ) or user_is_user_admin(session, actor)
        ret["can_disable"] = ret["can_control"]
        ret["can_enable"] = user_is_user_admin(session, actor)
        ret["can_enable_preserving_membership"] = user_is_user_admin(session, actor)
        ret["account"] = user.service_account
    else:
        ret["can_control"] = user.name == actor.name or user_is_user_admin(session, actor)
        ret["can_disable"] = UserDisable.check_access(session, actor, user)
        ret["can_enable_preserving_membership"] = UserEnable.check_access(session, actor, user)
        ret["can_enable"] = UserEnable.check_access_without_membership(session, actor, user)

    if user.id == actor.id:
        ret["num_pending_group_requests"] = user_requests_aggregate(session, actor).count()
        _, ret["num_pending_perm_requests"] = get_requests(
            session, status="pending", limit=1, offset=0, owner=actor
        )
    else:
        ret["num_pending_group_requests"] = None
        ret["num_pending_perm_requests"] = None

    try:
        user_md = graph.get_user_details(user.name)
    except NoSuchUser:
        # Either user is probably very new, so they have no metadata yet, or
        # they're disabled, so we've excluded them from the in-memory graph.
        user_md = {}

    shell = (
        get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY).data_value
        if get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY)
        else "No shell configured"
    )
    ret["shell"] = shell
    ret["open_audits"] = user_open_audits(session, user)
    group_edge_list = get_groups_by_user(session, user) if user.enabled else []
    ret["groups"] = [
        {"name": g.name, "type": "Group", "role": ge._role} for g, ge in group_edge_list
    ]
    ret["passwords"] = user_passwords(session, user)
    ret["public_keys"] = get_public_keys_of_user(session, user.id)
    for key in ret["public_keys"]:
        key.tags = get_public_key_tags(session, key)
        key.pretty_permissions = [
            "{} ({})".format(perm.name, perm.argument if perm.argument else "unargumented")
            for perm in get_public_key_permissions(session, key)
        ]
    ret["log_entries"] = get_log_entries_by_user(session, user)
    ret["user_tokens"] = user.tokens

    if user.is_service_account:
        service_account = user.service_account
        ret["permissions"] = service_account_permissions(session, service_account)
    else:
        ret["permissions"] = user_md.get("permissions", [])

    return ret