Skip to content

Commit

Permalink
Basic email mapper and initial data configs
Browse files Browse the repository at this point in the history
  • Loading branch information
augustuswm committed Sep 29, 2023
1 parent ab2aedf commit 2b9d6b3
Show file tree
Hide file tree
Showing 11 changed files with 372 additions and 51 deletions.
111 changes: 83 additions & 28 deletions rfd-api/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ use rfd_model::{
storage::{
AccessGroupFilter, AccessGroupStore, AccessTokenStore, ApiKeyFilter, ApiKeyStore,
ApiUserFilter, ApiUserProviderFilter, ApiUserProviderStore, ApiUserStore, JobStore,
ListPagination, LoginAttemptFilter, LoginAttemptStore, OAuthClientFilter,
OAuthClientRedirectUriStore, OAuthClientSecretStore, OAuthClientStore, RfdFilter,
RfdPdfFilter, RfdPdfStore, RfdRevisionFilter, RfdRevisionStore, RfdStore, StoreError,
ListPagination, LoginAttemptFilter, LoginAttemptStore, MapperFilter, MapperStore,
OAuthClientFilter, OAuthClientRedirectUriStore, OAuthClientSecretStore, OAuthClientStore,
RfdFilter, RfdPdfFilter, RfdPdfStore, RfdRevisionFilter, RfdRevisionStore, RfdStore,
StoreError,
},
AccessGroup, AccessToken, ApiUser, ApiUserProvider, InvalidValueError, Job, LoginAttempt,
NewAccessGroup, NewAccessToken, NewApiKey, NewApiUser, NewApiUserProvider, NewJob,
Expand All @@ -42,7 +43,7 @@ use crate::{
LoginError, UserInfo,
},
error::{ApiError, AppError},
mapper::{ApiPermissionMapper, GroupMapper},
mapper::{MapperRule, MapperRules},
permissions::{ApiPermission, PermissionStorage},
util::response::{client_error, internal_error},
ApiCaller, ApiPermissions, User, UserToken,
Expand All @@ -64,6 +65,7 @@ pub trait Storage:
+ OAuthClientSecretStore
+ OAuthClientRedirectUriStore
+ AccessGroupStore<ApiPermission>
+ MapperStore
+ Send
+ Sync
+ 'static
Expand All @@ -83,6 +85,7 @@ impl<T> Storage for T where
+ OAuthClientSecretStore
+ OAuthClientRedirectUriStore
+ AccessGroupStore<ApiPermission>
+ MapperStore
+ Send
+ Sync
+ 'static
Expand All @@ -98,7 +101,6 @@ pub struct ApiContext {
pub secrets: SecretContext,
pub oauth_providers: HashMap<OAuthProviderName, Box<dyn OAuthProviderFn>>,
pub search: SearchContext,
pub mappers: MapperContext,
}

pub struct JwtContext {
Expand All @@ -118,11 +120,6 @@ pub struct SearchContext {
pub index: String,
}

pub struct MapperContext {
pub direct_permissions: Vec<Box<dyn ApiPermissionMapper>>,
pub groups: Vec<Box<dyn GroupMapper>>,
}

pub struct RegisteredAccessToken {
pub access_token: AccessToken,
pub signed_token: String,
Expand Down Expand Up @@ -225,10 +222,6 @@ impl ApiContext {
client: SearchClient::new(search.host, search.key),
index: search.index,
},
mappers: MapperContext {
direct_permissions: vec![],
groups: vec![],
},
})
}

