Skip to content

Commit

Permalink
Refactor to add unauthenticated caller. Add RFD tests
Browse files Browse the repository at this point in the history
  • Loading branch information
augustuswm committed Nov 6, 2023
1 parent 9391e7b commit c6b743f
Show file tree
Hide file tree
Showing 14 changed files with 449 additions and 102 deletions.
126 changes: 82 additions & 44 deletions rfd-api/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ pub struct ApiContext {
pub https_client: Client<HttpsConnector<HttpConnector>, Body>,
pub public_url: String,
pub storage: Arc<dyn Storage>,
pub unauthenticated_caller: ApiCaller,
pub jwt: JwtContext,
pub secrets: SecretContext,
pub oauth_providers: HashMap<OAuthProviderName, Box<dyn OAuthProviderFn>>,
Expand Down Expand Up @@ -220,6 +221,10 @@ impl ApiContext {
public_url,
storage,

unauthenticated_caller: ApiCaller {
id: Uuid::new_v4(),
permissions: vec![ApiPermission::SearchRfds].into(),
},
jwt: JwtContext {
default_expiration: jwt.default_expiration,
max_expiration: jwt.max_expiration,
Expand All @@ -243,8 +248,17 @@ impl ApiContext {
self.storage = storage;
}

pub async fn authn_token(&self, rqctx: &RequestContext<Self>) -> Result<AuthToken, AuthError> {
AuthToken::extract(rqctx).await
pub async fn authn_token(
&self,
rqctx: &RequestContext<Self>,
) -> Result<Option<AuthToken>, AuthError> {
match AuthToken::extract(rqctx).await {
Ok(token) => Ok(Some(token)),
Err(err) => match err {
AuthError::NoToken => Ok(None),
other => Err(other),
},
}
}

pub fn default_jwt_expiration(&self) -> i64 {
Expand All @@ -265,33 +279,42 @@ impl ApiContext {
}

#[instrument(skip(self, auth))]
pub async fn get_caller(&self, auth: &AuthToken) -> Result<ApiCaller, CallerError> {
let (api_user_id, permissions) = self.get_base_permissions(&auth).await?;
pub async fn get_caller(&self, auth: Option<&AuthToken>) -> Result<ApiCaller, CallerError> {
match auth {
Some(token) => {
let (api_user_id, permissions) = self.get_base_permissions(&token).await?;

// The permissions for the caller is the intersection of the user's permissions and the tokens permissions
if let Some(user) = self.get_api_user(&api_user_id).await? {
let user_permissions = self.get_user_permissions(&user).await?;
let token_permissions = permissions.expand(&user.id, Some(&user_permissions));
// The permissions for the caller is the intersection of the user's permissions and the tokens permissions
if let Some(user) = self.get_api_user(&api_user_id).await? {
let user_permissions = self.get_user_permissions(&user).await?;
let token_permissions = permissions.expand(&user.id, Some(&user_permissions));

let combined_permissions = token_permissions.intersect(&user_permissions);
let combined_permissions = token_permissions.intersect(&user_permissions);

tracing::info!(token = ?token_permissions, user = ?user_permissions, combined = ?combined_permissions, "Computed caller permissions");
tracing::info!(token = ?token_permissions, user = ?user_permissions, combined = ?combined_permissions, "Computed caller permissions");

let caller = Caller {
id: api_user_id,
permissions: combined_permissions,
// user,
};
let caller = Caller {
id: api_user_id,
permissions: combined_permissions,
// user,
};

tracing::info!(?caller, "Resolved caller");
tracing::info!(?caller, "Resolved caller");

Ok(caller)
} else {
tracing::error!("User for verified token does not exist");
Err(CallerError::FailedToAuthenticate)
Ok(caller)
} else {
tracing::error!("User for verified token does not exist");
Err(CallerError::FailedToAuthenticate)
}
}
None => Ok(self.get_unauthenticated_caller().clone()),
}
}

pub fn get_unauthenticated_caller(&self) -> &ApiCaller {
&self.unauthenticated_caller
}

async fn get_base_permissions(
&self,
auth: &AuthToken,
Expand Down Expand Up @@ -432,29 +455,33 @@ impl ApiContext {
caller: &Caller<ApiPermission>,
filter: Option<RfdFilter>,
) -> Result<Vec<ListRfd>, StoreError> {
let mut filter = filter.unwrap_or_default();

if !caller.can(&ApiPermission::GetRfdsAll) {
let numbers = caller
.permissions
.iter()
.filter_map(|p| match p {
ApiPermission::GetRfd(number) => Some(*number),
_ => None,
})
.collect::<Vec<_>>();

filter = filter.rfd_number(Some(numbers));
}

// List all of the RFDs first and then perform filter. This should be be improved once
// filters can be combined to support OR expressions. Effectively we need to be able to
// express "Has access to OR is public" with a filter
let mut rfds = RfdStore::list(
&*self.storage,
filter,
filter.unwrap_or_default(),
&ListPagination::default().limit(UNLIMITED),
)
.await
.tap_err(|err| tracing::error!(?err, "Failed to lookup RFDs"))?;

// Determine the list of RFDs the caller has direct access to
let direct_access_rfds = caller
.permissions
.iter()
.filter_map(|p| match p {
ApiPermission::GetRfd(number) => Some(*number),
_ => None,
})
.collect::<BTreeSet<_>>();

rfds.retain_mut(|rfd| {
caller.can(&ApiPermission::GetRfdsAll)
|| rfd.visibility == Visibility::Public
|| direct_access_rfds.contains(&rfd.rfd_number)
});

let mut rfd_revisions = RfdRevisionStore::list_unique_rfd(
&*self.storage,
RfdRevisionFilter::default().rfd(Some(rfds.iter().map(|rfd| rfd.id).collect())),
Expand Down Expand Up @@ -491,15 +518,16 @@ impl ApiContext {

pub async fn get_rfd(
&self,
caller: &Caller<ApiPermission>,
rfd_number: i32,
sha: Option<String>,
) -> Result<Option<FullRfd>, StoreError> {
let rfds = RfdStore::list(
&*self.storage,
RfdFilter::default().rfd_number(Some(vec![rfd_number])),
&ListPagination::default().limit(1),
)
.await?;
let rfds = self
.list_rfds(
caller,
Some(RfdFilter::default().rfd_number(Some(vec![rfd_number]))),
)
.await?;

if let Some(rfd) = rfds.into_iter().nth(0) {
let latest_revision = RfdRevisionStore::list(
Expand Down Expand Up @@ -1288,12 +1316,15 @@ mod tests {
let ctx = mock_context(storage).await;

let token_with_no_scope = create_token(&ctx, user_id, vec![]).await;
let permissions = ctx.get_caller(&token_with_no_scope).await.unwrap();
let permissions = ctx.get_caller(Some(&token_with_no_scope)).await.unwrap();
assert_eq!(ApiPermissions::new(), permissions.permissions);

let token_with_rfd_read_scope =
create_token(&ctx, user_id, vec!["rfd:content:r".to_string()]).await;
let permissions = ctx.get_caller(&token_with_rfd_read_scope).await.unwrap();
let permissions = ctx
.get_caller(Some(&token_with_rfd_read_scope))
.await
.unwrap();
assert_eq!(
ApiPermissions::from(vec![ApiPermission::GetRfd(5), ApiPermission::GetRfd(10)]),
permissions.permissions
Expand Down Expand Up @@ -1563,6 +1594,13 @@ pub(crate) mod test_mocks {
self.job_store.as_ref().unwrap().upsert(new_job).await
}

async fn start(
&self,
id: i32,
) -> Result<Option<rfd_model::Job>, rfd_model::storage::StoreError> {
self.job_store.as_ref().unwrap().start(id).await
}

async fn complete(
&self,
id: i32,
Expand Down
29 changes: 17 additions & 12 deletions rfd-api/src/endpoints/api_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ pub async fn get_self(
rqctx: RequestContext<ApiContext>,
) -> Result<HttpResponseOk<GetApiUserResponse>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await;
let caller = ctx.get_caller(&auth?).await?;
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;
get_api_user_op(ctx, &caller, &caller.id).await
}

Expand All @@ -65,7 +65,7 @@ pub async fn get_api_user(
let auth = ctx.authn_token(&rqctx).await?;
get_api_user_op(
ctx,
&ctx.get_caller(&auth).await?,
&ctx.get_caller(auth.as_ref()).await?,
&path.into_inner().identifier,
)
.await
Expand Down Expand Up @@ -128,7 +128,12 @@ pub async fn create_api_user(
) -> Result<HttpResponseCreated<User>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
create_api_user_op(ctx, &ctx.get_caller(&auth).await?, body.into_inner()).await
create_api_user_op(
ctx,
&ctx.get_caller(auth.as_ref()).await?,
body.into_inner(),
)
.await
}

#[instrument(skip(ctx, caller, body), fields(caller = ?caller.id), err(Debug))]
Expand Down Expand Up @@ -172,7 +177,7 @@ pub async fn update_api_user(
) -> Result<HttpResponseOk<User>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(&auth).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;
update_api_user_op(ctx, &caller, &path.into_inner(), body.into_inner()).await
}

Expand Down Expand Up @@ -215,7 +220,7 @@ pub async fn list_api_user_tokens(
) -> Result<HttpResponseOk<Vec<ApiKeyResponse>>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(&auth).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;
list_api_user_tokens_op(ctx, &caller, &path.into_inner()).await
}

Expand Down Expand Up @@ -281,7 +286,7 @@ pub async fn create_api_user_token(
) -> Result<HttpResponseCreated<InitialApiKeyResponse>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(&auth).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;
create_api_user_token_op(ctx, &caller, &path.into_inner(), body.into_inner()).await
}

Expand Down Expand Up @@ -355,7 +360,7 @@ pub async fn get_api_user_token(
) -> Result<HttpResponseOk<ApiKeyResponse>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(&auth).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;
get_api_user_token_op(ctx, &caller, &path.into_inner()).await
}

Expand Down Expand Up @@ -398,7 +403,7 @@ pub async fn delete_api_user_token(
) -> Result<HttpResponseOk<ApiKeyResponse>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(&auth).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;
delete_api_user_token_op(ctx, &caller, &path.into_inner()).await
}

Expand Down Expand Up @@ -446,7 +451,7 @@ pub async fn add_api_user_to_group(
) -> Result<HttpResponseOk<ApiUser<ApiPermission>>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(&auth).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;
let path = path.into_inner();
let body = body.into_inner();

Expand Down Expand Up @@ -479,7 +484,7 @@ pub async fn remove_api_user_from_group(
) -> Result<HttpResponseOk<ApiUser<ApiPermission>>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(&auth).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;
let path = path.into_inner();

if caller.can(&ApiPermission::RemoveFromGroup(path.group_id)) {
Expand Down Expand Up @@ -514,7 +519,7 @@ pub async fn link_provider(
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(&auth).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;
let path = path.into_inner();
let body = body.into_inner();

Expand Down
2 changes: 1 addition & 1 deletion rfd-api/src/endpoints/api_user_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub async fn create_link_token(
) -> Result<HttpResponseOk<ApiUserLinkRequestResponse>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(&auth).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;
let path = path.into_inner();
let body = body.into_inner();

Expand Down
8 changes: 4 additions & 4 deletions rfd-api/src/endpoints/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub async fn get_groups(
) -> Result<HttpResponseOk<Vec<AccessGroup<ApiPermission>>>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(&auth).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;

if caller.can(&ApiPermission::ListGroups) {
Ok(HttpResponseOk(
Expand Down Expand Up @@ -51,7 +51,7 @@ pub async fn create_group(
) -> Result<HttpResponseOk<AccessGroup<ApiPermission>>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(&auth).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;

if caller.can(&ApiPermission::CreateGroup) {
let body = body.into_inner();
Expand Down Expand Up @@ -87,7 +87,7 @@ pub async fn update_group(
) -> Result<HttpResponseOk<AccessGroup<ApiPermission>>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(&auth).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;
let path = path.into_inner();

if caller.can(&ApiPermission::UpdateGroup(path.group_id)) {
Expand Down Expand Up @@ -118,7 +118,7 @@ pub async fn delete_group(
) -> Result<HttpResponseOk<Option<AccessGroup<ApiPermission>>>, HttpError> {
let ctx = rqctx.context();
let auth = ctx.authn_token(&rqctx).await?;
let caller = ctx.get_caller(&auth).await?;
let caller = ctx.get_caller(auth.as_ref()).await?;
let path = path.into_inner();

if caller.can(&ApiPermission::DeleteGroup(path.group_id)) {
Expand Down
Loading

0 comments on commit c6b743f

Please sign in to comment.