Skip to content

Commit

Permalink
Work on making mappers consumable
Browse files Browse the repository at this point in the history
  • Loading branch information
augustuswm committed Oct 1, 2023
1 parent 2b9d6b3 commit 9a80692
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 41 deletions.
96 changes: 78 additions & 18 deletions rfd-api/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use rfd_model::{
AccessGroup, AccessToken, ApiUser, ApiUserProvider, InvalidValueError, Job, LoginAttempt,
NewAccessGroup, NewAccessToken, NewApiKey, NewApiUser, NewApiUserProvider, NewJob,
NewLoginAttempt, NewOAuthClient, NewOAuthClientRedirectUri, NewOAuthClientSecret, OAuthClient,
OAuthClientRedirectUri, OAuthClientSecret,
OAuthClientRedirectUri, OAuthClientSecret, NewMapper
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand All @@ -37,13 +37,12 @@ use crate::{
AuthError, AuthToken, Signer,
},
config::{AsymmetricKey, JwtConfig, SearchConfig},
email_validator::EmailValidator,
endpoints::login::{
oauth::{OAuthProvider, OAuthProviderError, OAuthProviderFn, OAuthProviderName},
LoginError, UserInfo,
},
error::{ApiError, AppError},
mapper::{MapperRule, MapperRules},
mapper::{MapperRule, MappingRules, Mapping},
permissions::{ApiPermission, PermissionStorage},
util::response::{client_error, internal_error},
ApiCaller, ApiPermissions, User, UserToken,
Expand Down Expand Up @@ -93,7 +92,6 @@ impl<T> Storage for T where
}

