-
Notifications
You must be signed in to change notification settings - Fork 59
Add list of group IDs to /session/me
response
#1830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
display_name: user.external_id, | ||
silo_id: user.silo_id, | ||
group_ids: groups.iter().map(|g| g.silo_group_id).collect(), | ||
})) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think views::SessionMe::new(user, groups)
would make sense here, but I struggled to decide where to define that. The obviously place would be right next to the definition of SessionMe
in views.rs
, but that would be the first time anything in that file refers to DB models. Could be fine, or maybe it offends our sense of propriety. Generally we seem to prefer doing our impl From
for these views next to the DB model, which to be honest is not what I prefer. I like to think of the view as "knowing about" the model but the model not knowing about the view.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact, there does not seem to be a way to import db
into nexus/types/src/external_api/views.rs
because it's a different crate and that would introduce a circular dependency. So it basically has to be defined here in the app, but I can't impl
a constructor for it because it comes from another crate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems to point to a fn Nexus::session_me(..) -> views::SessionMe
, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered that too. The problem is the app layer methods all return DB models, not views, and it's up to the From
impls to turn those into views in the entrypoints. This is a pretty rare case where we're making a view out of two models. I don't think we do that very often. So if it can't go in nexus/src/app
, it would basically be a helper that sits right next to the route handler right here, at which point I figure we might as well inline it (leave as is) since it's not going to be used anywhere else.
pub silo_id: Uuid, | ||
} | ||
|
||
/// Client view of a [`User`] with some extra stuff that's useful to the console |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
love a comment that makes you go "are we sure we should be doing this?"
nexus/src/app/iam.rs
Outdated
Ok(db_silo_user) | ||
} | ||
|
||
pub async fn silo_user_fetch_groups( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not also put _for_self
somewhere in this function? unless the idea is that it's implicit because of the function signature?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, I must have mixed up some different names and thought I was matching the other one, but I see now it has self
in the name. Fixing.
} | ||
); | ||
|
||
// TODO: fails bc user can't pull their own group memberships? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this comment still true?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No
display_name: user.external_id, | ||
silo_id: user.silo_id, | ||
group_ids: groups.iter().map(|g| g.silo_group_id).collect(), | ||
})) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems to point to a fn Nexus::session_me(..) -> views::SessionMe
, no?
I'm leaning towards 🚀 with the idea that we'll attempt putting together Oso policy to describe this at some point, even if it's a tradeoff we don't want to make now. |
id: USER_TEST_UNPRIVILEGED.id(), | ||
display_name: USER_TEST_UNPRIVILEGED.external_id.clone(), | ||
silo_id: DEFAULT_SILO.id(), | ||
group_ids: vec![], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be cool to be able to add the user to a group, then refetch and see the group here. But I see that there is no endpoint for adding a user to a group and modification of group memberships only happens during JIT user create. A next-best thing would be if the built-in privileged and unprivileged users were in groups, so at least this vec would come back non-empty. @jmpesp any thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In nexus/tests/integration_tests/saml.rs
, test_correct_saml_response_with_group_attributes
tests that groups are JITed correctly, and test_post_saml_response
calls /session/me
to validate a successful SAML login. I'm not sure about the built-in users being part of some fake groups when a test(s) could be done there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. In 0e61d09 I modified test_post_saml_response
to include groups and assert that the groups are there on /session/me
. One downside is that test didn't used to be about groups and now it is. Not sure if that is really a problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's not a problem, though if the order of groups returned changes then that assert_eq
will fire.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a cute helper for that in 52b9f46
"type": "string" | ||
}, | ||
"group_ids": { | ||
"type": "array", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this bounded in some way? in general I think we try to avoid responses that are effectively unbounded in terms of their size.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, and bad timing for the auto merge. I might revert it, or I might just make the necessary changes on top. I don't think there is a bound on the size of this list. The problem from the console point of view is that we really do want all the groups for the user.
Our standard approach is to make a separate paginated endpoint. I was trying to avoid that, but it's natural enough: the console can GET /session/me/groups
or whatever with a large page size (default is 1000) to try to get all the groups. This would likely cover us for a long time, especially since we can increase the page size up to (I think) 10000 if someone wants to get weird and assign a ton of groups, and it's not too surprising if the console experience degrades somewhat when a user is in 10,000 groups. In this case, the degradation would be that when we determine client-side whether a user has a given permission (by comparing the user's ID and group IDs to a policy response), we will determine wrong if we are missing groups for the user.
On the other hand, this whole rigmarole is a point in favor of moving that logic server-side, letting the console ask for the current user's effective role on any resource.
Another option is to actually limit the number of group memberships and not paginate this.
This is meant to be a starting point for figuring out how this should work. It's not particularly good.
Problem
In the console we would like to hide or disable certain controls when the user does not have permission to use them. For example, a user with only the
viewer
role on an organization does not have the ability to create projects in that organization, and we prefer disabling the button (likely with a tooltip on hover) over letting them click the button and find out they can't create when the API comes back with a 403. So I need to know the user's role on the organization when I render the UI.I plan to use data for the current user plus the policy for a given resource to determine the user's effective role on the resource. "Effective" is key because they could have roles from multiple sources — e.g., their user ID directly, group A they belong to, group B they belong to, or any of those from the policy on a parent resource 😓 — and the effective role from the API's point of view is the strongest role out of all of those.
Everything required to do that currently exists except there is no way of getting the current user's group memberships. This could be its own endpoint or it could be added to
/session/me
. I did the latter here because I don't really see why anyone would ever want to call one without the other, but I don't feel to strongly about it.Questions/issues
Policy is implicit in app code
The existing endpoint
silo_group_membership_for_user
takes anauthz::Silo
and a user ID, and the auth check is forListChildren
on the silo. Assuming we want any user to be able to list their own groups, we cannot stick to this pattern for the "list by groups" endpoint, because the unprivileged user does not necessarily have any permissions on the silo. What I've done here instead is not have any authz check, pulling the user ID off theopctx
. The implicit policy therefore is that any user can fetch their own groups. However, I noticed that this is only the second time we callopctx.authn.actor_required()
in anydb/datastore
file. The first is session delete, which has a helpful comment by @davepacheco:I think this logic also applies to what I've written, which means we should have a similar comment and we're ok.
New
SessionMe
response typeWe use the existing
User
in a number of places, not just/session/me
, and I don't think we want to clutter all those up with lists of groups (or maybe we do?) so we need a new response type for/session/me
. This isn't that weird and I'm pretty sure we've talked about doing it before, though I can't find that discussion.Should we list group IDs or
{ id, name }
objects?For my purposes in the console (matching against a policy) I only need IDs, but I can certainly see us needing names sometime soon. For example, it might make sense to display a list of my groups on
/settings/profile
, which currently only shows the user's UUID.