Expand Down Expand Up @@ -539,15 +532,11 @@ impl ApiContext {
.await?;

let mut mapped_permissions = ApiPermissions::new();

for mapper in &self.mappers.direct_permissions {
mapped_permissions.append(&mut mapper.permissions_for(&self, &info).await);
}

let mut mapped_groups = vec![];

for mapper in &self.mappers.groups {
mapped_groups.append(&mut mapper.groups_for(&self, &info).await);
for mapper in &self.get_mappers().await? {
mapped_permissions.append(&mut mapper.permissions_for(&self, &info).await?);
mapped_groups.append(&mut mapper.groups_for(&self, &info).await?);
}

match api_user_providers.len() {
Expand Down Expand Up @@ -596,16 +585,23 @@ impl ApiContext {
async fn ensure_api_user(
&self,
api_user_id: Uuid,
mapped_permissions: ApiPermissions,
mapped_grous: Vec<Uuid>,
mut mapped_permissions: ApiPermissions,
mut mapped_groups: Vec<Uuid>,
) -> Result<User, ApiError> {
match self.get_api_user(&api_user_id).await? {
Some(api_user) => Ok(api_user),
Some(api_user) => {
// Ensure that the existing user has "at least" the mapped permissions
let mut update: NewApiUser<ApiPermission> = api_user.into();
update.permissions.append(&mut mapped_permissions);
update.groups.append(&mut mapped_groups);

Ok(ApiUserStore::upsert(&*self.storage, update).await?)
}
None => self
.update_api_user(NewApiUser {
id: api_user_id,
permissions: mapped_permissions,
groups: mapped_grous,
groups: mapped_groups,
})
.await
.map_err(ApiError::Storage)
Expand Down Expand Up @@ -992,6 +988,26 @@ impl ApiContext {
None
})
}

// Mapper Operations

pub async fn get_mappers(&self) -> Result<Vec<MapperRules>, StoreError> {
Ok(MapperStore::list(
&*self.storage,
MapperFilter::default(),
&ListPagination::default().limit(UNLIMITED),
)
.await?
.into_iter()
.filter_map(|mapper| {
serde_json::from_value::<MapperRules>(mapper.rule)
.map_err(|err| {
tracing::error!(?err, "Failed to translate stored rule to mapper");
})
.ok()
})
.collect::<Vec<_>>())
}
}

#[cfg(test)]
Expand Down Expand Up @@ -1158,15 +1174,15 @@ pub(crate) mod test_mocks {
permissions::Caller,
storage::{
AccessGroupStore, AccessTokenStore, ApiKeyStore, ApiUserProviderStore, ApiUserStore,
JobStore, ListPagination, LoginAttemptStore, MockAccessGroupStore,
JobStore, ListPagination, LoginAttemptStore, MapperStore, MockAccessGroupStore,
MockAccessTokenStore, MockApiKeyStore, MockApiUserProviderStore, MockApiUserStore,
MockJobStore, MockLoginAttemptStore, MockOAuthClientRedirectUriStore,
MockJobStore, MockLoginAttemptStore, MockMapperStore, MockOAuthClientRedirectUriStore,
MockOAuthClientSecretStore, MockOAuthClientStore, MockRfdPdfStore,
MockRfdRevisionStore, MockRfdStore, OAuthClientRedirectUriStore,
OAuthClientSecretStore, OAuthClientStore, RfdPdfStore, RfdRevisionStore, RfdStore,
},
ApiKey, ApiUserProvider, NewAccessGroup, NewAccessToken, NewApiKey, NewApiUser,
NewApiUserProvider, NewJob, NewLoginAttempt, NewRfd, NewRfdPdf, NewRfdRevision,
NewApiUserProvider, NewJob, NewLoginAttempt, NewMapper, NewRfd, NewRfdPdf, NewRfdRevision,
};

use std::sync::Arc;
Expand Down Expand Up @@ -1227,6 +1243,7 @@ pub(crate) mod test_mocks {
pub oauth_client_secret_store: Option<Arc<MockOAuthClientSecretStore>>,
pub oauth_client_redirect_uri_store: Option<Arc<MockOAuthClientRedirectUriStore>>,
pub access_group_store: Option<Arc<MockAccessGroupStore<ApiPermission>>>,
pub mapper_store: Option<Arc<MockMapperStore>>,
}

impl MockStorage {
Expand All @@ -1246,6 +1263,7 @@ pub(crate) mod test_mocks {
oauth_client_secret_store: None,
oauth_client_redirect_uri_store: None,
access_group_store: None,
mapper_store: None,
}
}
}
Expand Down Expand Up @@ -1766,4 +1784,41 @@ pub(crate) mod test_mocks {
self.access_group_store.as_ref().unwrap().delete(id).await
}
}

#[async_trait]
impl MapperStore for MockStorage {
async fn get(
&self,
id: &uuid::Uuid,
deleted: bool,
) -> Result<Option<rfd_model::Mapper>, rfd_model::storage::StoreError> {
self.mapper_store.as_ref().unwrap().get(id, deleted).await
}

async fn list(
&self,
filter: rfd_model::storage::MapperFilter,
pagination: &ListPagination,
) -> Result<Vec<rfd_model::Mapper>, rfd_model::storage::StoreError> {
self.mapper_store
.as_ref()
.unwrap()
.list(filter, pagination)
.await
}

async fn upsert(
&self,
new_mapper: &NewMapper,
) -> Result<rfd_model::Mapper, rfd_model::storage::StoreError> {
self.mapper_store.as_ref().unwrap().upsert(new_mapper).await
}

async fn delete(
&self,
id: &uuid::Uuid,
) -> Result<Option<rfd_model::Mapper>, rfd_model::storage::StoreError> {
self.mapper_store.as_ref().unwrap().delete(id).await
}
}
}
47 changes: 47 additions & 0 deletions rfd-api/src/initial_data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use config::{Config, ConfigError, Environment, File};
use rfd_model::{storage::StoreError, NewAccessGroup};
use serde::Deserialize;
use uuid::Uuid;

