server: extract access_control to a separate crate

This commit is contained in:
Valentin Tolmer
2025-04-04 21:44:21 -05:00
committed by nitnelave
parent 0eef966c3e
commit dbba4c4e26
18 changed files with 97 additions and 73 deletions

View File

@@ -79,6 +79,9 @@ features = ["builder", "serde", "smtp-transport", "tokio1-rustls-tls"]
default-features = false
version = "0.10.1"
[dependencies.lldap_access_control]
path = "../crates/access-control"
[dependencies.lldap_auth]
path = "../crates/auth"
features = ["opaque_server", "opaque_client", "sea_orm"]

View File

@@ -1,8 +1,6 @@
use crate::{
domain::sql_backend_handler::SqlBackendHandler,
infra::access_control::UserReadableBackendHandler,
};
use async_trait::async_trait;
use crate::domain::sql_backend_handler::SqlBackendHandler;
use lldap_access_control::UserReadableBackendHandler;
use lldap_domain::{
requests::{CreateGroupRequest, UpdateGroupRequest},
types::{AttributeName, Group, GroupDetails, GroupId, Serialized, Uuid},

View File

@@ -1,338 +0,0 @@
use std::collections::HashSet;
use async_trait::async_trait;
use lldap_auth::access_control::{Permission, ValidationResults};
use lldap_domain_handlers::handler::{
BackendHandler, GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter,
ReadSchemaBackendHandler, SchemaBackendHandler, UserBackendHandler, UserListerBackendHandler,
UserRequestFilter,
};
use tracing::info;
use lldap_domain::{
public_schema::PublicSchema,
requests::{
CreateAttributeRequest, CreateGroupRequest, CreateUserRequest, UpdateGroupRequest,
UpdateUserRequest,
},
schema::{AttributeSchema, Schema},
types::{
AttributeName, Group, GroupDetails, GroupId, GroupName, LdapObjectClass, User,
UserAndGroups, UserId,
},
};
use lldap_domain_model::error::Result;
#[async_trait]
pub trait UserReadableBackendHandler: ReadSchemaBackendHandler {
async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>>;
async fn get_schema(&self) -> Result<PublicSchema>;
}
#[async_trait]
pub trait ReadonlyBackendHandler: UserReadableBackendHandler {
async fn list_users(
&self,
filters: Option<UserRequestFilter>,
get_groups: bool,
) -> Result<Vec<UserAndGroups>>;
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>;
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>;
}
#[async_trait]
pub trait UserWriteableBackendHandler: UserReadableBackendHandler {
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
}
#[async_trait]
pub trait AdminBackendHandler:
UserWriteableBackendHandler
+ ReadonlyBackendHandler
+ UserWriteableBackendHandler
+ SchemaBackendHandler
{
async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
async fn delete_user(&self, user_id: &UserId) -> Result<()>;
async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>;
async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>;
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;
async fn create_group(&self, request: CreateGroupRequest) -> Result<GroupId>;
async fn delete_group(&self, group_id: GroupId) -> Result<()>;
async fn add_user_attribute(&self, request: CreateAttributeRequest) -> Result<()>;
async fn add_group_attribute(&self, request: CreateAttributeRequest) -> Result<()>;
async fn delete_user_attribute(&self, name: &AttributeName) -> Result<()>;
async fn delete_group_attribute(&self, name: &AttributeName) -> Result<()>;
async fn add_user_object_class(&self, name: &LdapObjectClass) -> Result<()>;
async fn add_group_object_class(&self, name: &LdapObjectClass) -> Result<()>;
async fn delete_user_object_class(&self, name: &LdapObjectClass) -> Result<()>;
async fn delete_group_object_class(&self, name: &LdapObjectClass) -> Result<()>;
}
#[async_trait]
impl<Handler: BackendHandler> UserReadableBackendHandler for Handler {
async fn get_user_details(&self, user_id: &UserId) -> Result<User> {
<Handler as UserBackendHandler>::get_user_details(self, user_id).await
}
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>> {
<Handler as UserBackendHandler>::get_user_groups(self, user_id).await
}
async fn get_schema(&self) -> Result<PublicSchema> {
Ok(PublicSchema::from(
<Handler as ReadSchemaBackendHandler>::get_schema(self).await?,
))
}
}
#[async_trait]
impl<Handler: BackendHandler> ReadonlyBackendHandler for Handler {
async fn list_users(
&self,
filters: Option<UserRequestFilter>,
get_groups: bool,
) -> Result<Vec<UserAndGroups>> {
<Handler as UserListerBackendHandler>::list_users(self, filters, get_groups).await
}
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>> {
<Handler as GroupListerBackendHandler>::list_groups(self, filters).await
}
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails> {
<Handler as GroupBackendHandler>::get_group_details(self, group_id).await
}
}
#[async_trait]
impl<Handler: BackendHandler> UserWriteableBackendHandler for Handler {
async fn update_user(&self, request: UpdateUserRequest) -> Result<()> {
<Handler as UserBackendHandler>::update_user(self, request).await
}
}
#[async_trait]
impl<Handler: BackendHandler> AdminBackendHandler for Handler {
async fn create_user(&self, request: CreateUserRequest) -> Result<()> {
<Handler as UserBackendHandler>::create_user(self, request).await
}
async fn delete_user(&self, user_id: &UserId) -> Result<()> {
<Handler as UserBackendHandler>::delete_user(self, user_id).await
}
async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> {
<Handler as UserBackendHandler>::add_user_to_group(self, user_id, group_id).await
}
async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> {
<Handler as UserBackendHandler>::remove_user_from_group(self, user_id, group_id).await
}
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()> {
<Handler as GroupBackendHandler>::update_group(self, request).await
}
async fn create_group(&self, request: CreateGroupRequest) -> Result<GroupId> {
<Handler as GroupBackendHandler>::create_group(self, request).await
}
async fn delete_group(&self, group_id: GroupId) -> Result<()> {
<Handler as GroupBackendHandler>::delete_group(self, group_id).await
}
async fn add_user_attribute(&self, request: CreateAttributeRequest) -> Result<()> {
<Handler as SchemaBackendHandler>::add_user_attribute(self, request).await
}
async fn add_group_attribute(&self, request: CreateAttributeRequest) -> Result<()> {
<Handler as SchemaBackendHandler>::add_group_attribute(self, request).await
}
async fn delete_user_attribute(&self, name: &AttributeName) -> Result<()> {
<Handler as SchemaBackendHandler>::delete_user_attribute(self, name).await
}
async fn delete_group_attribute(&self, name: &AttributeName) -> Result<()> {
<Handler as SchemaBackendHandler>::delete_group_attribute(self, name).await
}
async fn add_user_object_class(&self, name: &LdapObjectClass) -> Result<()> {
<Handler as SchemaBackendHandler>::add_user_object_class(self, name).await
}
async fn add_group_object_class(&self, name: &LdapObjectClass) -> Result<()> {
<Handler as SchemaBackendHandler>::add_group_object_class(self, name).await
}
async fn delete_user_object_class(&self, name: &LdapObjectClass) -> Result<()> {
<Handler as SchemaBackendHandler>::delete_user_object_class(self, name).await
}
async fn delete_group_object_class(&self, name: &LdapObjectClass) -> Result<()> {
<Handler as SchemaBackendHandler>::delete_group_object_class(self, name).await
}
}
pub struct AccessControlledBackendHandler<Handler> {
handler: Handler,
}
impl<Handler: Clone> Clone for AccessControlledBackendHandler<Handler> {
fn clone(&self) -> Self {
Self {
handler: self.handler.clone(),
}
}
}
impl<Handler> AccessControlledBackendHandler<Handler> {
pub fn unsafe_get_handler(&self) -> &Handler {
&self.handler
}
}
impl<Handler: BackendHandler> AccessControlledBackendHandler<Handler> {
pub fn new(handler: Handler) -> Self {
Self { handler }
}
pub fn get_admin_handler(
&self,
validation_result: &ValidationResults,
) -> Option<&(impl AdminBackendHandler + use<Handler>)> {
validation_result.is_admin().then_some(&self.handler)
}
pub fn get_readonly_handler(
&self,
validation_result: &ValidationResults,
) -> Option<&(impl ReadonlyBackendHandler + use<Handler>)> {
validation_result.can_read_all().then_some(&self.handler)
}
pub fn get_writeable_handler(
&self,
validation_result: &ValidationResults,
user_id: &UserId,
) -> Option<&(impl UserWriteableBackendHandler + use<Handler>)> {
validation_result
.can_write(user_id)
.then_some(&self.handler)
}
pub fn get_readable_handler(
&self,
validation_result: &ValidationResults,
user_id: &UserId,
) -> Option<&(impl UserReadableBackendHandler + use<Handler>)> {
validation_result.can_read(user_id).then_some(&self.handler)
}
pub fn get_user_restricted_lister_handler(
&self,
validation_result: &ValidationResults,
) -> UserRestrictedListerBackendHandler<'_, Handler> {
UserRestrictedListerBackendHandler {
handler: &self.handler,
user_filter: if validation_result.can_read_all() {
None
} else {
info!("Unprivileged search, limiting results");
Some(validation_result.user.clone())
},
}
}
pub async fn get_permissions_for_user(&self, user_id: UserId) -> Result<ValidationResults> {
let user_groups = self.handler.get_user_groups(&user_id).await?;
Ok(self.get_permissions_from_groups(user_id, user_groups.iter().map(|g| &g.display_name)))
}
pub fn get_permissions_from_groups<Groups, T>(
&self,
user_id: UserId,
groups: Groups,
) -> ValidationResults
where
Groups: Iterator<Item = T> + Clone,
T: AsRef<GroupName>,
{
let is_in_group = |name: GroupName| groups.clone().any(|g| *g.as_ref() == name);
ValidationResults {
user: user_id,
permission: if is_in_group("lldap_admin".into()) {
Permission::Admin
} else if is_in_group("lldap_password_manager".into()) {
Permission::PasswordManager
} else if is_in_group("lldap_strict_readonly".into()) {
Permission::Readonly
} else {
Permission::Regular
},
}
}
}
pub struct UserRestrictedListerBackendHandler<'a, Handler> {
handler: &'a Handler,
user_filter: Option<UserId>,
}
#[async_trait]
impl<Handler: ReadSchemaBackendHandler + Sync> ReadSchemaBackendHandler
for UserRestrictedListerBackendHandler<'_, Handler>
{
async fn get_schema(&self) -> Result<Schema> {
let mut schema = self.handler.get_schema().await?;
if self.user_filter.is_some() {
let filter_attributes = |attributes: &mut Vec<AttributeSchema>| {
attributes.retain(|a| a.is_visible);
};
filter_attributes(&mut schema.user_attributes.attributes);
filter_attributes(&mut schema.group_attributes.attributes);
}
Ok(schema)
}
}
#[async_trait]
impl<Handler: UserListerBackendHandler + Sync> UserListerBackendHandler
for UserRestrictedListerBackendHandler<'_, Handler>
{
async fn list_users(
&self,
filters: Option<UserRequestFilter>,
get_groups: bool,
) -> Result<Vec<UserAndGroups>> {
let user_filter = self
.user_filter
.as_ref()
.map(|u| UserRequestFilter::UserId(u.clone()));
let filters = match (filters, user_filter) {
(None, None) => None,
(None, u) => u,
(f, None) => f,
(Some(f), Some(u)) => Some(UserRequestFilter::And(vec![f, u])),
};
self.handler.list_users(filters, get_groups).await
}
}
#[async_trait]
impl<Handler: GroupListerBackendHandler + Sync> GroupListerBackendHandler
for UserRestrictedListerBackendHandler<'_, Handler>
{
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>> {
let group_filter = self
.user_filter
.as_ref()
.map(|u| GroupRequestFilter::Member(u.clone()));
let filters = match (filters, group_filter) {
(None, None) => None,
(None, u) => u,
(f, None) => f,
(Some(f), Some(u)) => Some(GroupRequestFilter::And(vec![f, u])),
};
self.handler.list_groups(filters).await
}
}
#[async_trait]
pub trait UserAndGroupListerBackendHandler:
UserListerBackendHandler + GroupListerBackendHandler
{
fn user_filter(&self) -> &Option<UserId>;
}
#[async_trait]
impl<Handler: GroupListerBackendHandler + UserListerBackendHandler + Sync>
UserAndGroupListerBackendHandler for UserRestrictedListerBackendHandler<'_, Handler>
{
fn user_filter(&self) -> &Option<UserId> {
&self.user_filter
}
}

