diff --git a/libs/@local/graph/authorization/src/api.rs b/libs/@local/graph/authorization/src/api.rs index 3425a6ddced..2d5bb4d3d53 100644 --- a/libs/@local/graph/authorization/src/api.rs +++ b/libs/@local/graph/authorization/src/api.rs @@ -14,8 +14,8 @@ use crate::{ CheckError, CheckResponse, ModifyRelationError, ModifyRelationshipOperation, ReadError, }, schema::{ - AccountGroupPermission, AccountGroupRelationAndSubject, DataTypePermission, - DataTypeRelationAndSubject, EntityPermission, EntityRelationAndSubject, + AccountGroupPermission, AccountGroupRelationAndSubject, AccountIdOrPublic, + DataTypePermission, DataTypeRelationAndSubject, EntityPermission, EntityRelationAndSubject, EntityTypePermission, EntityTypeRelationAndSubject, PropertyTypePermission, PropertyTypeRelationAndSubject, WebPermission, WebRelationAndSubject, }, @@ -116,6 +116,20 @@ pub trait AuthorizationApi: Send + Sync { consistency: Consistency<'_>, ) -> impl Future>> + Send; + fn get_entities( + &self, + actor: AccountId, + permission: EntityPermission, + consistency: Consistency<'_>, + ) -> impl Future, Report>> + Send; + + fn get_entity_accounts( + &self, + entity: EntityUuid, + permission: EntityPermission, + consistency: Consistency<'_>, + ) -> impl Future, Report>> + Send; + fn modify_entity_relations( &mut self, relationships: impl IntoIterator< @@ -516,6 +530,26 @@ impl AuthorizationApi for &mut A { .get_data_type_relations(data_type, consistency) .await } + + async fn get_entities( + &self, + actor: AccountId, + permission: EntityPermission, + consistency: Consistency<'_>, + ) -> Result, Report> { + (**self).get_entities(actor, permission, consistency).await + } + + async fn get_entity_accounts( + &self, + entity: EntityUuid, + permission: EntityPermission, + consistency: Consistency<'_>, + ) -> Result, Report> { + (**self) + .get_entity_accounts(entity, permission, consistency) + .await + } } /// Managed pool to keep track about [`AuthorizationApi`]s. diff --git a/libs/@local/graph/authorization/src/backend/mod.rs b/libs/@local/graph/authorization/src/backend/mod.rs index 33b65d8fe90..a87a154ec73 100644 --- a/libs/@local/graph/authorization/src/backend/mod.rs +++ b/libs/@local/graph/authorization/src/backend/mod.rs @@ -238,6 +238,33 @@ pub trait ZanzibarBackend { impl Serialize + Send + Sync, >, ) -> impl Future>> + Send; + + fn lookup_resources( + &self, + subject: &( + impl Subject, Relation: Serialize> + Sync + ), + permission: &(impl Serialize + Permission + Sync), + resource_kind: &O::Kind, + consistency: Consistency<'_>, + ) -> impl Future, Report>> + Send + where + for<'de> O: Resource + Send> + Send; + + fn lookup_subjects( + &self, + subject_type: &::Kind, + subject_relation: Option<&S::Relation>, + permission: &(impl Serialize + Permission + Sync), + resource: &O, + consistency: Consistency<'_>, + ) -> impl Future::Id>, Report>> + Send + where + for<'de> S: Subject< + Resource: Resource + Send>, + Relation: Serialize + Sync, + > + Send, + O: Resource + Sync; } impl ZanzibarBackend for &mut Z { @@ -344,6 +371,48 @@ impl ZanzibarBackend for &mut Z { ) -> Result> { ZanzibarBackend::delete_relations(&mut **self, filter).await } + + async fn lookup_resources( + &self, + subject: &( + impl Subject, Relation: Serialize> + Sync + ), + permission: &(impl Serialize + Permission + Sync), + resource_kind: &O::Kind, + consistency: Consistency<'_>, + ) -> Result, Report> + where + for<'de> O: Resource + Send> + Send, + { + ZanzibarBackend::lookup_resources(&**self, subject, permission, resource_kind, consistency) + .await + } + + async fn lookup_subjects( + &self, + subject_type: &::Kind, + subject_relation: Option<&S::Relation>, + permission: &(impl Serialize + Permission + Sync), + resource: &O, + consistency: Consistency<'_>, + ) -> Result::Id>, Report> + where + for<'de> S: Subject< + Resource: Resource + Send>, + Relation: Serialize + Sync, + > + Send, + O: Resource + Sync, + { + ZanzibarBackend::lookup_subjects::( + &**self, + subject_type, + subject_relation, + permission, + resource, + consistency, + ) + .await + } } impl ZanzibarBackend for NoAuthorization { @@ -439,6 +508,39 @@ impl ZanzibarBackend for NoAuthorization { deleted_at: Zookie::empty(), }) } + + async fn lookup_resources( + &self, + _: &( + impl Subject, Relation: Serialize> + Sync + ), + _: &(impl Serialize + Permission + Sync), + _: &O::Kind, + _: Consistency<'_>, + ) -> Result, Report> + where + for<'de> O: Resource + Send> + Send, + { + Ok(Vec::new()) + } + + async fn lookup_subjects( + &self, + _: &::Kind, + _: Option<&S::Relation>, + _: &(impl Serialize + Permission + Sync), + _: &O, + _: Consistency<'_>, + ) -> Result::Id>, Report> + where + for<'de> S: Subject< + Resource: Resource + Send>, + Relation: Serialize + Sync, + > + Send, + O: Resource + Sync, + { + Ok(Vec::new()) + } } /// Return value for [`ZanzibarBackend::import_schema`]. diff --git a/libs/@local/graph/authorization/src/backend/spicedb/api.rs b/libs/@local/graph/authorization/src/backend/spicedb/api.rs index b29ac4ca86e..07c88a3f02c 100644 --- a/libs/@local/graph/authorization/src/backend/spicedb/api.rs +++ b/libs/@local/graph/authorization/src/backend/spicedb/api.rs @@ -15,7 +15,7 @@ use crate::{ DeleteRelationshipResponse, ExportSchemaError, ExportSchemaResponse, ImportSchemaError, ImportSchemaResponse, ModifyRelationshipError, ModifyRelationshipOperation, ModifyRelationshipResponse, ReadError, SpiceDbOpenApi, ZanzibarBackend, - spicedb::model::{self, Permissionship, RpcError}, + spicedb::model::{self, LookupPermissionship, Permissionship, RpcError}, }, zanzibar::{ Consistency, Permission, @@ -600,4 +600,123 @@ impl ZanzibarBackend for SpiceDbOpenApi { }) .change_context(DeleteRelationshipError) } + + async fn lookup_resources( + &self, + subject: &( + impl Subject, Relation: Serialize> + Sync + ), + permission: &(impl Serialize + Permission + Sync), + resource_kind: &O::Kind, + consistency: Consistency<'_>, + ) -> Result, Report> + where + for<'de> O: Resource + Send> + Send, + { + #[derive(Serialize)] + #[serde( + rename_all = "camelCase", + bound = " + O: Serialize, + R: Serialize, + S: Subject, Relation: Serialize>" + )] + struct LookupResourcesRequest<'a, O, R, S> { + consistency: model::Consistency<'a>, + resource_object_type: &'a O, + permission: &'a R, + #[serde(with = "super::serde::subject_ref")] + subject: &'a S, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct LookupResourcesResponse { + resource_object_id: O, + #[serde(rename = "permissionship")] + _permissionship: LookupPermissionship, + } + + self.stream::, _>( + "/v1/permissions/resources", + &LookupResourcesRequest { + consistency: model::Consistency::from(consistency), + resource_object_type: resource_kind, + permission, + subject, + }, + ) + .await + .change_context(ReadError)? + .map_ok(|response| response.resource_object_id) + .map_err(|error| error.change_context(ReadError)) + .try_collect() + .await + } + + async fn lookup_subjects( + &self, + subject_type: &::Kind, + subject_relation: Option<&S::Relation>, + permission: &(impl Serialize + Permission + Sync), + resource: &O, + consistency: Consistency<'_>, + ) -> Result::Id>, Report> + where + for<'de> S: Subject< + Resource: Resource + Send>, + Relation: Serialize + Sync, + > + Send, + O: Resource + Sync, + { + #[derive(Serialize)] + #[serde( + rename_all = "camelCase", + bound = " + O: Resource, + R: Serialize, + S: Serialize, + SR: Serialize" + )] + struct LookupSubjectsRequest<'a, O, R, S, SR> { + consistency: model::Consistency<'a>, + #[serde(with = "super::serde::resource_ref")] + resource: &'a O, + permission: &'a R, + subject_object_type: &'a S, + #[serde(skip_serializing_if = "Option::is_none")] + optional_subject_relation: Option<&'a SR>, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct ResolvedObject { + subject_object_id: O, + #[serde(rename = "permissionship")] + _permissionship: LookupPermissionship, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct LookupSubjectsResponse { + subject: ResolvedObject, + } + + self.stream::::Id>, _>( + "/v1/permissions/subjects", + &LookupSubjectsRequest { + consistency: model::Consistency::from(consistency), + resource, + permission, + subject_object_type: subject_type, + optional_subject_relation: subject_relation, + }, + ) + .await + .change_context(ReadError)? + .map_ok(|response| response.subject.subject_object_id) + .map_err(|error| error.change_context(ReadError)) + .try_collect() + .await + } } diff --git a/libs/@local/graph/authorization/src/backend/spicedb/mod.rs b/libs/@local/graph/authorization/src/backend/spicedb/mod.rs index be31664165d..d6f3feecc78 100644 --- a/libs/@local/graph/authorization/src/backend/spicedb/mod.rs +++ b/libs/@local/graph/authorization/src/backend/spicedb/mod.rs @@ -27,11 +27,11 @@ impl SpiceDbOpenApi { /// /// # Panics /// - /// - Panics if `key` is not a valid value for the token + /// - if `key` is not a valid value for the token /// /// # Errors /// - /// - Errors if the client could not be built + /// - if the client could not be built pub fn new( base_path: impl Into, key: Option<&str>, diff --git a/libs/@local/graph/authorization/src/backend/spicedb/model.rs b/libs/@local/graph/authorization/src/backend/spicedb/model.rs index ea3c00e86a5..c566d3e5af7 100644 --- a/libs/@local/graph/authorization/src/backend/spicedb/model.rs +++ b/libs/@local/graph/authorization/src/backend/spicedb/model.rs @@ -105,6 +105,12 @@ pub(crate) enum Permissionship { Conditional, } +#[derive(Debug, Copy, Clone, Deserialize)] +pub(crate) enum LookupPermissionship { + #[serde(rename = "LOOKUP_PERMISSIONSHIP_HAS_PERMISSION")] + HasPermission, +} + impl From for bool { fn from(permissionship: Permissionship) -> Self { match permissionship { diff --git a/libs/@local/graph/authorization/src/lib.rs b/libs/@local/graph/authorization/src/lib.rs index 4fa62092188..3ba383c6a65 100644 --- a/libs/@local/graph/authorization/src/lib.rs +++ b/libs/@local/graph/authorization/src/lib.rs @@ -11,9 +11,10 @@ use std::collections::HashMap; pub use self::api::{AuthorizationApi, AuthorizationApiPool}; use crate::schema::{ - AccountGroupRelationAndSubject, DataTypePermission, DataTypeRelationAndSubject, - EntityRelationAndSubject, EntityTypePermission, EntityTypeRelationAndSubject, - PropertyTypePermission, PropertyTypeRelationAndSubject, WebRelationAndSubject, + AccountGroupRelationAndSubject, AccountIdOrPublic, DataTypePermission, + DataTypeRelationAndSubject, EntityRelationAndSubject, EntityTypePermission, + EntityTypeRelationAndSubject, PropertyTypePermission, PropertyTypeRelationAndSubject, + WebRelationAndSubject, }; mod api; @@ -307,6 +308,24 @@ impl AuthorizationApi for NoAuthorization { ) -> Result, Report> { Ok(Vec::new()) } + + async fn get_entities( + &self, + _: AccountId, + _: EntityPermission, + _: Consistency<'_>, + ) -> Result, Report> { + Ok(Vec::new()) + } + + async fn get_entity_accounts( + &self, + _: EntityUuid, + _: EntityPermission, + _: Consistency<'_>, + ) -> Result, Report> { + Ok(Vec::new()) + } } impl AuthorizationApiPool for A diff --git a/libs/@local/graph/authorization/src/schema/account.rs b/libs/@local/graph/authorization/src/schema/account.rs index 0cfc0169112..3dc6370ac30 100644 --- a/libs/@local/graph/authorization/src/schema/account.rs +++ b/libs/@local/graph/authorization/src/schema/account.rs @@ -90,3 +90,48 @@ impl Subject for PublicAccess { Subject::into_parts(*self) } } + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AccountIdOrPublic { + AccountId(AccountId), + PublicAccess(PublicAccess), +} + +impl Resource for AccountIdOrPublic { + type Id = Self; + type Kind = AccountNamespace; + + #[expect(refining_impl_trait)] + fn from_parts(kind: Self::Kind, id: Self::Id) -> Result { + match kind { + AccountNamespace::Account => Ok(id), + } + } + + fn into_parts(self) -> (Self::Kind, Self::Id) { + (AccountNamespace::Account, self) + } + + fn to_parts(&self) -> (Self::Kind, Self::Id) { + Resource::into_parts(*self) + } +} + +impl Subject for AccountIdOrPublic { + type Relation = !; + type Resource = Self; + + #[expect(refining_impl_trait)] + fn from_parts(resource: Self::Resource, _relation: Option) -> Result { + Ok(resource) + } + + fn into_parts(self) -> (Self::Resource, Option) { + (self, None) + } + + fn to_parts(&self) -> (Self::Resource, Option) { + Subject::into_parts(*self) + } +} diff --git a/libs/@local/graph/authorization/src/schema/mod.rs b/libs/@local/graph/authorization/src/schema/mod.rs index c2501c96a74..3a49c1d01a6 100644 --- a/libs/@local/graph/authorization/src/schema/mod.rs +++ b/libs/@local/graph/authorization/src/schema/mod.rs @@ -9,7 +9,7 @@ mod settings; mod web; pub use self::{ - account::{AccountNamespace, PublicAccess}, + account::{AccountIdOrPublic, AccountNamespace, PublicAccess}, account_group::{ AccountGroupAdministratorSubject, AccountGroupMemberSubject, AccountGroupNamespace, AccountGroupPermission, AccountGroupRelationAndSubject, AccountGroupSubject, diff --git a/libs/@local/graph/authorization/src/zanzibar/api.rs b/libs/@local/graph/authorization/src/zanzibar/api.rs index 5fa13ccb097..94ba41ead58 100644 --- a/libs/@local/graph/authorization/src/zanzibar/api.rs +++ b/libs/@local/graph/authorization/src/zanzibar/api.rs @@ -19,11 +19,12 @@ use crate::{ ModifyRelationshipOperation, ModifyRelationshipResponse, ReadError, ZanzibarBackend, }, schema::{ - AccountGroupPermission, AccountGroupRelationAndSubject, DataTypePermission, - DataTypeRelationAndSubject, EntityPermission, EntityRelationAndSubject, EntitySetting, - EntityTypePermission, EntityTypeRelationAndSubject, PropertyTypePermission, - PropertyTypeRelationAndSubject, SettingName, SettingRelationAndSubject, SettingSubject, - WebPermission, WebRelationAndSubject, + AccountGroupPermission, AccountGroupRelationAndSubject, AccountIdOrPublic, + AccountNamespace, DataTypePermission, DataTypeRelationAndSubject, EntityNamespace, + EntityPermission, EntityRelationAndSubject, EntitySetting, EntityTypePermission, + EntityTypeRelationAndSubject, PropertyTypePermission, PropertyTypeRelationAndSubject, + SettingName, SettingRelationAndSubject, SettingSubject, WebPermission, + WebRelationAndSubject, }, zanzibar::{ Consistency, Permission, Zookie, @@ -258,6 +259,38 @@ where .change_context(CheckError) } + #[tracing::instrument(level = "info", skip(self))] + async fn get_entities( + &self, + actor: AccountId, + permission: EntityPermission, + consistency: Consistency<'_>, + ) -> Result, Report> { + self.backend + .lookup_resources(&actor, &permission, &EntityNamespace::Entity, consistency) + .await + .change_context(ReadError) + } + + #[tracing::instrument(level = "info", skip(self))] + async fn get_entity_accounts( + &self, + entity: EntityUuid, + permission: EntityPermission, + consistency: Consistency<'_>, + ) -> Result, Report> { + self.backend + .lookup_subjects::( + &AccountNamespace::Account, + None, + &permission, + &entity, + consistency, + ) + .await + .change_context(ReadError) + } + #[tracing::instrument(level = "info", skip(self))] async fn get_entity_relations( &self, @@ -678,4 +711,47 @@ where ) -> Result> { self.backend.delete_relations(filter).await } + + async fn lookup_resources( + &self, + subject: &( + impl Subject, Relation: Serialize> + Sync + ), + permission: &(impl Serialize + Permission + Sync), + resource_kind: &O::Kind, + consistency: Consistency<'_>, + ) -> Result, Report> + where + for<'de> O: Resource + Send> + Send, + { + self.backend + .lookup_resources(subject, permission, resource_kind, consistency) + .await + } + + async fn lookup_subjects( + &self, + subject_type: &::Kind, + subject_relation: Option<&S::Relation>, + permission: &(impl Serialize + Permission + Sync), + resource: &O, + consistency: Consistency<'_>, + ) -> Result::Id>, Report> + where + for<'de> S: Subject< + Resource: Resource + Send>, + Relation: Serialize + Sync, + > + Send, + O: Resource + Sync, + { + self.backend + .lookup_subjects::( + subject_type, + subject_relation, + permission, + resource, + consistency, + ) + .await + } }