use crate::{context::ApiContext, mapper::MapperRules, ApiPermissions};

#[derive(Debug, Deserialize)]
pub struct InitialData {
pub groups: Vec<InitialGroup>,
pub mappers: Vec<MapperRules>,
}

#[derive(Debug, Deserialize)]
pub struct InitialGroup {
pub name: String,
pub permissions: ApiPermissions,
}

impl InitialData {
pub fn new() -> Result<Self, ConfigError> {
let config = Config::builder()
.add_source(File::with_name("baseline.toml").required(false))
.add_source(File::with_name("rfd-api/baseline.toml").required(false))
.add_source(Environment::default())
.build()?;

config.try_deserialize()
}

pub async fn initialize(&self, ctx: &ApiContext) -> Result<(), StoreError> {
for group in &self.groups {
ctx.create_group(NewAccessGroup {
id: Uuid::new_v4(),
name: group.name.clone(),
permissions: group.permissions.clone(),
})
.await?;
}

for _mapper in &self.mappers {
// TODO: Configure initial mappers
}

Ok(())
}
}
5 changes: 5 additions & 0 deletions rfd-api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::{
endpoints::login::oauth::{
github::GitHubOAuthProvider, google::GoogleOAuthProvider, OAuthProviderName,
},
initial_data::InitialData,
};

mod authn;
Expand All @@ -27,6 +28,7 @@ mod context;
mod email_validator;
mod endpoints;
mod error;
mod initial_data;
mod mapper;
mod permissions;
mod server;
Expand Down Expand Up @@ -67,6 +69,9 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
)
.await?;

let init_data = InitialData::new()?;
init_data.initialize(&context).await?;

if let Some(github) = config.authn.oauth.github {
context.insert_oauth_provider(
OAuthProviderName::GitHub,
Expand Down
76 changes: 72 additions & 4 deletions rfd-api/src/mapper.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,82 @@
use async_trait::async_trait;
use rfd_model::storage::StoreError;
use serde::Deserialize;
use uuid::Uuid;

use crate::{context::ApiContext, endpoints::login::UserInfo, ApiPermissions};

#[async_trait]
pub trait ApiPermissionMapper: Send + Sync {
async fn permissions_for(&self, ctx: &ApiContext, user: &UserInfo) -> ApiPermissions;
pub trait MapperRule: Send + Sync {
async fn permissions_for(
&self,
ctx: &ApiContext,
user: &UserInfo,
) -> Result<ApiPermissions, StoreError>;
async fn groups_for(&self, ctx: &ApiContext, user: &UserInfo) -> Result<Vec<Uuid>, StoreError>;
}

#[derive(Debug, Deserialize)]
pub enum MapperRules {
EmailDomain(EmailDomainMapper),
}

#[derive(Debug, Deserialize)]
pub struct EmailDomainMapper {
domain: String,
groups: Vec<String>,
}

#[async_trait]
impl MapperRule for EmailDomainMapper {
async fn permissions_for(
&self,
_ctx: &ApiContext,
_user: &UserInfo,
) -> Result<ApiPermissions, StoreError> {
Ok(ApiPermissions::new())
}

async fn groups_for(&self, ctx: &ApiContext, user: &UserInfo) -> Result<Vec<Uuid>, StoreError> {
let has_email_in_domain = user
.verified_emails
.iter()
.fold(false, |found, email| found || email.ends_with(&self.domain));

if has_email_in_domain {
let groups = ctx
.get_groups()
.await?
.into_iter()
.filter_map(|group| {
if self.groups.contains(&group.name) {
Some(group.id)
} else {
None
}
})
.collect::<Vec<_>>();
Ok(groups)
} else {
Ok(vec![])
}
}
}

#[async_trait]
pub trait GroupMapper: Send + Sync {
async fn groups_for(&self, ctx: &ApiContext, user: &UserInfo) -> Vec<Uuid>;
impl MapperRule for MapperRules {
async fn permissions_for(
&self,
ctx: &ApiContext,
user: &UserInfo,
) -> Result<ApiPermissions, StoreError> {
match self {
Self::EmailDomain(rule) => rule.permissions_for(ctx, user).await,
}
}

async fn groups_for(&self, ctx: &ApiContext, user: &UserInfo) -> Result<Vec<Uuid>, StoreError> {
match self {
Self::EmailDomain(rule) => rule.groups_for(ctx, user).await,
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE mapper;
7 changes: 7 additions & 0 deletions rfd-model/migrations/2023-09-29-200104_add_mappers/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE mapper (
id UUID PRIMARY KEY,
name VARCHAR NOT NULL UNIQUE,
rule JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
Loading

0 comments on commit 2b9d6b3

Please sign in to comment.