Skip to content

Conversation

@zephraph
Copy link
Contributor

@zephraph zephraph commented Feb 27, 2023

Part of #2028

Implements the APIs for

  • Silos
  • Silo users
  • Identity providers

I've noted a big departure from the original API around silo users that I've taken here that I'd specifically like feedback on.

Comment on lines +1030 to +1034
#[endpoint {
method = GET,
path = "/v1/system/users",
tags = ["system"],
}]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @davepacheco, @ahl, @jmpesp

This diverges from the initial implementation as discussed here: #1784 (comment).

As per the original implementation this would've been /v1/system/users/all. The problem (as I see it) with that approach is that it's divergent from our other endpoint definitions. The stated challenge we were trying to work around is that there are potentially other ways that we may want to look up a user other than ID. While I appreciate that challenge, I think there are other ways we can go about solving it.

My current suggestion (for looking up a user by external_id) is simply to add a filter to the list endpoint.

GET /v1/system/users?external_id={id}`

This would yield an array with 0 or 1 results. While not strictly as clean from a result perspective, this approach could be added to other endpoints which already implement ID or name lookups as a subroute of the list endpoint without requiring breaking path changes.

Bulk lookups were also mentioned. I feel like this could also be handled via filtering the list endpoint. One could envision an array query parameter like /v1/system/users?ids[]=xxx&ids[]=yyy.

I acknowledge that there may well be a strong counter argument here that having multiple operating modes in a single endpoint makes that endpoints implementation more complex. While acknowledging that, I'd also argue that we're simply talking about different types of filters here. Generally I think we'll want to give more robust filtering capabilities to users and this'll be something we should work towards adding anyway.

All that said, feed back is definitely welcome.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have the same problem in #2423 with looking up groups by external ID. I agree with Justin that while it's a bit weird, treating it as a list filter is the best way.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I initially went with separate endpoints out of my general fear of scalably implementing arbitrary filtering. More specifically, if we choose not to implement arbitrary filtering, and instead implement only a handful of filters that are important (e.g., users in one group, user for an external id, all users, users for a set of ids), will the resulting endpoint restrict users in ways that they'll find surprising and annoying (like: "I can take a set of user ids or a group id, but not both")? I'm still worried about arbitrary filtering but I don't have a strong feeling either way on which makes a cleaner API.

Tangentially: I think we have generally avoided structured query parameters (i.e., where the backend synthesizes more complicated structures from the key-value pairs), but maybe that's just because we haven't had enough need for them. The main downside (to me) is that I don't think there's really a standard for it so it winds up being API-specific instead of predictable or conventional. But we're probably going to need them at some point so we may as well come up with a standard. In this case that could be ids[]=a&ids[]=b (as you suggested) or id=a&id=b or id='%5Ba%2C%20b%5D (filter field is url-encoded JSON) or even filter='%22%7B%5C%22id%5C%22%3A%5B%5C%22a%5C%22%2C%5C%22b%5C%22%5D%7D%22 (filter is url-encoded JSON).

I think I wouldn't worry too much about either of these here.

@zephraph zephraph changed the title WIP: V1 APIs for Silos, Identity Providers, and Users V1 APIs for Silos, Identity Providers, and Users Mar 1, 2023
@zephraph zephraph requested review from ahl, davepacheco and jmpesp March 1, 2023 19:38
@zephraph zephraph marked this pull request as ready for review March 1, 2023 19:39
Comment on lines +34 to +51
pub fn silo_lookup<'a>(
&'a self,
opctx: &'a OpContext,
silo: &'a NameOrId,
) -> LookupResult<lookup::Silo<'a>> {
match silo {
NameOrId::Id(id) => {
let silo =
LookupPath::new(opctx, &self.db_datastore).silo_id(*id);
Ok(silo)
}
NameOrId::Name(name) => {
let silo = LookupPath::new(opctx, &self.db_datastore)
.silo_name(Name::ref_cast(name));
Ok(silo)
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lookup mechanism differs from the normal lookups in that it doesn't take a selector. Once we've moved over to the V1 API I fully expect we'll simplify the selector implementations anyway so I think it's fine that this one case is slightly different.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@david-crespo david-crespo mentioned this pull request Mar 2, 2023
david-crespo added a commit that referenced this pull request Mar 2, 2023
We need this endpoint to power a group detail page in the console where
we fetch the group by ID and display its name and the list of users in
it.

As mentioned in a comment, groups have an `external_id` instead of a
`name` and therefore don't implement `ObjectIdentity`, which makes doing
a lookup by external ID a little less cute than lookup by name.
`external_id` is unique per silo, so looking up by it with the silo
implicit is perfectly possible in theory. _However_, unlike with `Name`,
we do not (and probably cannot) enforce that the `external_id` of a
group is _not_ a UUID, which means our usual technique of falling back
to name only once the thing fails to parse as a UUID does not work. If
an external ID was a UUID, there would be no way to fetch a group by it
because we would always assume it was one of _our_ DB UUIDs.

One option would be to add a query param that allows the caller to
specify that the given identifier is meant to be treated as the group
external ID and not our DB ID. Another option, which we can do in a
followup, is what @zephraph suggested in
#2442 (comment):
implement the `external_id` filter on the list endpoint instead. Because
`external_id` is unique per silo, we know this will return either 1 or 0
results. It's a bit odd but it is conceptually consistent and requires
no shenanigans.
@david-crespo
Copy link
Contributor

david-crespo commented Mar 2, 2023

Some endpoints are missing deprecated = true:

  • silo_list
  • silo_create
  • silo_policy_update
  • saml_identity_provider_create
  • local_idp_user_create
  • silo_user_view
  • system_policy_update

This list is comprehensive, I went through them all (last one is not even from this PR).

saml_identity_provider: NameOrId::Id(_),
silo_selector: Some(_),
} => Err(Error::invalid_request("when providing provider as an ID, silo should not be specified")),
_ => Err(Error::invalid_request("provider should either be a UUID or silo should be specified"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nittiest of nits

Suggested change
_ => Err(Error::invalid_request("provider should either be a UUID or silo should be specified"))
_ => Err(Error::invalid_request("either provider should be a UUID or silo should be specified"))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm 👍 to this, but the message is consistent between all the other lookup functions. Maybe we should make this change across all of them at the same time?

Also makes me wonder if the should just be a lookup error that wraps around these cases 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, probably do a followup to change them all.

.fetch()
.await?;
// TODO-security: This should likely be fetch_for CreateChild on the silo
let (authz_silo, db_silo) = silo_lookup.fetch().await?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't that auth check get done when you actually do the creating?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit of an imperfect process across the API. We do a lot of the authz calls twice. Once at the entrance of the nexus layer and again at the datastore layer. Sometimes the nexus layer has more authz checks because there are logical permissions that must be validated beyond just what the datastore layer would check. @davepacheco has mentioned potentially caching duplicatively authz queries.

The pattern here is fairly typical though. Do an authz check at the entrance of the nexus function.

.silo_name(silo_name)
.lookup_for(authz::Action::Read)
.await?;
let (authz_silo,) = silo_lookup.lookup_for(authz::Action::Read).await?;
Copy link
Contributor

@david-crespo david-crespo Mar 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in chat with @zephraph, we considered the possibility that this would succeed at looking up a user by ID as long as the specified silo exists, even if the silo doesn't match the user's ID (because the silo is technically not needed for the lookup if we have a user ID to look up directly). But there is an explicit check that the retrieved user's silo ID matches the one passed in:

if db_silo_user.silo_id != authz_silo.id() {
return Err(authz_silo_user.not_found());
}

Copy link
Contributor

@david-crespo david-crespo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think we got em

@zephraph zephraph merged commit 457eb58 into main Mar 6, 2023
@zephraph zephraph deleted the rfd-322-silos branch March 6, 2023 18:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants