diff --git a/v-api-permission-derive/src/lib.rs b/v-api-permission-derive/src/lib.rs index 3f5950e..bc54152 100644 --- a/v-api-permission-derive/src/lib.rs +++ b/v-api-permission-derive/src/lib.rs @@ -631,7 +631,7 @@ fn as_scope_trait_tokens( quote! { } }; let variant_ident = variant.ident.clone(); - quote! { #permission_type::#variant_ident #fields => #to } + quote! { #permission_type::#variant_ident #fields => Some(#to) } }) }); let from_scope_mapping = scope_settings @@ -658,30 +658,35 @@ fn as_scope_trait_tokens( }); quote! { - impl v_model::permissions::AsScope for #permission_type { - fn as_scope(&self) -> &str { - match self { - #(#as_scope_mapping,)* - _ => "" - } + impl v_model::permissions::AsScopeInternal for #permission_type { + fn as_scope(&self) -> Option<&str> { + ::as_scope(self).or_else(|| { + match self { + #(#as_scope_mapping,)* + _ => None, + } + }) } - fn from_scope( - scope: impl Iterator, - ) -> Result, v_model::permissions::PermissionError> + fn from_scope( + scope: T, + ) -> Permissions<#permission_type> where + T: Iterator + Clone, S: AsRef, { - let mut permissions = Permissions::new(); + let mut permissions = ::from_scope(scope.clone()); for entry in scope { match entry.as_ref() { #(#from_scope_mapping,)* - other => return Err(v_model::permissions::PermissionError::InvalidScope(other.to_string())), + other => { + // Drop any unrecognized scopes + } } } - Ok(permissions) + permissions } } } @@ -697,7 +702,7 @@ fn permission_storage_trait_tokens( let expand_tokens = permission_storage_expand_tokens(permission_type, expand_settings); quote! { - impl v_model::permissions::PermissionStorage for #permission_type { + impl v_model::permissions::PermissionStorageInternal for #permission_type { #contract_tokens #expand_tokens } @@ -765,6 +770,7 @@ fn permission_storage_contract_tokens( quote! { fn contract(collection: &Permissions) -> Permissions { + let mut base = ::contract(collection); let mut contracted = Vec::new(); #collection_instantiation @@ -780,7 +786,8 @@ fn permission_storage_contract_tokens( #collections_add - contracted.into() + base.append(&mut contracted.into()); + base } } } @@ -847,6 +854,7 @@ fn permission_storage_expand_tokens( actor: &newtype_uuid::TypedUuid, actor_permissions: Option<&Permissions>, ) -> Permissions { + let mut base = ::expand(collection, actor, actor_permissions.clone()); let mut expanded = Vec::new(); for p in collection.iter() { @@ -857,7 +865,8 @@ fn permission_storage_expand_tokens( } } - expanded.into() + base.append(&mut expanded.into()); + base } } } diff --git a/v-api-permission-derive/tests/derive.rs b/v-api-permission-derive/tests/derive.rs index ae3dc00..bced2e5 100644 --- a/v-api-permission-derive/tests/derive.rs +++ b/v-api-permission-derive/tests/derive.rs @@ -1,9 +1,8 @@ -use newtype_uuid::TypedUuid; use std::collections::BTreeSet; use uuid::Uuid; use v_api::permissions::VPermission; use v_api_permission_derive::v_api; -use v_model::{Permissions, UserId, ApiKeyId, MapperId, AccessGroupId, OAuthClientId}; +use v_model::{permissions::{AsScope, PermissionStorage}, Permissions}; #[test] fn test_derive() { @@ -28,4 +27,7 @@ fn test_derive() { #[v_api(expand(kind = alias, variant = ReadItem, source = actor), scope(to = "read", from = "read"))] ReadItemsAssigned, } + + impl AsScope for AppPermissions {} + impl PermissionStorage for AppPermissions {} } diff --git a/v-api/src/authn/jwt.rs b/v-api/src/authn/jwt.rs index 08cbecd..b68e23a 100644 --- a/v-api/src/authn/jwt.rs +++ b/v-api/src/authn/jwt.rs @@ -20,13 +20,13 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use tracing::instrument; use v_model::{ - permissions::{Permission, AsScope, PermissionStorage}, AccessTokenId, ApiUser, ApiUserProvider, UserId, UserProviderId, + AccessTokenId, ApiUser, ApiUserProvider, UserId, UserProviderId, }; use crate::{ config::AsymmetricKey, context::VContext, - permissions::VPermission, + permissions::VAppPermission, }; use super::{Signer, SigningKeyError}; @@ -73,7 +73,7 @@ impl Claims { expires_at: DateTime, ) -> Self where - T: Permission + From + AsScope + PermissionStorage, + T: VAppPermission, { Claims { iss: ctx.public_url().to_string(), @@ -91,7 +91,7 @@ impl Claims { impl Jwt { pub async fn new(ctx: &VContext, token: &str) -> Result where - T: Permission + From + AsScope + PermissionStorage, + T: VAppPermission, { tracing::trace!("Decode JWT from headers"); diff --git a/v-api/src/context.rs b/v-api/src/context.rs index 4d6903b..7963cf1 100644 --- a/v-api/src/context.rs +++ b/v-api/src/context.rs @@ -18,7 +18,7 @@ use thiserror::Error; use tracing::{info_span, instrument, Instrument}; use uuid::Uuid; use v_model::{ - permissions::{AsScope, Caller, Permission, PermissionError, PermissionStorage, Permissions}, + permissions::{AsScopeInternal, Caller, Permission, PermissionError, PermissionStorageInternal, Permissions}, schema_ext::LoginAttemptState, storage::{ AccessGroupFilter, AccessGroupStore, AccessTokenStore, ApiKeyFilter, ApiKeyStore, @@ -50,7 +50,7 @@ use crate::{ }, error::{ApiError, AppError}, mapper::{MapperRule, Mapping}, - permissions::VPermission, + permissions::{VAppPermission, VPermission}, util::response::{ bad_request, client_error, internal_error, resource_error, resource_restricted, ResourceError, ResourceResult, ToResourceResult, ToResourceResultOpt, @@ -107,13 +107,13 @@ pub struct VContext { } pub trait ApiContext: ServerContext { - type AppPermissions: Permission + From + AsScope; + type AppPermissions: VAppPermission; fn v_ctx(&self) -> &VContext; } impl ApiContext for VContext where - T: Permission + From + AsScope, + T: VAppPermission, { type AppPermissions = T; fn v_ctx(&self) -> &VContext { @@ -157,7 +157,7 @@ impl From for HttpError { impl VContextWithCaller for RequestContext where - T: Permission + From + AsScope + PermissionStorage, + T: VAppPermission, U: ApiContext, { async fn as_ctx(&self) -> Result<(&VContext, Caller), VContextCallerError> { @@ -229,7 +229,7 @@ enum BasePermissions { impl VContext where - T: Permission + From + AsScope + PermissionStorage, + T: VAppPermission, { pub async fn new( public_url: String, @@ -351,7 +351,7 @@ where BasePermissions::Full => user_permissions.clone(), BasePermissions::Restricted(permissions) => { let token_permissions = - T::expand(permissions, &user.id, Some(&user_permissions)); + ::expand(permissions, &user.id, Some(&user_permissions)); token_permissions.intersect(&user_permissions) } }; @@ -470,7 +470,7 @@ where AuthToken::Jwt(jwt) => { // AuthnToken::Jwt can only be generated from a verified JWT let permissions = match &jwt.claims.scp { - Some(scp) => BasePermissions::Restricted(T::from_scope(scp.iter())?), + Some(scp) => BasePermissions::Restricted(::from_scope(scp.iter())), None => BasePermissions::Full, }; Ok((jwt.claims.sub, permissions)) @@ -508,7 +508,7 @@ where let permissions = groups .into_iter() .fold(Permissions::new(), |mut aggregate, group| { - let mut expanded = T::expand(&group.permissions, &user.id, Some(&user.permissions)); + let mut expanded = ::expand(&group.permissions, &user.id, Some(&user.permissions)); tracing::trace!(group_id = ?group.id, group_name = ?group.name, permissions = ?expanded, "Transformed group into permission set"); aggregate.append(&mut expanded); @@ -810,7 +810,7 @@ where .await .map(|opt| { opt.map(|mut user| { - user.permissions = T::expand(&user.permissions, &user.id, None); + user.permissions = ::expand(&user.permissions, &user.id, None); user }) }) @@ -853,7 +853,7 @@ where permissions: permissions, groups: groups, }; - new_user.permissions = T::contract(&new_user.permissions); + new_user.permissions = ::contract(&new_user.permissions); ApiUserStore::upsert(&*self.storage, new_user) .await .to_resource_result() @@ -872,7 +872,7 @@ where &VPermission::ManageApiUser(api_user.id).into(), &VPermission::ManageApiUsersAll.into(), ]) { - api_user.permissions = T::contract(&api_user.permissions); + api_user.permissions = ::contract(&api_user.permissions); ApiUserStore::upsert(&*self.storage, api_user) .await .to_resource_result() diff --git a/v-api/src/endpoints/login/oauth/code.rs b/v-api/src/endpoints/login/oauth/code.rs index 65f9235..8157cd9 100644 --- a/v-api/src/endpoints/login/oauth/code.rs +++ b/v-api/src/endpoints/login/oauth/code.rs @@ -27,7 +27,7 @@ use tap::TapFallible; use tracing::instrument; use v_model::{ schema_ext::LoginAttemptState, LoginAttempt, LoginAttemptId, NewLoginAttempt, OAuthClient, - OAuthClientId, permissions::{AsScope, PermissionStorage} + OAuthClientId, permissions::PermissionStorage }; use super::{OAuthProvider, OAuthProviderNameParam, UserInfoProvider}; @@ -39,7 +39,7 @@ use crate::{ LoginError, UserInfo, }, error::ApiError, - permissions::{VAppPermission, VPermission}, + permissions::VAppPermission, secrets::OpenApiSecretString, util::{ request::RequestCookies, @@ -182,9 +182,6 @@ where // Check that the passed in scopes are valid. The scopes are not currently restricted by client let scope = query.scope.unwrap_or_else(|| DEFAULT_SCOPE.to_string()); - let scope_error = VPermission::from_scope_arg(&scope) - .err() - .map(|_| "invalid_scope".to_string()); // Construct a new login attempt with the minimum required values let mut attempt = NewLoginAttempt::new( @@ -202,8 +199,7 @@ where // TODO: Make this configurable attempt.expires_at = Some(Utc::now().add(TimeDelta::try_minutes(5).unwrap())); - // Assign any scope errors that arose - attempt.error = scope_error; + // TODO: Assign any scope errors that arose. Currently we drop any unknown or unsupported scopes // Add in the user defined state and redirect uri attempt.state = Some(query.state); diff --git a/v-api/src/endpoints/mappers.rs b/v-api/src/endpoints/mappers.rs index 6698b66..d9d17cf 100644 --- a/v-api/src/endpoints/mappers.rs +++ b/v-api/src/endpoints/mappers.rs @@ -12,7 +12,7 @@ use v_model::{permissions::{Permission, AsScope, PermissionStorage}, Mapper, Map use crate::{ context::ApiContext, mapper::MappingRules, - permissions::VPermission, + permissions::{VAppPermission, VPermission}, util::{ is_uniqueness_error, response::{conflict, ResourceError}, @@ -58,7 +58,7 @@ pub async fn create_mapper_op( ) -> Result, HttpError> where T: ApiContext, - T::AppPermissions: Permission + From + AsScope + PermissionStorage, + T::AppPermissions: VAppPermission, { let ctx = rqctx.v_ctx(); let auth = ctx.authn_token(&rqctx).await?; diff --git a/v-api/src/mapper/default.rs b/v-api/src/mapper/default.rs index 7a8d7fd..fab4163 100644 --- a/v-api/src/mapper/default.rs +++ b/v-api/src/mapper/default.rs @@ -8,7 +8,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; use v_model::{ - permissions::{Permission, Permissions, AsScope, PermissionStorage}, + permissions::Permissions, storage::StoreError, AccessGroupId, }; @@ -16,7 +16,7 @@ use v_model::{ use crate::{ context::VContext, endpoints::login::UserInfo, - permissions::VPermission, + permissions::VAppPermission, util::response::ResourceResult, }; @@ -32,7 +32,7 @@ pub struct DefaultMapper { #[async_trait] impl MapperRule for DefaultMapper where - T: Permission + From + AsScope + PermissionStorage, + T: VAppPermission, { async fn permissions_for( &self, diff --git a/v-api/src/mapper/email_address.rs b/v-api/src/mapper/email_address.rs index 8be8ced..3f5762c 100644 --- a/v-api/src/mapper/email_address.rs +++ b/v-api/src/mapper/email_address.rs @@ -9,7 +9,7 @@ use newtype_uuid::TypedUuid; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use v_model::{ - permissions::{Permission, Permissions, AsScope, PermissionStorage}, + permissions::Permissions, storage::StoreError, AccessGroupId, }; @@ -17,7 +17,7 @@ use v_model::{ use crate::{ context::VContext, endpoints::login::UserInfo, - permissions::VPermission, + permissions::VAppPermission, util::response::ResourceResult, }; @@ -34,7 +34,7 @@ pub struct EmailAddressMapper { #[async_trait] impl MapperRule for EmailAddressMapper where - T: Permission + From + AsScope + PermissionStorage, + T: VAppPermission { async fn permissions_for( &self, diff --git a/v-api/src/mapper/email_domain.rs b/v-api/src/mapper/email_domain.rs index 5ed1698..a2d7318 100644 --- a/v-api/src/mapper/email_domain.rs +++ b/v-api/src/mapper/email_domain.rs @@ -9,7 +9,7 @@ use newtype_uuid::TypedUuid; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use v_model::{ - permissions::{Permission, Permissions, AsScope, PermissionStorage}, + permissions::Permissions, storage::StoreError, AccessGroupId, }; @@ -17,7 +17,7 @@ use v_model::{ use crate::{ context::VContext, endpoints::login::UserInfo, - permissions::VPermission, + permissions::VAppPermission, util::response::ResourceResult, }; @@ -34,7 +34,7 @@ pub struct EmailDomainMapper { #[async_trait] impl MapperRule for EmailDomainMapper where - T: Permission + From + AsScope + PermissionStorage, + T: VAppPermission, { async fn permissions_for( &self, diff --git a/v-api/src/mapper/github_username.rs b/v-api/src/mapper/github_username.rs index 42bd795..e90a8d0 100644 --- a/v-api/src/mapper/github_username.rs +++ b/v-api/src/mapper/github_username.rs @@ -9,7 +9,7 @@ use newtype_uuid::TypedUuid; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use v_model::{ - permissions::{Permission, Permissions, AsScope, PermissionStorage}, + permissions::Permissions, storage::StoreError, AccessGroupId, }; @@ -17,7 +17,7 @@ use v_model::{ use crate::{ context::VContext, endpoints::login::UserInfo, - permissions::VPermission, + permissions::VAppPermission, util::response::ResourceResult, }; @@ -34,7 +34,7 @@ pub struct GitHubUsernameMapper { #[async_trait] impl MapperRule for GitHubUsernameMapper where - T: Permission + From + AsScope + PermissionStorage, + T: VAppPermission, { async fn permissions_for( &self, diff --git a/v-api/src/mapper/mod.rs b/v-api/src/mapper/mod.rs index c18039d..3505dae 100644 --- a/v-api/src/mapper/mod.rs +++ b/v-api/src/mapper/mod.rs @@ -10,7 +10,7 @@ use schemars::JsonSchema; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tap::TapFallible; use v_model::{ - permissions::{Permission, Permissions, AsScope, PermissionStorage}, + permissions::Permissions, storage::StoreError, AccessGroupId, Mapper, MapperId, }; @@ -18,7 +18,7 @@ use v_model::{ use crate::{ context::VContext, endpoints::login::UserInfo, - permissions::VPermission, + permissions::VAppPermission, util::response::ResourceResult, }; @@ -35,7 +35,7 @@ pub mod github_username; #[async_trait] pub trait MapperRule: Send + Sync where - T: Permission + From + AsScope + PermissionStorage, + T: VAppPermission, { async fn permissions_for( &self, @@ -91,7 +91,7 @@ pub enum MappingRules { #[async_trait] impl MapperRule for MappingRules where - T: Permission + From + AsScope + PermissionStorage, + T: VAppPermission, { async fn permissions_for( &self, diff --git a/v-api/src/permissions.rs b/v-api/src/permissions.rs index cecc0c3..54b1092 100644 --- a/v-api/src/permissions.rs +++ b/v-api/src/permissions.rs @@ -2,17 +2,16 @@ // 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/. -use newtype_uuid::TypedUuid; use partial_struct::partial; use v_api_permission_derive::v_api; use schemars::JsonSchema; use serde::{Serialize, Deserialize}; use std::collections::BTreeSet; -use v_model::permissions::{Permission, Permissions}; -use v_model::{AccessGroupId, ApiKeyId, MapperId, OAuthClientId, UserId, permissions::AsScope}; +use v_model::permissions::{AsScopeInternal, Permission, PermissionStorage, PermissionStorageInternal, Permissions}; +use v_model::permissions::AsScope; -pub trait VAppPermission: Permission + From + AsScope {} -impl VAppPermission for T where T: Permission + From + AsScope {} +pub trait VAppPermission: Permission + From + AsScopeInternal + PermissionStorageInternal {} +impl VAppPermission for T where T: Permission + From + AsScopeInternal + PermissionStorageInternal {} pub trait VAppPermissionResponse: Permission {} impl VAppPermissionResponse for T where T: Permission {} @@ -26,3 +25,6 @@ impl VAppPermissionResponse for T where T: Permission {} Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, PartialOrd, Ord, )] pub enum VPermission {} + +impl AsScope for VPermission {} +impl PermissionStorage for VPermission {} diff --git a/v-model/src/permissions.rs b/v-model/src/permissions.rs index 115f497..85229c3 100644 --- a/v-model/src/permissions.rs +++ b/v-model/src/permissions.rs @@ -200,18 +200,53 @@ pub enum PermissionError { } pub trait AsScope: Sized { - fn as_scope(&self) -> &str; - fn from_scope_arg(scope_arg: &str) -> Result, PermissionError> { + fn as_scope(&self) -> Option<&str> { + None + } + fn from_scope_arg(scope_arg: &str) -> Permissions { Self::from_scope(scope_arg.split(' ')) } - fn from_scope( - scope: impl Iterator, - ) -> Result, PermissionError> + fn from_scope( + _scope: T, + ) -> Permissions + where + T: Iterator + Clone, + S: AsRef { + Permissions::default() + } +} + +pub trait AsScopeInternal: Sized + AsScope { + fn as_scope(&self) -> Option<&str>; + fn from_scope_arg(scope_arg: &str) -> Permissions { + ::from_scope(scope_arg.split(' ')) + } + fn from_scope( + scope: T, + ) -> Permissions where + T: Iterator + Clone, S: AsRef; } pub trait PermissionStorage { + fn contract(_collection: &Permissions) -> Permissions + where + Self: Sized { + Permissions::default() + } + fn expand( + _collection: &Permissions, + _actor: &TypedUuid, + _actor_permissions: Option<&Permissions>, + ) -> Permissions + where + Self: Sized { + Permissions::default() + } +} + +pub trait PermissionStorageInternal: PermissionStorage { fn contract(collection: &Permissions) -> Permissions where Self: Sized;