add support for assigning roles on Organizations#1002
Conversation
| if !$cond { | ||
| return Err($crate::api::external::Error::internal_error(&format!( | ||
| $($arg)*))) | ||
| Err($crate::api::external::Error::internal_error(&format!( |
There was a problem hiding this comment.
This kind of strange change makes it possible to use bail_unless! in a situation where your function isn't returning Error per se, but something that impls From<Error>.
| pub trait ApiResource: Clone + Send + Sync + 'static { | ||
| /// If roles can be assigned to this resource, return the type and id of the | ||
| /// database record describing this resource | ||
| pub trait ApiResource: |
There was a problem hiding this comment.
I found the combination of ApiResource and ApiResourceError unnecessarily confusing, so I combined them. This basically meant removing the Clone + oso::PolarClass trait bounds here (so that ApiResource can be object-safe) and adding them back (actually oso::ToPolar) in the few places where we actually need them.
|
I think this is close enough to start review. There are two outstanding issues:
The test failure is: I'm not yet sure how to deal with this. |
|
I'm confused about that error because I don't see |
| "role_assignments" | ||
| ] | ||
| }, | ||
| "OrganizationRolesRoleAssignment": { |
There was a problem hiding this comment.
I don't like the thought that every resource will have its own copy of these three types, though if the roles can be different I guess we have no choice?
There was a problem hiding this comment.
Yeah, I think this is a tradeoff for having the allowed roles be expressed in the API. If there's another way to do it without that tradeoff that'd be great!
There was a problem hiding this comment.
Will all such types be the same except for their names?
There was a problem hiding this comment.
If they will, then one approach would be to impl Into<RolesPolicy> for OrganizationRolesPolicy on all the individual types. That would give them all the same type in the OpenAPI spec.
There was a problem hiding this comment.
But I see what we're buying with the separate types: a generated client will already know at the type level what roles are valid for a given resource, which is pretty nifty.
type OrganizationRoles = 'Admin' | 'Collaborator'
type OrganizationRolesPolicy = {
roleAssignments: OrganizationRolesRoleAssignment[]
}
type OrganizationRolesRoleAssignment = {
identityId: string
identityType: IdentityType
roleName: OrganizationRoles
}I guess I'm basically lamenting that OpenAPI doesn't let us express this relationship (as far as I know):
type RolesPolicy<Role extends string> = {
roleAssignments: RoleAssignment<Role>[]
}
type RoleAssignment<Role extends string> = {
identityId: string
identityType: IdentityType
roleName: Role
}
type OrganizationRole = 'Admin' | 'Collaborator'
// use RoleAssignment<OrganizationRole> wherever
type ProjectRole = 'Admin' | 'Collaborator' | 'SomethingElse'
// use RoleAssignment<ProjectRole> whereverThere was a problem hiding this comment.
Yeah, it would be neat if OpenAPI let you do that.
To summarize: my primary intent here was to have the Nexus type reflect the set of allowed role names. That way, this validation is handled like all other validation, while parsing the input in the first place, and the other code paths can know from the type that the role names have been validated.
I went the logical next step of using this type directly in the HTTP handler functions (both input and output) so that the OpenAPI spec reflects the allowed values, especially since we do validate that so we may as well document what the accepted values are in the spec. As you noted, this means the generated clients can know exactly which values are allowed, which is cool. We don't have to do this part though. We could still accept and return strings instead of enums. And it's possible that in the future we'll want to do that because if we support custom roles, then there won't be an enum of allowed values. But I'd rather not spend much time now worrying about that for the usual reasons: it's hard to know we're actually solving the future problem no matter what we do now, and we can always evolve this API compatibly anyway.
There was a problem hiding this comment.
@david-crespo is your lament, in essence, that OpenAPI / JSON Schema doesn't permit the expression of generics types / types with parameters?
There was a problem hiding this comment.
Yes — having identical *RolesPolicy and *RoleAssignment types for every resource feels silly. But it seems there's no way around it if we want to keep the per-resource role enums as opposed to making them all string (or make them all share the same enum).
plotnick
left a comment
There was a problem hiding this comment.
This looks great; thanks for tackling this very thorny issue! One abstract thought that I had reading it was that a Policy can be more general than just a list of role assignments, but I don't think there's anything in here that limits us to just that if we need to expand the notion later. I also worry slightly about keeping the several enums for identity/actor type in sync, but don't have a better alternative at the moment.
The reason for this is that the test that updates the OpenAPI spec checks lint first. So the copy of the spec in the repo for that commit isn't what was being linted. To show the spec that was generated without the serde rename, I generated this commit: In that one, the test checks lint after updating the local copy, so that spec is what's being linted. |
That's right. For the MVP it's just that, but the intent is to be able to expand it later if we want.
I share that worry, but it seems to be the pattern we're using for better or worse. I copied the pattern from nexus/src/db/model.rs, where we do something similar for DatasetKind and InstanceState. At least Rust will tell us if either grows a variant that isn't handled in the other. |
ahl
left a comment
There was a problem hiding this comment.
for posterity now since I realize this is already merged...
| SerializeDisplay, | ||
| JsonSchema, | ||
| )] | ||
| #[display(style = "kebab-case")] |
There was a problem hiding this comment.
I see that there is dissensus about how we name enum variants. I'm not sure how we started doing kebab case, but I think that other things are doing snake_case.
- it obviously doesn't matter with these variants
- it's internal so who cares (but we do have both in the external API)
- I think we should probably add this to the linter (and be "we" I mean "I" and by "should" I mean "what do you think?")
There was a problem hiding this comment.
Agreed. I don't think I care between snake_case and kebab-case. I'm sure I copied this from somewhere else thinking it was our standard. Yes, it'd be great to lint it!
| // TODO-correctness As with the rest of the API, we're lacking an ability | ||
| // for an ETag precondition check here. |
There was a problem hiding this comment.
I think that we can now do express this with Dropshot if that's what you mean... which is not to say this should be done now, but are there pieces missing to make this tenable?
There was a problem hiding this comment.
I suspect there's a lot of work here but I don't know. I feel like several times over the last year I've thought "ugh, <thing I'm working on> is gonna get complicated when we implement etags". But thinking out loud:
- We need to decide how to generate the etag for every kind of resource. Is it random (and updated with every modification) or content-based? If content-based, is it stored in the database or calculated when needed? If calculated when needed, it'd be nice if we could do it in SQL so that you can calculate it and check it within a SQL query (see below). What fields go into it? How do we specify that on a per-resource basis? How do we keep it up to date if you're doing something like
update ... set ... where <condition that matches 10 rows>? - We need to plumb it through on "list" and "get" for everything. Gotta figure out where to store it...if it's stored in
the database, great. If not, how do we pass it around? Maybe it goes into theauthzobject? That might sound weird but theauthzobject basically represents a handle to a specific object, regardless of how it was looked up or what it's called now. It makes sense that would also be locked to a specific revision of that object. - We need to modify all the "update" datastore functions so that they accept an optional etag and do the conditional update. In an ideal world, we'd issue a SQL "update" with the etag condition in it (rather than read-check-write).
- We need to plumb that optional etag argument out to all the PUT entrypoints.
That doesn't sound so bad. We might need better transaction support (see #973) -- I'm not sure.
| // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| // file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
|
||
| //! Types that are used as both views and params |
There was a problem hiding this comment.
At the time we split out so-called views and params, I think we anticipated the need for types that might exist in both. Is there a reason to maintain that split? The current files enforce an organization that doesn't particularly make sense to me. We could, for example, have groups of endpoints and types live together. ... Obviously not a blocker for this review...
There was a problem hiding this comment.
I agree that the current split is confusing. I understand the current guidance to be:
- if it's an input param, it goes in "params"
- if it's an output, it goes in "views"
- if it's both, it goes in "shared"
- there's also a bunch of stuff in "common" that hasn't been put into one of those three places because it's hard
This last point makes it kind of a mess, and it's also possible that this last part is hard because there are other constraints involved that this guidance doesn't accommodate. Progenitor changes a bunch of this too -- I think part of the original reason for "common" was for stuff that was shared between clients and servers of a given service.
On your specific example:
We could, for example, have groups of endpoints and types live together.
We could. Any way I can think to group them sucks a little bit -- including the current approach of putting them in one big file. (If we group them by resource, one might be sad that the various "create" endpoints (which are very parallel to each other and use some of the same types, like IdentityMetadataCreate) don't live near each other. Also, it's kind of nice for some small groups of resources to live together (e.g., built-in users and roles, which might go under an "IAM" bucket). So maybe this is by tag after all?)
Basically, I don't have a strong idea of how these should be better organized.
| /// Client view of a [`Policy`], which describes how this resource may be | ||
| /// accessed |
There was a problem hiding this comment.
Forgive me as this is neither here nor there, but the docs here become the external docs. I think we'll eventually want to change this to be something like:
| /// Client view of a [`Policy`], which describes how this resource may be | |
| /// accessed | |
| /// Policy that describes how this resource may be accessed |
There was a problem hiding this comment.
💯 agree -- and I think that'll be true for all the stuff in views. (I copied the form there for these types.) I think we'll want to make an edit pass that documents all these things properly.
| ] | ||
| }, | ||
| "IdentityType": { | ||
| "description": "Describes what kind of identity is described by an id", |
There was a problem hiding this comment.
I wasn't sure where this came from, but this seems a bit circular in terms of customer-facing docs
| "role_assignments" | ||
| ] | ||
| }, | ||
| "OrganizationRolesRoleAssignment": { |
There was a problem hiding this comment.
@david-crespo is your lament, in essence, that OpenAPI / JSON Schema doesn't permit the expression of generics types / types with parameters?
This PR adds initial support for assigning roles on Organizations using the interface described in #890 ("option 2"). There's a big block comment I need to update and some missing validation but I'm hopeful that this is close to ready for review.
I'm hopeful that it will be pretty easy to add support for assigning roles on the Fleet, Silos, and Projects because all the underlying pieces here are generic.