-
Notifications
You must be signed in to change notification settings - Fork 52
Use typed UUIDs for silo user and group #8803
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
This commit changes SiloUser and SiloGroup to use typed UUIDs. The biggest reason that this couldn't happen without a bunch of work was the `lookup_resource` macro: for a resource like SshKey, it looked like ``` lookup_resource! { name = "SshKey", ancestors = [ "Silo", "SiloUser" ], lookup_by_name = true, soft_deletes = true, primary_key_columns = [ { column_name = "id", rust_type = Uuid } ] } ``` One of the methods that the `lookup_resource` macro generates will create a lookup for ancestors of the resource, and this was one using the type returned from the corresponding authz resource's `id` method: ``` quote! { .filter(dsl::#parent_id.eq(#parent_authz_name.id())) }, ``` Changing SiloUser to use a typed UUID in the authz resource: ``` authz_resource! { name = "SiloUser", parent = "Silo", primary_key = { uuid_kind = SiloUserKind }, <-- here roles_allowed = false, polar_snippet = Custom, } ``` and changing the `SiloUser` db model to use `DbTypedUuid` meant that a call to `to_db_typed_uuid` was required. The lookup_resource macro has no type information from the string "SiloUser", so this PR adds a check: if the ancestor string is suffixed with a '*', then the lookup_resource macro should assume that the `parent_id` is a typed UUID, and generate the call to `to_db_typed_uuid`. Most of the work after that was mechanical, changing Uuid to their typed equivalent, changing method argument types, etc etc. Some other related things made it into this PR: - UserBuiltIn now also uses a typed UUID as well, distinguishing them from silo users - Actor no longer has the `actor_id` method, instead requiring call sites to check which variant of Actor is being used - AuthenticatedActor stores the full Actor instead of only the actor id, leading to typed comparisons in its oso::PolarClass impl - User and Group path params are now typed
Can't wait to resolve the conflicts with #7339! I just put that on auto merge, happy to handle rebasing this on top of that when it's in. |
No no, definitely land that one first! :) |
UserBuiltin { user_builtin_id: Uuid }, | ||
SiloUser { silo_user_id: Uuid, silo_id: Uuid }, | ||
UserBuiltin { user_builtin_id: BuiltInUserUuid }, | ||
SiloUser { silo_user_id: SiloUserUuid, silo_id: Uuid }, |
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.
IMO at the view layer these should all be regular UUIDs. I don't think this information is likely to be useful for clients, and might even require shenanigans we'd rather avoid. There are a couple in the OpenAPI schema already but I lean toward considering that a mistake to be fixed.
Lines 25998 to 26013 in 33d4f82
"TypedUuidForAlertKind": { | |
"type": "string", | |
"format": "uuid" | |
}, | |
"TypedUuidForAlertReceiverKind": { | |
"type": "string", | |
"format": "uuid" | |
}, | |
"TypedUuidForInstanceKind": { | |
"type": "string", | |
"format": "uuid" | |
}, | |
"TypedUuidForSupportBundleKind": { | |
"type": "string", | |
"format": "uuid" | |
}, |
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.
PR to get rid of those here #8910
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.
ab1ae6f tries to use the same with
trick
)), | ||
} | ||
} | ||
|
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 match actor
would make it easier to see why this might fail.
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.
agreed, done in f926dad
I noticed in #8803 (comment) that there are some typed UUIDs leaking into the external API. As I said in the comment, I don't think this information is likely to be useful for clients because we don't actually use it in any of the request body types, only the responses. I don't think this change hurts correctness in our code because the conversion to generic UUID usually happens right at the end of request handling, when models are converted to views for response serialization. However, there are a couple of spots where the struct is used a little earlier in the process (see how the affinity datastore functions return view structs) or is shared with internal API code, so there may be a tiny risk to correctness.
nexus/db-macros/src/lookup.rs
Outdated
(name, true) | ||
} else { | ||
(name, false) | ||
}; |
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.
same thing, but look how nice
let (name, primary_key_is_typed_uuid) = match name.strip_suffix('*') {
Some(stripped) => (stripped, true),
None => (name, false),
};
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.
removed this code in 94fe84b
::nexus_db_model::to_db_typed_uuid(#parent_authz_name.id()) | ||
)) } | ||
} else { | ||
quote! { .filter(dsl::#parent_id.eq(#parent_authz_name.id())) } |
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 makes sense to me. gpt-5 had some interesting suggestions for alternative approaches.
https://gist.github.com/david-crespo/cab60b2f494a3728661dfe7d2926eec0
I don't think any are clearly better, but I kind of like the trait one. The below change does compile, but I'm not sure if there's a similarly simple approach for the parent_id
. If there is, it's kinda nice!
diff --git a/nexus/db-macros/src/lookup.rs b/nexus/db-macros/src/lookup.rs
index d2c4868277..13cb71f307 100644
--- a/nexus/db-macros/src/lookup.rs
+++ b/nexus/db-macros/src/lookup.rs
@@ -817,14 +817,8 @@
).await?;
}
},
- // If the parent's id is a typed uuid, then the `to_db_typed_uuid`
- // method is required to convert to the db typed uuid
- if p.primary_key_is_typed_uuid {
- quote! { .filter(dsl::#parent_id.eq(
- ::nexus_db_model::to_db_typed_uuid(#parent_authz_name.id())
- )) }
- } else {
- quote! { .filter(dsl::#parent_id.eq(#parent_authz_name.id())) }
+ quote! {
+ .filter(dsl::#parent_id.eq(::nexus_db_model::ToDbId::to_db_id(#parent_authz_name.id())))
},
quote! { #parent_authz_name },
)
diff --git a/nexus/db-model/src/lib.rs b/nexus/db-model/src/lib.rs
index 92ab5c14e1..6db3ecb060 100644
--- a/nexus/db-model/src/lib.rs
+++ b/nexus/db-model/src/lib.rs
@@ -196,6 +196,8 @@
pub use migration_state::*;
pub use name::*;
pub use network_interface::*;
+use omicron_uuid_kinds::TypedUuid;
+use omicron_uuid_kinds::TypedUuidKind;
pub use oximeter_info::*;
pub use oximeter_read_policy::*;
pub use physical_disk::*;
@@ -519,6 +521,26 @@
}
}
+pub trait ToDbId {
+ type DbId;
+ fn to_db_id(self) -> Self::DbId;
+}
+
+impl ToDbId for uuid::Uuid {
+ type DbId = uuid::Uuid;
+ fn to_db_id(self) -> Self::DbId {
+ self
+ }
+}
+
+impl<K: TypedUuidKind> ToDbId for TypedUuid<K> {
+ type DbId = DbTypedUuid<K>;
+
+ fn to_db_id(self) -> Self::DbId {
+ to_db_typed_uuid(self)
+ }
+}
+
#[cfg(test)]
mod tests {
use crate::RequestAddressError;
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.
hah, I originally both tried the "make it structured" approach and the "use a trait" approach, but settled on the approach in this PR :) The trait didn't make sense because it was such a small impl, and the structure approach ended up being too complicated to implement without changing every lookup_resource!
invocation.
I tried another idea and ended up with a different approach that is simpler than the asterisk suffix: match on the resource name to determine if the lookup should use the to_db_typed_uuid
call. Right now this is only true for SiloUser
, but for something like Project
this avoids having to suffix everything that uses project as a parent.
See commit 94fe84b
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.
Ingenious. The kind of out of the box thinking LLMs will figure out in about a month.
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.
heroic. epic. zero changes to public API
This commit changes SiloUser and SiloGroup to use typed UUIDs.
The biggest reason that this couldn't happen without a bunch of work was the
lookup_resource
macro: for a resource like SshKey, it looked likeOne of the methods that the
lookup_resource
macro generates will create a lookup for ancestors of the resource, and this was one using the type returned from the corresponding authz resource'sid
method:Changing SiloUser to use a typed UUID in the authz resource:
and changing the
SiloUser
db model to useDbTypedUuid
meant that a call toto_db_typed_uuid
was required. The lookup_resource macro has no type information from the string "SiloUser", so this PR adds a check: if the ancestor string is suffixed with a '*', then the lookup_resource macro should assume that theparent_id
is a typed UUID, and generate the call toto_db_typed_uuid
.Most of the work after that was mechanical, changing Uuid to their typed equivalent, changing method argument types, etc etc.
Some other related things made it into this PR:
UserBuiltIn now also uses a typed UUID as well, distinguishing them from silo users
Actor no longer has the
actor_id
method, instead requiring call sites to check which variant of Actor is being usedAuthenticatedActor stores the full Actor instead of only the actor id, leading to typed comparisons in its oso::PolarClass impl
User and Group path params are now typed