pub struct ApiContext {
pub email_validator: Arc<dyn EmailValidator + Send + Sync>,
pub https_client: Client<HttpsConnector<HttpConnector>, Body>,
pub public_url: String,
pub storage: Arc<dyn Storage>,
Expand Down Expand Up @@ -187,7 +185,6 @@ pub struct FullRfdPdfEntry {

impl ApiContext {
pub async fn new(
email_validator: Arc<dyn EmailValidator + Send + Sync>,
public_url: String,
storage: Arc<dyn Storage>,
jwt: JwtConfig,
Expand All @@ -201,7 +198,6 @@ impl ApiContext {
}

Ok(Self {
email_validator,
https_client: hyper::Client::builder().build(HttpsConnector::new()),
public_url,
storage,
Expand Down Expand Up @@ -531,13 +527,7 @@ impl ApiContext {
.list_api_user_provider(filter, &ListPagination::latest())
.await?;

let mut mapped_permissions = ApiPermissions::new();
let mut mapped_groups = vec![];

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?);
}
let (mapped_permissions, mapped_groups) = self.get_mapped_fields(&info).await?;

match api_user_providers.len() {
0 => {
Expand Down Expand Up @@ -582,6 +572,53 @@ impl ApiContext {
}
}

async fn get_mapped_fields(&self, info: &UserInfo) -> Result<(ApiPermissions, Vec<Uuid>), StoreError> {
let mut mapped_permissions = ApiPermissions::new();
let mut mapped_groups = vec![];

// We optimistically load mappers here. We do not want to take a lock on the mappers and
// instead handle mappers that become depleted before we can evaluate them at evaluation
// time.
for mapping in self.get_mappers().await? {
let (permissions, groups) = (
mapping.rule.permissions_for(&self, &info).await?,
mapping.rule.groups_for(&self, &info).await?,
);

// If a rule is set to apply a permission or group to a user, then the rule needs to be
// checked for usage. If it does not have an activation limit then nothing is needed.
// If it does have a limit then we need to attempt to consume an activation. If the
// consumption works then we add the permissions. If they fail then we do not, but we
// do not fail the entire mapping process
let apply = if !permissions.is_empty() || !groups.is_empty() {
if mapping.max_activations.is_some() {
match self.consume_mapping_activation(&mapping).await {
Ok(_) => {
true
}
Err(err) => {
// TODO: Inspect the error. We expect to see a conflict error, and
// should is expected to be seen. Other errors are problematic.
tracing::warn!(?err, "Login may have attempted to use depleted mapper. This may be ok if it is an isolated occurrence, but should occur repeatedly.");
false
}
}
} else {
true
}
} else {
false
};

if apply {
mapped_permissions.append(&mut mapping.rule.permissions_for(&self, &info).await?);
mapped_groups.append(&mut mapping.rule.groups_for(&self, &info).await?);
}
}

Ok((mapped_permissions, mapped_groups))
}

async fn ensure_api_user(
&self,
api_user_id: Uuid,
Expand Down Expand Up @@ -991,7 +1028,7 @@ impl ApiContext {

// Mapper Operations

pub async fn get_mappers(&self) -> Result<Vec<MapperRules>, StoreError> {
pub async fn get_mappers(&self) -> Result<Vec<Mapping>, StoreError> {
Ok(MapperStore::list(
&*self.storage,
MapperFilter::default(),
Expand All @@ -1000,14 +1037,37 @@ impl ApiContext {
.await?
.into_iter()
.filter_map(|mapper| {
serde_json::from_value::<MapperRules>(mapper.rule)
serde_json::from_value::<MappingRules>(mapper.rule)
.map_err(|err| {
tracing::error!(?err, "Failed to translate stored rule to mapper");
})
.ok()
.map(|rule| {
Mapping {
id: mapper.id,
name: mapper.name,
rule,
activations: mapper.activations,
max_activations: mapper.max_activations,
}
})
})
.collect::<Vec<_>>())
}

async fn consume_mapping_activation(&self, mapping: &Mapping) -> Result<(), StoreError> {
Ok(MapperStore::upsert(&*self.storage, &NewMapper {
id: mapping.id,
name: mapping.name.clone(),
// If a rule fails to serialize, then something critical has gone wrong. Rules should
// never be modified after they are created, and rules must be persisted before they
// can be used for an activation. So if a rule fails to serialize, then the stored rule
// has become corrupted or something in the application has manipulated the rule.
rule: serde_json::to_value(&mapping.rule).expect("Store rules must be able to be re-serialized"),
activations: mapping.activations.map(|i| i + 1),
max_activations: mapping.max_activations,
}).await.map(|_| ())?)
}
}

#[cfg(test)]
Expand Down Expand Up @@ -1191,15 +1251,14 @@ pub(crate) mod test_mocks {
config::{JwtConfig, SearchConfig},
endpoints::login::oauth::{google::GoogleOAuthProvider, OAuthProviderName},
permissions::ApiPermission,
util::tests::{mock_key, AnyEmailValidator},
util::tests::mock_key,
};

use super::ApiContext;

// Construct a mock context that can be used in tests
pub async fn mock_context(storage: MockStorage) -> ApiContext {
let mut ctx = ApiContext::new(
Arc::new(AnyEmailValidator),
"".to_string(),
Arc::new(storage),
JwtConfig::default(),
Expand Down Expand Up @@ -1790,9 +1849,10 @@ pub(crate) mod test_mocks {
async fn get(
&self,
id: &uuid::Uuid,
used: bool,
deleted: bool,
) -> Result<Option<rfd_model::Mapper>, rfd_model::storage::StoreError> {
self.mapper_store.as_ref().unwrap().get(id, deleted).await
self.mapper_store.as_ref().unwrap().get(id, used, deleted).await
}

async fn list(
Expand Down
9 changes: 7 additions & 2 deletions rfd-api/src/initial_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ use rfd_model::{storage::StoreError, NewAccessGroup};
use serde::Deserialize;
use uuid::Uuid;

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

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

#[derive(Debug, Deserialize)]
Expand All @@ -17,6 +17,11 @@ pub struct InitialGroup {
pub permissions: ApiPermissions,
}

#[derive(Debug, Deserialize)]
pub struct InitialMapper {
pub rule: MappingRules,
}

impl InitialData {
pub fn new() -> Result<Self, ConfigError> {
let config = Config::builder()
Expand Down
3 changes: 0 additions & 3 deletions rfd-api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ use tracing_subscriber::EnvFilter;

use crate::{
config::{AppConfig, ServerLogFormat},
email_validator::DomainValidator,
endpoints::login::oauth::{
github::GitHubOAuthProvider, google::GoogleOAuthProvider, OAuthProviderName,
},
Expand All @@ -25,7 +24,6 @@ use crate::{
mod authn;
mod config;
mod context;
mod email_validator;
mod endpoints;
mod error;
mod initial_data;
Expand Down Expand Up @@ -54,7 +52,6 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
};

let mut context = ApiContext::new(
Arc::new(DomainValidator::new(vec![])),
config.public_url,
Arc::new(
PostgresStore::new(&config.database_url)
Expand Down
19 changes: 14 additions & 5 deletions rfd-api/src/mapper.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use async_trait::async_trait;
use rfd_model::storage::StoreError;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::{context::ApiContext, endpoints::login::UserInfo, ApiPermissions};
Expand All @@ -15,12 +15,21 @@ pub trait MapperRule: Send + Sync {
async fn groups_for(&self, ctx: &ApiContext, user: &UserInfo) -> Result<Vec<Uuid>, StoreError>;
}

#[derive(Debug, Deserialize)]
pub enum MapperRules {
#[derive(Debug, Serialize)]
pub struct Mapping {
pub id: Uuid,
pub name: String,
pub rule: MappingRules,
pub activations: Option<i32>,
pub max_activations: Option<i32>,
}

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

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Serialize)]
pub struct EmailDomainMapper {
domain: String,
groups: Vec<String>,
Expand Down Expand Up @@ -63,7 +72,7 @@ impl MapperRule for EmailDomainMapper {
}

#[async_trait]
impl MapperRule for MapperRules {
impl MapperRule for MappingRules {
async fn permissions_for(
&self,
ctx: &ApiContext,
Expand Down
9 changes: 1 addition & 8 deletions rfd-api/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ pub mod tests {
RsaPrivateKey, RsaPublicKey,
};

use crate::{config::AsymmetricKey, email_validator::EmailValidator};
use crate::config::AsymmetricKey;

pub fn get_status<T>(res: &Result<T, HttpError>) -> StatusCode
where
Expand All @@ -153,13 +153,6 @@ pub mod tests {
}
}

pub struct AnyEmailValidator;
impl EmailValidator for AnyEmailValidator {
fn validate(&self, _email: &str) -> bool {
true
}
}

pub fn mock_key() -> AsymmetricKey {
let mut rng = rand::thread_rng();
let bits = 2048;
Expand Down
6 changes: 5 additions & 1 deletion rfd-model/migrations/2023-09-29-200104_add_mappers/up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ CREATE TABLE mapper (
id UUID PRIMARY KEY,
name VARCHAR NOT NULL UNIQUE,
rule JSONB NOT NULL,
activations INTEGER,
max_activations INTEGER,
depleted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
deleted_at TIMESTAMPTZ,
CHECK (activations <= max_activations)
);
3 changes: 3 additions & 0 deletions rfd-model/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ pub struct MapperModel {
pub id: Uuid,
pub name: String,
pub rule: Value,
pub activations: Option<i32>,
pub max_activations: Option<i32>,
pub depleted_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub deleted_at: Option<DateTime<Utc>>,
}
7 changes: 7 additions & 0 deletions rfd-model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,10 @@ pub struct Mapper {
pub id: Uuid,
pub name: String,
pub rule: Value,
pub activations: Option<i32>,
pub max_activations: Option<i32>,
#[partial(NewMapper(skip))]
pub depleted_at: Option<DateTime<Utc>>,
#[partial(NewMapper(skip))]
pub created_at: DateTime<Utc>,
#[partial(NewMapper(skip))]
Expand All @@ -436,6 +440,9 @@ impl From<MapperModel> for Mapper {
id: value.id,
name: value.name,
rule: value.rule,
activations: value.activations,
max_activations: value.max_activations,
depleted_at: value.depleted_at,
created_at: value.created_at,
deleted_at: value.deleted_at,
}
Expand Down
4 changes: 4 additions & 0 deletions rfd-model/src/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ where
pub fn len(&self) -> usize {
self.0.len()
}

pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}

impl<T> From<BTreeSet<T>> for Permissions<T>
Expand Down
3 changes: 3 additions & 0 deletions rfd-model/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ diesel::table! {
id -> Uuid,
name -> Varchar,
rule -> Jsonb,
activations -> Nullable<Int4>,
max_activations -> Nullable<Int4>,
depleted_at -> Nullable<Timestamptz>,
created_at -> Timestamptz,
deleted_at -> Nullable<Timestamptz>,
}
Expand Down
3 changes: 2 additions & 1 deletion rfd-model/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,13 +406,14 @@ pub trait AccessGroupStore<T: Permission + Ord> {
pub struct MapperFilter {
pub id: Option<Vec<Uuid>>,
pub name: Option<Vec<String>>,
pub depleted: bool,
pub deleted: bool,
}

#[cfg_attr(feature = "mock", automock)]
#[async_trait]
pub trait MapperStore {
async fn get(&self, id: &Uuid, deleted: bool) -> Result<Option<Mapper>, StoreError>;
async fn get(&self, id: &Uuid, depleted: bool, deleted: bool) -> Result<Option<Mapper>, StoreError>;
async fn list(
&self,
filter: MapperFilter,
Expand Down
Loading

0 comments on commit 9a80692

Please sign in to comment.