server: statically enforce access control

This commit is contained in:
Valentin Tolmer
2023-02-17 15:59:32 +01:00
committed by nitnelave
parent 322bf26db5
commit c9997d4c17
18 changed files with 712 additions and 359 deletions

View File

@@ -1,29 +1,77 @@
use crate::{
domain::handler::BackendHandler,
domain::{handler::BackendHandler, types::UserId},
infra::{
auth_service::{check_if_token_is_valid, ValidationResults},
access_control::{
AccessControlledBackendHandler, AdminBackendHandler, ReadonlyBackendHandler,
UserReadableBackendHandler, UserWriteableBackendHandler, ValidationResults,
},
auth_service::check_if_token_is_valid,
cli::ExportGraphQLSchemaOpts,
graphql::{mutation::Mutation, query::Query},
tcp_server::AppState,
},
};
use actix_web::{web, Error, HttpResponse};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use juniper::{EmptySubscription, RootNode};
use juniper::{EmptySubscription, FieldError, RootNode};
use juniper_actix::{graphiql_handler, graphql_handler, playground_handler};
use super::{mutation::Mutation, query::Query};
use tracing::debug;
pub struct Context<Handler: BackendHandler> {
pub handler: Box<Handler>,
pub handler: AccessControlledBackendHandler<Handler>,
pub validation_result: ValidationResults,
}
pub fn field_error_callback<'a>(
span: &'a tracing::Span,
error_message: &'a str,
) -> impl 'a + FnOnce() -> FieldError {
move || {
span.in_scope(|| debug!("Unauthorized"));
FieldError::from(error_message)
}
}
impl<Handler: BackendHandler> Context<Handler> {
#[cfg(test)]
pub fn new_for_tests(handler: Handler, validation_result: ValidationResults) -> Self {
Self {
handler: AccessControlledBackendHandler::new(handler),
validation_result,
}
}
pub fn get_admin_handler(&self) -> Option<&impl AdminBackendHandler> {
self.handler.get_admin_handler(&self.validation_result)
}
pub fn get_readonly_handler(&self) -> Option<&impl ReadonlyBackendHandler> {
self.handler.get_readonly_handler(&self.validation_result)
}
pub fn get_writeable_handler(
&self,
user_id: &UserId,
) -> Option<&impl UserWriteableBackendHandler> {
self.handler
.get_writeable_handler(&self.validation_result, user_id)
}
pub fn get_readable_handler(
&self,
user_id: &UserId,
) -> Option<&impl UserReadableBackendHandler> {
self.handler
.get_readable_handler(&self.validation_result, user_id)
}
}
impl<Handler: BackendHandler> juniper::Context for Context<Handler> {}
type Schema<Handler> =
RootNode<'static, Query<Handler>, Mutation<Handler>, EmptySubscription<Context<Handler>>>;
fn schema<Handler: BackendHandler + Sync>() -> Schema<Handler> {
fn schema<Handler: BackendHandler>() -> Schema<Handler> {
Schema::new(
Query::<Handler>::new(),
Mutation::<Handler>::new(),
@@ -58,7 +106,7 @@ async fn playground_route() -> Result<HttpResponse, Error> {
playground_handler("/api/graphql", None).await
}
async fn graphql_route<Handler: BackendHandler + Sync>(
async fn graphql_route<Handler: BackendHandler + Clone>(
req: actix_web::HttpRequest,
mut payload: actix_web::web::Payload,
data: web::Data<AppState<Handler>>,
@@ -67,7 +115,7 @@ async fn graphql_route<Handler: BackendHandler + Sync>(
let bearer = BearerAuth::from_request(&req, &mut payload.0).await?;
let validation_result = check_if_token_is_valid(&data, bearer.token())?;
let context = Context::<Handler> {
handler: Box::new(data.backend_handler.clone()),
handler: data.backend_handler.clone(),
validation_result,
};
graphql_handler(&schema(), &context, req, payload).await
@@ -75,7 +123,7 @@ async fn graphql_route<Handler: BackendHandler + Sync>(
pub fn configure_endpoint<Backend>(cfg: &mut web::ServiceConfig)
where
Backend: BackendHandler + Sync + 'static,
Backend: BackendHandler + Clone + 'static,
{
let json_config = web::JsonConfig::default()
.limit(4096)

View File

@@ -1,6 +1,15 @@
use crate::domain::{
handler::{BackendHandler, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest},
types::{GroupId, JpegPhoto, UserId},
use crate::{
domain::{
handler::{BackendHandler, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest},
types::{GroupId, JpegPhoto, UserId},
},
infra::{
access_control::{
AdminBackendHandler, ReadonlyBackendHandler, UserReadableBackendHandler,
UserWriteableBackendHandler,
},
graphql::api::field_error_callback,
},
};
use anyhow::Context as AnyhowContext;
use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject};
@@ -65,19 +74,18 @@ impl Success {
}
#[graphql_object(context = Context<Handler>)]
impl<Handler: BackendHandler + Sync> Mutation<Handler> {
impl<Handler: BackendHandler> Mutation<Handler> {
async fn create_user(
context: &Context<Handler>,
user: CreateUserInput,
) -> FieldResult<super::query::User<Handler>> {
let span = debug_span!("[GraphQL mutation] create_user");
span.in_scope(|| {
debug!(?user.id);
debug!("{:?}", &user.id);
});
if !context.validation_result.is_admin() {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized user creation".into());
}
let handler = context
.get_admin_handler()
.ok_or_else(field_error_callback(&span, "Unauthorized user creation"))?;
let user_id = UserId::new(&user.id);
let avatar = user
.avatar
@@ -87,8 +95,7 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
.map(JpegPhoto::try_from)
.transpose()
.context("Provided image is not a valid JPEG")?;
context
.handler
handler
.create_user(CreateUserRequest {
user_id: user_id.clone(),
email: user.email,
@@ -99,8 +106,7 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
})
.instrument(span.clone())
.await?;
Ok(context
.handler
Ok(handler
.get_user_details(&user_id)
.instrument(span)
.await
@@ -115,13 +121,11 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
span.in_scope(|| {
debug!(?name);
});
if !context.validation_result.is_admin() {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized group creation".into());
}
let group_id = context.handler.create_group(&name).await?;
Ok(context
.handler
let handler = context
.get_admin_handler()
.ok_or_else(field_error_callback(&span, "Unauthorized group creation"))?;
let group_id = handler.create_group(&name).await?;
Ok(handler
.get_group_details(group_id)
.instrument(span)
.await
@@ -137,10 +141,9 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
debug!(?user.id);
});
let user_id = UserId::new(&user.id);
if !context.validation_result.can_write(&user_id) {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized user update".into());
}
let handler = context
.get_writeable_handler(&user_id)
.ok_or_else(field_error_callback(&span, "Unauthorized user update"))?;
let avatar = user
.avatar
.map(base64::decode)
@@ -149,8 +152,7 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
.map(JpegPhoto::try_from)
.transpose()
.context("Provided image is not a valid JPEG")?;
context
.handler
handler
.update_user(UpdateUserRequest {
user_id,
email: user.email,
@@ -172,16 +174,14 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
span.in_scope(|| {
debug!(?group.id);
});
if !context.validation_result.is_admin() {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized group update".into());
}
let handler = context
.get_admin_handler()
.ok_or_else(field_error_callback(&span, "Unauthorized group update"))?;
if group.id == 1 {
span.in_scope(|| debug!("Cannot change admin group details"));
return Err("Cannot change admin group details".into());
}
context
.handler
handler
.update_group(UpdateGroupRequest {
group_id: GroupId(group.id),
display_name: group.display_name,
@@ -200,12 +200,13 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
span.in_scope(|| {
debug!(?user_id, ?group_id);
});
if !context.validation_result.is_admin() {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized group membership modification".into());
}
context
.handler
let handler = context
.get_admin_handler()
.ok_or_else(field_error_callback(
&span,
"Unauthorized group membership modification",
))?;
handler
.add_user_to_group(&UserId::new(&user_id), GroupId(group_id))
.instrument(span)
.await?;
@@ -221,17 +222,18 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
span.in_scope(|| {
debug!(?user_id, ?group_id);
});
if !context.validation_result.is_admin() {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized group membership modification".into());
}
let handler = context
.get_admin_handler()
.ok_or_else(field_error_callback(
&span,
"Unauthorized group membership modification",
))?;
let user_id = UserId::new(&user_id);
if context.validation_result.user == user_id && group_id == 1 {
span.in_scope(|| debug!("Cannot remove admin rights for current user"));
return Err("Cannot remove admin rights for current user".into());
}
context
.handler
handler
.remove_user_from_group(&user_id, GroupId(group_id))
.instrument(span)
.await?;
@@ -244,19 +246,14 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
debug!(?user_id);
});
let user_id = UserId::new(&user_id);
if !context.validation_result.is_admin() {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized user deletion".into());
}
let handler = context
.get_admin_handler()
.ok_or_else(field_error_callback(&span, "Unauthorized user deletion"))?;
if context.validation_result.user == user_id {
span.in_scope(|| debug!("Cannot delete current user"));
return Err("Cannot delete current user".into());
}
context
.handler
.delete_user(&user_id)
.instrument(span)
.await?;
handler.delete_user(&user_id).instrument(span).await?;
Ok(Success::new())
}
@@ -265,16 +262,14 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
span.in_scope(|| {
debug!(?group_id);
});
if !context.validation_result.is_admin() {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized group deletion".into());
}
let handler = context
.get_admin_handler()
.ok_or_else(field_error_callback(&span, "Unauthorized group deletion"))?;
if group_id == 1 {
span.in_scope(|| debug!("Cannot delete admin group"));
return Err("Cannot delete admin group".into());
}
context
.handler
handler
.delete_group(GroupId(group_id))
.instrument(span)
.await?;

View File

@@ -1,7 +1,13 @@
use crate::domain::{
handler::BackendHandler,
ldap::utils::map_user_field,
types::{GroupDetails, GroupId, UserColumn, UserId},
use crate::{
domain::{
handler::BackendHandler,
ldap::utils::map_user_field,
types::{GroupDetails, GroupId, UserColumn, UserId},
},
infra::{
access_control::{ReadonlyBackendHandler, UserReadableBackendHandler},
graphql::api::field_error_callback,
},
};
use chrono::TimeZone;
use juniper::{graphql_object, FieldResult, GraphQLInputObject};
@@ -112,7 +118,7 @@ impl<Handler: BackendHandler> Query<Handler> {
}
#[graphql_object(context = Context<Handler>)]
impl<Handler: BackendHandler + Sync> Query<Handler> {
impl<Handler: BackendHandler> Query<Handler> {
fn api_version() -> &'static str {
"1.0"
}
@@ -123,12 +129,13 @@ impl<Handler: BackendHandler + Sync> Query<Handler> {
debug!(?user_id);
});
let user_id = UserId::new(&user_id);
if !context.validation_result.can_read(&user_id) {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized access to user data".into());
}
Ok(context
.handler
let handler = context
.get_readable_handler(&user_id)
.ok_or_else(field_error_callback(
&span,
"Unauthorized access to user data",
))?;
Ok(handler
.get_user_details(&user_id)
.instrument(span)
.await
@@ -143,12 +150,13 @@ impl<Handler: BackendHandler + Sync> Query<Handler> {
span.in_scope(|| {
debug!(?filters);
});
if !context.validation_result.is_admin_or_readonly() {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized access to user list".into());
}
Ok(context
.handler
let handler = context
.get_readonly_handler()
.ok_or_else(field_error_callback(
&span,
"Unauthorized access to user list",
))?;
Ok(handler
.list_users(filters.map(TryInto::try_into).transpose()?, false)
.instrument(span)
.await
@@ -157,12 +165,13 @@ impl<Handler: BackendHandler + Sync> Query<Handler> {
async fn groups(context: &Context<Handler>) -> FieldResult<Vec<Group<Handler>>> {
let span = debug_span!("[GraphQL query] groups");
if !context.validation_result.is_admin_or_readonly() {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized access to group list".into());
}
Ok(context
.handler
let handler = context
.get_readonly_handler()
.ok_or_else(field_error_callback(
&span,
"Unauthorized access to group list",
))?;
Ok(handler
.list_groups(None)
.instrument(span)
.await
@@ -174,12 +183,13 @@ impl<Handler: BackendHandler + Sync> Query<Handler> {
span.in_scope(|| {
debug!(?group_id);
});
if !context.validation_result.is_admin_or_readonly() {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized access to group data".into());
}
Ok(context
.handler
let handler = context
.get_readonly_handler()
.ok_or_else(field_error_callback(
&span,
"Unauthorized access to group data",
))?;
Ok(handler
.get_group_details(GroupId(group_id))
.instrument(span)
.await
@@ -205,7 +215,7 @@ impl<Handler: BackendHandler> Default for User<Handler> {
}
#[graphql_object(context = Context<Handler>)]
impl<Handler: BackendHandler + Sync> User<Handler> {
impl<Handler: BackendHandler> User<Handler> {
fn id(&self) -> &str {
self.user.user_id.as_str()
}
@@ -244,8 +254,10 @@ impl<Handler: BackendHandler + Sync> User<Handler> {
span.in_scope(|| {
debug!(user_id = ?self.user.user_id);
});
Ok(context
.handler
let handler = context
.get_readable_handler(&self.user.user_id)
.expect("We shouldn't be able to get there without readable permission");
Ok(handler
.get_user_groups(&self.user.user_id)
.instrument(span)
.await
@@ -283,7 +295,7 @@ pub struct Group<Handler: BackendHandler> {
}
#[graphql_object(context = Context<Handler>)]
impl<Handler: BackendHandler + Sync> Group<Handler> {
impl<Handler: BackendHandler> Group<Handler> {
fn id(&self) -> i32 {
self.group_id
}
@@ -302,12 +314,13 @@ impl<Handler: BackendHandler + Sync> Group<Handler> {
span.in_scope(|| {
debug!(name = %self.display_name);
});
if !context.validation_result.is_admin_or_readonly() {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized access to group data".into());
}
Ok(context
.handler
let handler = context
.get_readonly_handler()
.ok_or_else(field_error_callback(
&span,
"Unauthorized access to group data",
))?;
Ok(handler
.list_users(
Some(DomainRequestFilter::MemberOfId(GroupId(self.group_id))),
false,
@@ -347,7 +360,9 @@ impl<Handler: BackendHandler> From<DomainGroup> for Group<Handler> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{domain::handler::MockTestBackendHandler, infra::auth_service::ValidationResults};
use crate::{
domain::handler::MockTestBackendHandler, infra::access_control::ValidationResults,
};
use chrono::TimeZone;
use juniper::{
execute, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType,
@@ -406,10 +421,8 @@ mod tests {
.with(eq(UserId::new("bob")))
.return_once(|_| Ok(groups));
let context = Context::<MockTestBackendHandler> {
handler: Box::new(mock),
validation_result: ValidationResults::admin(),
};
let context =
Context::<MockTestBackendHandler>::new_for_tests(mock, ValidationResults::admin());
let schema = schema(Query::<MockTestBackendHandler>::new());
assert_eq!(
@@ -486,10 +499,8 @@ mod tests {
])
});
let context = Context::<MockTestBackendHandler> {
handler: Box::new(mock),
validation_result: ValidationResults::admin(),
};
let context =
Context::<MockTestBackendHandler>::new_for_tests(mock, ValidationResults::admin());
let schema = schema(Query::<MockTestBackendHandler>::new());
assert_eq!(