View File

@@ -1,3 +1,10 @@
use crate::{
domain::opaque_handler::OpaqueHandler,
infra::{
tcp_backend_handler::*,
tcp_server::{AppState, TcpError, TcpResult, error_to_http_response},
},
};
use actix_web::{
HttpRequest, HttpResponse,
cookie::{Cookie, SameSite},
@@ -12,6 +19,15 @@ use futures::future::{Ready, ok};
use futures_util::FutureExt;
use hmac::Hmac;
use jwt::{SignWithKey, VerifyWithKey};
use lldap_access_control::{ReadonlyBackendHandler, UserReadableBackendHandler};
use lldap_auth::{
JWTClaims, access_control::ValidationResults, login, password_reset, registration,
};
use lldap_domain::types::{GroupDetails, GroupName, UserId};
use lldap_domain_handlers::handler::{
BackendHandler, BindRequest, LoginHandler, UserRequestFilter,
};
use lldap_domain_model::{error::DomainError, model::UserColumn};
use sha2::Sha512;
use std::{
collections::HashSet,
@@ -22,24 +38,6 @@ use std::{
use time::ext::NumericalDuration;
use tracing::{debug, info, instrument, warn};
use lldap_auth::{
JWTClaims, access_control::ValidationResults, login, password_reset, registration,
};
use lldap_domain::types::{GroupDetails, GroupName, UserId};
use lldap_domain_handlers::handler::{
BackendHandler, BindRequest, LoginHandler, UserRequestFilter,
};
use lldap_domain_model::{error::DomainError, model::UserColumn};
use crate::{
domain::opaque_handler::OpaqueHandler,
infra::{
access_control::{ReadonlyBackendHandler, UserReadableBackendHandler},
tcp_backend_handler::*,
tcp_server::{AppState, TcpError, TcpResult, error_to_http_response},
},
};
type Token<S> = jwt::Token<jwt::Header, JWTClaims, S>;
type SignedToken = Token<jwt::token::Signed>;

View File

@@ -1,14 +1,9 @@
use crate::infra::{
access_control::{
AccessControlledBackendHandler, AdminBackendHandler, ReadonlyBackendHandler,
UserReadableBackendHandler, UserWriteableBackendHandler,
},
auth_service::check_if_token_is_valid,
cli::ExportGraphQLSchemaOpts,
graphql::{mutation::Mutation, query::Query},
tcp_server::AppState,
};
use actix_web::FromRequest;
use actix_web::HttpMessage;
use actix_web::{Error, HttpRequest, HttpResponse, error::JsonPayloadError, web};
@@ -20,6 +15,10 @@ use juniper::{
playground::playground_source,
},
};
use lldap_access_control::{
AccessControlledBackendHandler, AdminBackendHandler, ReadonlyBackendHandler,
UserReadableBackendHandler, UserWriteableBackendHandler,
};
use lldap_auth::{access_control::ValidationResults, types::UserId};
use lldap_domain_handlers::handler::BackendHandler;
use tracing::debug;

View File

@@ -1,17 +1,13 @@
use std::{collections::BTreeMap, sync::Arc};
use crate::{
domain::deserialize::deserialize_attribute_value,
infra::{
access_control::{
AdminBackendHandler, ReadonlyBackendHandler, UserReadableBackendHandler,
UserWriteableBackendHandler,
},
graphql::api::{Context, field_error_callback},
},
infra::graphql::api::{Context, field_error_callback},
};
use anyhow::{Context as AnyhowContext, anyhow};
use juniper::{FieldError, FieldResult, GraphQLInputObject, GraphQLObject, graphql_object};
use lldap_access_control::{
AdminBackendHandler, ReadonlyBackendHandler, UserReadableBackendHandler,
UserWriteableBackendHandler,
};
use lldap_domain::{
public_schema::PublicSchema,
requests::{
@@ -26,6 +22,7 @@ use lldap_domain::{
};
use lldap_domain_handlers::handler::BackendHandler;
use lldap_validation::attributes::{ALLOWED_CHARACTERS_DESCRIPTION, validate_attribute_name};
use std::{collections::BTreeMap, sync::Arc};
use tracing::{Instrument, Span, debug, debug_span};
#[derive(PartialEq, Eq, Debug)]

View File

@@ -5,14 +5,12 @@ use crate::{
deserialize::deserialize_attribute_value,
ldap::utils::{UserFieldType, map_user_field},
},
infra::{
access_control::{ReadonlyBackendHandler, UserReadableBackendHandler},
graphql::api::{Context, field_error_callback},
},
infra::graphql::api::{Context, field_error_callback},
};
use anyhow::Context as AnyhowContext;
use chrono::TimeZone;
use juniper::{FieldResult, GraphQLInputObject, graphql_object};
use lldap_access_control::{ReadonlyBackendHandler, UserReadableBackendHandler};
use lldap_domain::{
public_schema::PublicSchema,
types::{AttributeType, Cardinality, GroupDetails, GroupId, LdapObjectClass, UserId},

View File

@@ -6,11 +6,12 @@ use crate::{
utils::{LdapInfo, UserOrGroupName, get_user_or_group_id_from_distinguished_name},
},
},
infra::{access_control::AdminBackendHandler, ldap::handler::make_add_response},
infra::ldap::handler::make_add_response,
};
use ldap3_proto::proto::{
LdapAddRequest, LdapAttribute, LdapOp, LdapPartialAttribute, LdapResultCode,
};
use lldap_access_control::AdminBackendHandler;
use lldap_domain::{
requests::{CreateGroupRequest, CreateUserRequest},
types::{Attribute, AttributeName, AttributeType, Email, GroupName, UserId},

View File

@@ -3,8 +3,8 @@ use crate::{
error::{LdapError, LdapResult},
utils::{LdapInfo, UserOrGroupName, get_user_or_group_id_from_distinguished_name},
},
infra::access_control::AdminBackendHandler,
};
use lldap_access_control::AdminBackendHandler;
use ldap3_proto::proto::{LdapOp, LdapResult as LdapResultOp, LdapResultCode};
use lldap_domain::types::{GroupName, UserId};
use lldap_domain_handlers::handler::GroupRequestFilter;

View File

@@ -6,15 +6,12 @@ use crate::{
},
opaque_handler::OpaqueHandler,
},
infra::{
access_control::AccessControlledBackendHandler,
ldap::{
compare, create, delete, modify,
password::{self, do_password_modification},
search::{
self, is_root_dse_request, make_search_error, make_search_request,
make_search_success, root_dse_response,
},
infra::ldap::{
compare, create, delete, modify,
password::{self, do_password_modification},
search::{
self, is_root_dse_request, make_search_error, make_search_request, make_search_success,
root_dse_response,
},
},
};
@@ -23,6 +20,7 @@ use ldap3_proto::proto::{
LdapExtendedResponse, LdapFilter, LdapModifyRequest, LdapOp, LdapPasswordModifyRequest,
LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, OID_PASSWORD_MODIFY, OID_WHOAMI,
};
use lldap_access_control::AccessControlledBackendHandler;
use lldap_auth::access_control::ValidationResults;
use lldap_domain::types::AttributeName;
use lldap_domain_handlers::handler::{BackendHandler, LoginHandler};

View File

@@ -6,15 +6,13 @@ use crate::{
},
opaque_handler::OpaqueHandler,
},
infra::{
access_control::UserReadableBackendHandler,
ldap::{
handler::make_modify_response,
password::{self},
},
infra::ldap::{
handler::make_modify_response,
password::{self},
},
};
use ldap3_proto::proto::{LdapModify, LdapModifyRequest, LdapModifyType, LdapOp, LdapResultCode};
use lldap_access_control::UserReadableBackendHandler;
use lldap_auth::access_control::ValidationResults;
use lldap_domain::types::UserId;

View File

@@ -7,10 +7,10 @@ use crate::{
opaque_handler::OpaqueHandler,
},
infra::{
access_control::{AccessControlledBackendHandler, UserReadableBackendHandler},
ldap::handler::make_extended_response,
},
};
use lldap_access_control::{AccessControlledBackendHandler, UserReadableBackendHandler};
use anyhow::Result;
use ldap3_proto::proto::{
LdapBindCred, LdapBindRequest, LdapOp, LdapPasswordModifyRequest, LdapResultCode,

View File

@@ -1,3 +1,9 @@
use crate::domain::ldap::{
error::{LdapError, LdapResult},
group::{convert_groups_to_ldap_op, get_groups_list},
user::{convert_users_to_ldap_op, get_user_list},
utils::{LdapInfo, is_subtree, parse_distinguished_name},
};
use ldap3_proto::{
LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, LdapSearchScope,
proto::{
@@ -5,22 +11,13 @@ use ldap3_proto::{
OID_PASSWORD_MODIFY, OID_WHOAMI,
},
};
use lldap_access_control::UserAndGroupListerBackendHandler;
use lldap_domain::{
public_schema::PublicSchema,
types::{Group, UserAndGroups},
};
use tracing::{debug, instrument, warn};
use crate::{
domain::ldap::{
error::{LdapError, LdapResult},
group::{convert_groups_to_ldap_op, get_groups_list},
user::{convert_users_to_ldap_op, get_user_list},
utils::{LdapInfo, is_subtree, parse_distinguished_name},
},
infra::access_control::UserAndGroupListerBackendHandler,
};
#[derive(Debug)]
enum SearchScope {
Global,

View File

@@ -1,11 +1,11 @@
use crate::{
domain::opaque_handler::OpaqueHandler,
infra::{
access_control::AccessControlledBackendHandler,
configuration::{Configuration, LdapsOptions},
ldap::handler::LdapHandler,
},
};
use lldap_access_control::AccessControlledBackendHandler;
use actix_rt::net::TcpStream;
use actix_server::ServerBuilder;
use actix_service::{ServiceFactoryExt, fn_service};

View File

@@ -1,4 +1,3 @@
pub mod access_control;
pub mod auth_service;
pub mod cli;
pub mod configuration;

View File

@@ -1,13 +1,13 @@
use crate::{
domain::opaque_handler::OpaqueHandler,
infra::{
access_control::{AccessControlledBackendHandler, ReadonlyBackendHandler},
auth_service,
configuration::{Configuration, MailOptions},
logging::CustomRootSpanBuilder,
tcp_backend_handler::*,
},
};
use lldap_access_control::{AccessControlledBackendHandler, ReadonlyBackendHandler};
use actix_files::Files;
use actix_http::{HttpServiceBuilder, header};
use actix_server::ServerBuilder;