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

@ -122,13 +122,17 @@ pub struct UpdateGroupRequest {
} }
#[async_trait] #[async_trait]
pub trait LoginHandler: Clone + Send { pub trait LoginHandler: Send + Sync {
async fn bind(&self, request: BindRequest) -> Result<()>; async fn bind(&self, request: BindRequest) -> Result<()>;
} }
#[async_trait] #[async_trait]
pub trait GroupBackendHandler { pub trait GroupListerBackendHandler {
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>; async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>;
}
#[async_trait]
pub trait GroupBackendHandler {
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>; async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>;
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;
async fn create_group(&self, group_name: &str) -> Result<GroupId>; async fn create_group(&self, group_name: &str) -> Result<GroupId>;
@ -136,12 +140,16 @@ pub trait GroupBackendHandler {
} }
#[async_trait] #[async_trait]
pub trait UserBackendHandler { pub trait UserListerBackendHandler {
async fn list_users( async fn list_users(
&self, &self,
filters: Option<UserRequestFilter>, filters: Option<UserRequestFilter>,
get_groups: bool, get_groups: bool,
) -> Result<Vec<UserAndGroups>>; ) -> Result<Vec<UserAndGroups>>;
}
#[async_trait]
pub trait UserBackendHandler {
async fn get_user_details(&self, user_id: &UserId) -> Result<User>; async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
@ -152,7 +160,15 @@ pub trait UserBackendHandler {
} }
#[async_trait] #[async_trait]
pub trait BackendHandler: Clone + Send + GroupBackendHandler + UserBackendHandler {} pub trait BackendHandler:
Send
+ Sync
+ GroupBackendHandler
+ UserBackendHandler
+ UserListerBackendHandler
+ GroupListerBackendHandler
{
}
#[cfg(test)] #[cfg(test)]
mockall::mock! { mockall::mock! {
@ -161,16 +177,22 @@ mockall::mock! {
fn clone(&self) -> Self; fn clone(&self) -> Self;
} }
#[async_trait] #[async_trait]
impl GroupBackendHandler for TestBackendHandler { impl GroupListerBackendHandler for TestBackendHandler {
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>; async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>;
}
#[async_trait]
impl GroupBackendHandler for TestBackendHandler {
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>; async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>;
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;
async fn create_group(&self, group_name: &str) -> Result<GroupId>; async fn create_group(&self, group_name: &str) -> Result<GroupId>;
async fn delete_group(&self, group_id: GroupId) -> Result<()>; async fn delete_group(&self, group_id: GroupId) -> Result<()>;
} }
#[async_trait] #[async_trait]
impl UserBackendHandler for TestBackendHandler { impl UserListerBackendHandler for TestBackendHandler {
async fn list_users(&self, filters: Option<UserRequestFilter>, get_groups: bool) -> Result<Vec<UserAndGroups>>; async fn list_users(&self, filters: Option<UserRequestFilter>, get_groups: bool) -> Result<Vec<UserAndGroups>>;
}
#[async_trait]
impl UserBackendHandler for TestBackendHandler {
async fn get_user_details(&self, user_id: &UserId) -> Result<User>; async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;

View File

@ -1,10 +1,10 @@
use ldap3_proto::{ use ldap3_proto::{
proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry,
}; };
use tracing::{debug, info, instrument, warn}; use tracing::{debug, instrument, warn};
use crate::domain::{ use crate::domain::{
handler::{BackendHandler, GroupRequestFilter}, handler::{GroupListerBackendHandler, GroupRequestFilter},
ldap::error::LdapError, ldap::error::LdapError,
types::{Group, GroupColumn, UserId, Uuid}, types::{Group, GroupColumn, UserId, Uuid},
}; };
@ -21,7 +21,7 @@ pub fn get_group_attribute(
group: &Group, group: &Group,
base_dn_str: &str, base_dn_str: &str,
attribute: &str, attribute: &str,
user_filter: &Option<&UserId>, user_filter: &Option<UserId>,
ignored_group_attributes: &[String], ignored_group_attributes: &[String],
) -> Option<Vec<Vec<u8>>> { ) -> Option<Vec<Vec<u8>>> {
let attribute = attribute.to_ascii_lowercase(); let attribute = attribute.to_ascii_lowercase();
@ -34,7 +34,7 @@ pub fn get_group_attribute(
"member" | "uniquemember" => group "member" | "uniquemember" => group
.users .users
.iter() .iter()
.filter(|u| user_filter.map(|f| *u == f).unwrap_or(true)) .filter(|u| user_filter.as_ref().map(|f| *u == f).unwrap_or(true))
.map(|u| format!("uid={},ou=people,{}", u, base_dn_str).into_bytes()) .map(|u| format!("uid={},ou=people,{}", u, base_dn_str).into_bytes())
.collect(), .collect(),
"1.1" => return None, "1.1" => return None,
@ -81,7 +81,7 @@ fn make_ldap_search_group_result_entry(
group: Group, group: Group,
base_dn_str: &str, base_dn_str: &str,
attributes: &[String], attributes: &[String],
user_filter: &Option<&UserId>, user_filter: &Option<UserId>,
ignored_group_attributes: &[String], ignored_group_attributes: &[String],
) -> LdapSearchResultEntry { ) -> LdapSearchResultEntry {
let expanded_attributes = expand_group_attribute_wildcards(attributes); let expanded_attributes = expand_group_attribute_wildcards(attributes);
@ -201,25 +201,17 @@ fn convert_group_filter(
} }
#[instrument(skip_all, level = "debug")] #[instrument(skip_all, level = "debug")]
pub async fn get_groups_list<Backend: BackendHandler>( pub async fn get_groups_list<Backend: GroupListerBackendHandler>(
ldap_info: &LdapInfo, ldap_info: &LdapInfo,
ldap_filter: &LdapFilter, ldap_filter: &LdapFilter,
base: &str, base: &str,
user_filter: &Option<&UserId>, backend: &Backend,
backend: &mut Backend,
) -> LdapResult<Vec<Group>> { ) -> LdapResult<Vec<Group>> {
debug!(?ldap_filter); debug!(?ldap_filter);
let filter = convert_group_filter(ldap_info, ldap_filter)?; let filters = convert_group_filter(ldap_info, ldap_filter)?;
let parsed_filters = match user_filter { debug!(?filters);
None => filter,
Some(u) => {
info!("Unprivileged search, limiting results");
GroupRequestFilter::And(vec![filter, GroupRequestFilter::Member((*u).clone())])
}
};
debug!(?parsed_filters);
backend backend
.list_groups(Some(parsed_filters)) .list_groups(Some(filters))
.await .await
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::Other, code: LdapResultCode::Other,
@ -231,7 +223,7 @@ pub fn convert_groups_to_ldap_op<'a>(
groups: Vec<Group>, groups: Vec<Group>,
attributes: &'a [String], attributes: &'a [String],
ldap_info: &'a LdapInfo, ldap_info: &'a LdapInfo,
user_filter: &'a Option<&'a UserId>, user_filter: &'a Option<UserId>,
) -> impl Iterator<Item = LdapOp> + 'a { ) -> impl Iterator<Item = LdapOp> + 'a {
groups.into_iter().map(move |g| { groups.into_iter().map(move |g| {
LdapOp::SearchResultEntry(make_ldap_search_group_result_entry( LdapOp::SearchResultEntry(make_ldap_search_group_result_entry(

View File

@ -2,10 +2,10 @@ use chrono::TimeZone;
use ldap3_proto::{ use ldap3_proto::{
proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry,
}; };
use tracing::{debug, info, instrument, warn}; use tracing::{debug, instrument, warn};
use crate::domain::{ use crate::domain::{
handler::{BackendHandler, UserRequestFilter}, handler::{UserListerBackendHandler, UserRequestFilter},
ldap::{ ldap::{
error::LdapError, error::LdapError,
utils::{expand_attribute_wildcards, get_user_id_from_distinguished_name}, utils::{expand_attribute_wildcards, get_user_id_from_distinguished_name},
@ -217,26 +217,18 @@ fn expand_user_attribute_wildcards(attributes: &[String]) -> Vec<&str> {
} }
#[instrument(skip_all, level = "debug")] #[instrument(skip_all, level = "debug")]
pub async fn get_user_list<Backend: BackendHandler>( pub async fn get_user_list<Backend: UserListerBackendHandler>(
ldap_info: &LdapInfo, ldap_info: &LdapInfo,
ldap_filter: &LdapFilter, ldap_filter: &LdapFilter,
request_groups: bool, request_groups: bool,
base: &str, base: &str,
user_filter: &Option<&UserId>, backend: &Backend,
backend: &mut Backend,
) -> LdapResult<Vec<UserAndGroups>> { ) -> LdapResult<Vec<UserAndGroups>> {
debug!(?ldap_filter); debug!(?ldap_filter);
let filters = convert_user_filter(ldap_info, ldap_filter)?; let filters = convert_user_filter(ldap_info, ldap_filter)?;
let parsed_filters = match user_filter { debug!(?filters);
None => filters,
Some(u) => {
info!("Unprivileged search, limiting results");
UserRequestFilter::And(vec![filters, UserRequestFilter::UserId((*u).clone())])
}
};
debug!(?parsed_filters);
backend backend
.list_users(Some(parsed_filters), request_groups) .list_users(Some(filters), request_groups)
.await .await
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::Other, code: LdapResultCode::Other,

View File

@ -4,7 +4,7 @@ use async_trait::async_trait;
pub use lldap_auth::{login, registration}; pub use lldap_auth::{login, registration};
#[async_trait] #[async_trait]
pub trait OpaqueHandler: Clone + Send { pub trait OpaqueHandler: Send + Sync {
async fn login_start( async fn login_start(
&self, &self,
request: login::ClientLoginStartRequest, request: login::ClientLoginStartRequest,

View File

@ -1,4 +1,4 @@
use super::{handler::BackendHandler, sql_tables::DbConnection}; use crate::domain::{handler::BackendHandler, sql_tables::DbConnection};
use crate::infra::configuration::Configuration; use crate::infra::configuration::Configuration;
use async_trait::async_trait; use async_trait::async_trait;
@ -23,7 +23,8 @@ pub mod tests {
use crate::{ use crate::{
domain::{ domain::{
handler::{ handler::{
CreateUserRequest, GroupBackendHandler, UserBackendHandler, UserRequestFilter, CreateUserRequest, GroupBackendHandler, UserBackendHandler,
UserListerBackendHandler, UserRequestFilter,
}, },
sql_tables::init_table, sql_tables::init_table,
types::{GroupId, UserId}, types::{GroupId, UserId},

View File

@ -1,6 +1,8 @@
use crate::domain::{ use crate::domain::{
error::{DomainError, Result}, error::{DomainError, Result},
handler::{GroupBackendHandler, GroupRequestFilter, UpdateGroupRequest}, handler::{
GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter, UpdateGroupRequest,
},
model::{self, GroupColumn, MembershipColumn}, model::{self, GroupColumn, MembershipColumn},
sql_backend_handler::SqlBackendHandler, sql_backend_handler::SqlBackendHandler,
types::{Group, GroupDetails, GroupId, Uuid}, types::{Group, GroupDetails, GroupId, Uuid},
@ -57,7 +59,7 @@ fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond {
} }
#[async_trait] #[async_trait]
impl GroupBackendHandler for SqlBackendHandler { impl GroupListerBackendHandler for SqlBackendHandler {
#[instrument(skip_all, level = "debug", ret, err)] #[instrument(skip_all, level = "debug", ret, err)]
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>> { async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>> {
debug!(?filters); debug!(?filters);
@ -94,7 +96,10 @@ impl GroupBackendHandler for SqlBackendHandler {
}) })
.collect()) .collect())
} }
}
#[async_trait]
impl GroupBackendHandler for SqlBackendHandler {
#[instrument(skip_all, level = "debug", ret, err)] #[instrument(skip_all, level = "debug", ret, err)]
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails> { async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails> {
debug!(?group_id); debug!(?group_id);

View File

@ -1,6 +1,9 @@
use super::{ use crate::domain::{
error::{DomainError, Result}, error::{DomainError, Result},
handler::{CreateUserRequest, UpdateUserRequest, UserBackendHandler, UserRequestFilter}, handler::{
CreateUserRequest, UpdateUserRequest, UserBackendHandler, UserListerBackendHandler,
UserRequestFilter,
},
model::{self, GroupColumn, UserColumn}, model::{self, GroupColumn, UserColumn},
sql_backend_handler::SqlBackendHandler, sql_backend_handler::SqlBackendHandler,
types::{GroupDetails, GroupId, User, UserAndGroups, UserId, Uuid}, types::{GroupDetails, GroupId, User, UserAndGroups, UserId, Uuid},
@ -70,7 +73,7 @@ fn to_value(opt_name: &Option<String>) -> ActiveValue<Option<String>> {
} }
#[async_trait] #[async_trait]
impl UserBackendHandler for SqlBackendHandler { impl UserListerBackendHandler for SqlBackendHandler {
#[instrument(skip_all, level = "debug", ret, err)] #[instrument(skip_all, level = "debug", ret, err)]
async fn list_users( async fn list_users(
&self, &self,
@ -135,7 +138,10 @@ impl UserBackendHandler for SqlBackendHandler {
.collect()) .collect())
} }
} }
}
#[async_trait]
impl UserBackendHandler for SqlBackendHandler {
#[instrument(skip_all, level = "debug", ret)] #[instrument(skip_all, level = "debug", ret)]
async fn get_user_details(&self, user_id: &UserId) -> Result<User> { async fn get_user_details(&self, user_id: &UserId) -> Result<User> {
debug!(?user_id); debug!(?user_id);

View File

@ -0,0 +1,317 @@
use std::collections::HashSet;
use async_trait::async_trait;
use tracing::info;
use crate::domain::{
error::Result,
handler::{
BackendHandler, CreateUserRequest, GroupListerBackendHandler, GroupRequestFilter,
UpdateGroupRequest, UpdateUserRequest, UserListerBackendHandler, UserRequestFilter,
},
types::{Group, GroupDetails, GroupId, User, UserAndGroups, UserId},
};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Permission {
Admin,
PasswordManager,
Readonly,
Regular,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationResults {
pub user: UserId,
pub permission: Permission,
}
impl ValidationResults {
#[cfg(test)]
pub fn admin() -> Self {
Self {
user: UserId::new("admin"),
permission: Permission::Admin,
}
}
#[must_use]
pub fn is_admin(&self) -> bool {
self.permission == Permission::Admin
}
#[must_use]
pub fn can_read_all(&self) -> bool {
self.permission == Permission::Admin
|| self.permission == Permission::Readonly
|| self.permission == Permission::PasswordManager
}
#[must_use]
pub fn can_read(&self, user: &UserId) -> bool {
self.permission == Permission::Admin
|| self.permission == Permission::PasswordManager
|| self.permission == Permission::Readonly
|| &self.user == user
}
#[must_use]
pub fn can_change_password(&self, user: &UserId, user_is_admin: bool) -> bool {
self.permission == Permission::Admin
|| (self.permission == Permission::PasswordManager && !user_is_admin)
|| &self.user == user
}
#[must_use]
pub fn can_write(&self, user: &UserId) -> bool {
self.permission == Permission::Admin || &self.user == user
}
}
#[async_trait]
pub trait UserReadableBackendHandler {
async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>>;
}
#[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
{
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, group_name: &str) -> Result<GroupId>;
async fn delete_group(&self, group_id: GroupId) -> Result<()>;
}
#[async_trait]
impl<Handler: BackendHandler> UserReadableBackendHandler for Handler {
async fn get_user_details(&self, user_id: &UserId) -> Result<User> {
self.get_user_details(user_id).await
}
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>> {
self.get_user_groups(user_id).await
}
}
#[async_trait]
impl<Handler: BackendHandler> ReadonlyBackendHandler for Handler {
async fn list_users(
&self,
filters: Option<UserRequestFilter>,
get_groups: bool,
) -> Result<Vec<UserAndGroups>> {
self.list_users(filters, get_groups).await
}
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>> {
self.list_groups(filters).await
}
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails> {
self.get_group_details(group_id).await
}
}
#[async_trait]
impl<Handler: BackendHandler> UserWriteableBackendHandler for Handler {
async fn update_user(&self, request: UpdateUserRequest) -> Result<()> {
self.update_user(request).await
}
}
#[async_trait]
impl<Handler: BackendHandler> AdminBackendHandler for Handler {
async fn create_user(&self, request: CreateUserRequest) -> Result<()> {
self.create_user(request).await
}
async fn delete_user(&self, user_id: &UserId) -> Result<()> {
self.delete_user(user_id).await
}
async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> {
self.add_user_to_group(user_id, group_id).await
}
async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> {
self.remove_user_from_group(user_id, group_id).await
}
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()> {
self.update_group(request).await
}
async fn create_group(&self, group_name: &str) -> Result<GroupId> {
self.create_group(group_name).await
}
async fn delete_group(&self, group_id: GroupId) -> Result<()> {
self.delete_group(group_id).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> {
validation_result.is_admin().then_some(&self.handler)
}
pub fn get_readonly_handler(
&self,
validation_result: &ValidationResults,
) -> Option<&impl ReadonlyBackendHandler> {
validation_result.can_read_all().then_some(&self.handler)
}
pub fn get_writeable_handler(
&self,
validation_result: &ValidationResults,
user_id: &UserId,
) -> Option<&impl UserWriteableBackendHandler> {
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> {
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<'a, Groups: Iterator<Item = &'a String> + Clone + 'a>(
&self,
user_id: UserId,
groups: Groups,
) -> ValidationResults {
let is_in_group = |name| groups.clone().any(|g| g == name);
ValidationResults {
user: user_id,
permission: if is_in_group("lldap_admin") {
Permission::Admin
} else if is_in_group("lldap_password_manager") {
Permission::PasswordManager
} else if is_in_group("lldap_strict_readonly") {
Permission::Readonly
} else {
Permission::Regular
},
}
}
}
pub struct UserRestrictedListerBackendHandler<'a, Handler> {
handler: &'a Handler,
pub user_filter: Option<UserId>,
}
#[async_trait]
impl<'a, Handler: UserListerBackendHandler + Sync> UserListerBackendHandler
for UserRestrictedListerBackendHandler<'a, 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<'a, Handler: GroupListerBackendHandler + Sync> GroupListerBackendHandler
for UserRestrictedListerBackendHandler<'a, 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
{
}
#[async_trait]
impl<'a, Handler: GroupListerBackendHandler + UserListerBackendHandler + Sync>
UserAndGroupListerBackendHandler for UserRestrictedListerBackendHandler<'a, Handler>
{
}

View File

@ -30,6 +30,7 @@ use crate::{
types::{GroupDetails, UserColumn, UserId}, types::{GroupDetails, UserColumn, UserId},
}, },
infra::{ infra::{
access_control::{ReadonlyBackendHandler, UserReadableBackendHandler, ValidationResults},
tcp_backend_handler::*, tcp_backend_handler::*,
tcp_server::{error_to_http_response, AppState, TcpError, TcpResult}, tcp_server::{error_to_http_response, AppState, TcpError, TcpResult},
}, },
@ -87,11 +88,10 @@ async fn get_refresh<Backend>(
where where
Backend: TcpBackendHandler + BackendHandler + 'static, Backend: TcpBackendHandler + BackendHandler + 'static,
{ {
let backend_handler = &data.backend_handler;
let jwt_key = &data.jwt_key; let jwt_key = &data.jwt_key;
let (refresh_token_hash, user) = get_refresh_token(request)?; let (refresh_token_hash, user) = get_refresh_token(request)?;
let found = data let found = data
.backend_handler .get_tcp_handler()
.check_token(refresh_token_hash, &user) .check_token(refresh_token_hash, &user)
.await?; .await?;
if !found { if !found {
@ -99,7 +99,8 @@ where
"Invalid refresh token".to_string(), "Invalid refresh token".to_string(),
))); )));
} }
Ok(backend_handler Ok(data
.get_readonly_handler()
.get_user_groups(&user) .get_user_groups(&user)
.await .await
.map(|groups| create_jwt(jwt_key, user.to_string(), groups)) .map(|groups| create_jwt(jwt_key, user.to_string(), groups))
@ -145,7 +146,7 @@ where
.get("user_id") .get("user_id")
.ok_or_else(|| TcpError::BadRequest("Missing user ID".to_string()))?; .ok_or_else(|| TcpError::BadRequest("Missing user ID".to_string()))?;
let user_results = data let user_results = data
.backend_handler .get_readonly_handler()
.list_users( .list_users(
Some(UserRequestFilter::Or(vec![ Some(UserRequestFilter::Or(vec![
UserRequestFilter::UserId(UserId::new(user_string)), UserRequestFilter::UserId(UserId::new(user_string)),
@ -163,7 +164,7 @@ where
} }
let user = &user_results[0].user; let user = &user_results[0].user;
let token = match data let token = match data
.backend_handler .get_tcp_handler()
.start_password_reset(&user.user_id) .start_password_reset(&user.user_id)
.await? .await?
{ {
@ -216,7 +217,7 @@ where
.get("token") .get("token")
.ok_or_else(|| TcpError::BadRequest("Missing reset token".to_owned()))?; .ok_or_else(|| TcpError::BadRequest("Missing reset token".to_owned()))?;
let user_id = data let user_id = data
.backend_handler .get_tcp_handler()
.get_user_id_for_password_reset_token(token) .get_user_id_for_password_reset_token(token)
.await .await
.map_err(|e| { .map_err(|e| {
@ -224,7 +225,7 @@ where
TcpError::NotFoundError("Wrong or expired reset token".to_owned()) TcpError::NotFoundError("Wrong or expired reset token".to_owned())
})?; })?;
let _ = data let _ = data
.backend_handler .get_tcp_handler()
.delete_password_reset_token(token) .delete_password_reset_token(token)
.await; .await;
let groups = HashSet::new(); let groups = HashSet::new();
@ -266,10 +267,10 @@ where
Backend: TcpBackendHandler + BackendHandler + 'static, Backend: TcpBackendHandler + BackendHandler + 'static,
{ {
let (refresh_token_hash, user) = get_refresh_token(request)?; let (refresh_token_hash, user) = get_refresh_token(request)?;
data.backend_handler data.get_tcp_handler()
.delete_refresh_token(refresh_token_hash) .delete_refresh_token(refresh_token_hash)
.await?; .await?;
let new_blacklisted_jwts = data.backend_handler.blacklist_jwts(&user).await?; let new_blacklisted_jwts = data.get_tcp_handler().blacklist_jwts(&user).await?;
let mut jwt_blacklist = data.jwt_blacklist.write().unwrap(); let mut jwt_blacklist = data.jwt_blacklist.write().unwrap();
for jwt in new_blacklisted_jwts { for jwt in new_blacklisted_jwts {
jwt_blacklist.insert(jwt); jwt_blacklist.insert(jwt);
@ -320,7 +321,7 @@ async fn opaque_login_start<Backend>(
where where
Backend: OpaqueHandler + 'static, Backend: OpaqueHandler + 'static,
{ {
data.backend_handler data.get_opaque_handler()
.login_start(request.into_inner()) .login_start(request.into_inner())
.await .await
.map(|res| ApiResult::Left(web::Json(res))) .map(|res| ApiResult::Left(web::Json(res)))
@ -337,8 +338,8 @@ where
{ {
// The authentication was successful, we need to fetch the groups to create the JWT // The authentication was successful, we need to fetch the groups to create the JWT
// token. // token.
let groups = data.backend_handler.get_user_groups(name).await?; let groups = data.get_readonly_handler().get_user_groups(name).await?;
let (refresh_token, max_age) = data.backend_handler.create_refresh_token(name).await?; let (refresh_token, max_age) = data.get_tcp_handler().create_refresh_token(name).await?;
let token = create_jwt(&data.jwt_key, name.to_string(), groups); let token = create_jwt(&data.jwt_key, name.to_string(), groups);
let refresh_token_plus_name = refresh_token + "+" + name.as_str(); let refresh_token_plus_name = refresh_token + "+" + name.as_str();
@ -374,7 +375,7 @@ where
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static, Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static,
{ {
let name = data let name = data
.backend_handler .get_opaque_handler()
.login_finish(request.into_inner()) .login_finish(request.into_inner())
.await?; .await?;
get_login_successful_response(&data, &name).await get_login_successful_response(&data, &name).await
@ -405,7 +406,7 @@ where
name: user_id.clone(), name: user_id.clone(),
password: request.password.clone(), password: request.password.clone(),
}; };
data.backend_handler.bind(bind_request).await?; data.get_login_handler().bind(bind_request).await?;
get_login_successful_response(&data, &user_id).await get_login_successful_response(&data, &user_id).await
} }
@ -431,7 +432,7 @@ where
{ {
let name = request.name.clone(); let name = request.name.clone();
debug!(%name); debug!(%name);
data.backend_handler.bind(request.into_inner()).await?; data.get_login_handler().bind(request.into_inner()).await?;
get_login_successful_response(&data, &name).await get_login_successful_response(&data, &name).await
} }
@ -474,7 +475,7 @@ where
.into_inner(); .into_inner();
let user_id = UserId::new(&registration_start_request.username); let user_id = UserId::new(&registration_start_request.username);
let user_is_admin = data let user_is_admin = data
.backend_handler .get_readonly_handler()
.get_user_groups(&user_id) .get_user_groups(&user_id)
.await? .await?
.iter() .iter()
@ -485,7 +486,7 @@ where
)); ));
} }
Ok(data Ok(data
.backend_handler .get_opaque_handler()
.registration_start(registration_start_request) .registration_start(registration_start_request)
.await?) .await?)
} }
@ -512,7 +513,7 @@ async fn opaque_register_finish<Backend>(
where where
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static, Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static,
{ {
data.backend_handler data.get_opaque_handler()
.registration_finish(request.into_inner()) .registration_finish(request.into_inner())
.await?; .await?;
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
@ -586,64 +587,8 @@ where
} }
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Permission {
Admin,
PasswordManager,
Readonly,
Regular,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationResults {
pub user: UserId,
pub permission: Permission,
}
impl ValidationResults {
#[cfg(test)]
pub fn admin() -> Self {
Self {
user: UserId::new("admin"),
permission: Permission::Admin,
}
}
#[must_use]
pub fn is_admin(&self) -> bool {
self.permission == Permission::Admin
}
#[must_use]
pub fn is_admin_or_readonly(&self) -> bool {
self.permission == Permission::Admin
|| self.permission == Permission::Readonly
|| self.permission == Permission::PasswordManager
}
#[must_use]
pub fn can_read(&self, user: &UserId) -> bool {
self.permission == Permission::Admin
|| self.permission == Permission::PasswordManager
|| self.permission == Permission::Readonly
|| &self.user == user
}
#[must_use]
pub fn can_change_password(&self, user: &UserId, user_is_admin: bool) -> bool {
self.permission == Permission::Admin
|| (self.permission == Permission::PasswordManager && !user_is_admin)
|| &self.user == user
}
#[must_use]
pub fn can_write(&self, user: &UserId) -> bool {
self.permission == Permission::Admin || &self.user == user
}
}
#[instrument(skip_all, level = "debug", err, ret)] #[instrument(skip_all, level = "debug", err, ret)]
pub(crate) fn check_if_token_is_valid<Backend>( pub(crate) fn check_if_token_is_valid<Backend: BackendHandler>(
state: &AppState<Backend>, state: &AppState<Backend>,
token_str: &str, token_str: &str,
) -> Result<ValidationResults, actix_web::Error> { ) -> Result<ValidationResults, actix_web::Error> {
@ -666,19 +611,10 @@ pub(crate) fn check_if_token_is_valid<Backend>(
if state.jwt_blacklist.read().unwrap().contains(&jwt_hash) { if state.jwt_blacklist.read().unwrap().contains(&jwt_hash) {
return Err(ErrorUnauthorized("JWT was logged out")); return Err(ErrorUnauthorized("JWT was logged out"));
} }
let is_in_group = |name| token.claims().groups.contains(name); Ok(state.backend_handler.get_permissions_from_groups(
Ok(ValidationResults { UserId::new(&token.claims().user),
user: UserId::new(&token.claims().user), token.claims().groups.iter(),
permission: if is_in_group("lldap_admin") { ))
Permission::Admin
} else if is_in_group("lldap_password_manager") {
Permission::PasswordManager
} else if is_in_group("lldap_strict_readonly") {
Permission::Readonly
} else {
Permission::Regular
},
})
} }
pub fn configure_server<Backend>(cfg: &mut web::ServiceConfig, enable_password_reset: bool) pub fn configure_server<Backend>(cfg: &mut web::ServiceConfig, enable_password_reset: bool)

View File

@ -1,29 +1,77 @@
use crate::{ use crate::{
domain::handler::BackendHandler, domain::{handler::BackendHandler, types::UserId},
infra::{ 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, cli::ExportGraphQLSchemaOpts,
graphql::{mutation::Mutation, query::Query},
tcp_server::AppState, tcp_server::AppState,
}, },
}; };
use actix_web::{web, Error, HttpResponse}; use actix_web::{web, Error, HttpResponse};
use actix_web_httpauth::extractors::bearer::BearerAuth; 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 juniper_actix::{graphiql_handler, graphql_handler, playground_handler};
use tracing::debug;
use super::{mutation::Mutation, query::Query};
pub struct Context<Handler: BackendHandler> { pub struct Context<Handler: BackendHandler> {
pub handler: Box<Handler>, pub handler: AccessControlledBackendHandler<Handler>,
pub validation_result: ValidationResults, 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> {} impl<Handler: BackendHandler> juniper::Context for Context<Handler> {}
type Schema<Handler> = type Schema<Handler> =
RootNode<'static, Query<Handler>, Mutation<Handler>, EmptySubscription<Context<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( Schema::new(
Query::<Handler>::new(), Query::<Handler>::new(),
Mutation::<Handler>::new(), Mutation::<Handler>::new(),
@ -58,7 +106,7 @@ async fn playground_route() -> Result<HttpResponse, Error> {
playground_handler("/api/graphql", None).await playground_handler("/api/graphql", None).await
} }
async fn graphql_route<Handler: BackendHandler + Sync>( async fn graphql_route<Handler: BackendHandler + Clone>(
req: actix_web::HttpRequest, req: actix_web::HttpRequest,
mut payload: actix_web::web::Payload, mut payload: actix_web::web::Payload,
data: web::Data<AppState<Handler>>, 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 bearer = BearerAuth::from_request(&req, &mut payload.0).await?;
let validation_result = check_if_token_is_valid(&data, bearer.token())?; let validation_result = check_if_token_is_valid(&data, bearer.token())?;
let context = Context::<Handler> { let context = Context::<Handler> {
handler: Box::new(data.backend_handler.clone()), handler: data.backend_handler.clone(),
validation_result, validation_result,
}; };
graphql_handler(&schema(), &context, req, payload).await 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) pub fn configure_endpoint<Backend>(cfg: &mut web::ServiceConfig)
where where
Backend: BackendHandler + Sync + 'static, Backend: BackendHandler + Clone + 'static,
{ {
let json_config = web::JsonConfig::default() let json_config = web::JsonConfig::default()
.limit(4096) .limit(4096)

View File

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

View File

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

View File

@ -12,7 +12,10 @@ use crate::{
opaque_handler::OpaqueHandler, opaque_handler::OpaqueHandler,
types::{Group, JpegPhoto, UserAndGroups, UserId}, types::{Group, JpegPhoto, UserAndGroups, UserId},
}, },
infra::auth_service::{Permission, ValidationResults}, infra::access_control::{
AccessControlledBackendHandler, AdminBackendHandler, UserAndGroupListerBackendHandler,
UserReadableBackendHandler, ValidationResults,
},
}; };
use anyhow::Result; use anyhow::Result;
use ldap3_proto::proto::{ use ldap3_proto::proto::{
@ -175,15 +178,27 @@ fn root_dse_response(base_dn: &str) -> LdapOp {
}) })
} }
pub struct LdapHandler<Backend: BackendHandler + LoginHandler + OpaqueHandler> { pub struct LdapHandler<Backend> {
user_info: Option<ValidationResults>, user_info: Option<ValidationResults>,
backend_handler: Backend, backend_handler: AccessControlledBackendHandler<Backend>,
ldap_info: LdapInfo, ldap_info: LdapInfo,
} }
impl<Backend: LoginHandler> LdapHandler<Backend> {
pub fn get_login_handler(&self) -> &impl LoginHandler {
self.backend_handler.unsafe_get_handler()
}
}
impl<Backend: OpaqueHandler> LdapHandler<Backend> {
pub fn get_opaque_handler(&self) -> &impl OpaqueHandler {
self.backend_handler.unsafe_get_handler()
}
}
impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend> { impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend> {
pub fn new( pub fn new(
backend_handler: Backend, backend_handler: AccessControlledBackendHandler<Backend>,
mut ldap_base_dn: String, mut ldap_base_dn: String,
ignored_user_attributes: Vec<String>, ignored_user_attributes: Vec<String>,
ignored_group_attributes: Vec<String>, ignored_group_attributes: Vec<String>,
@ -206,6 +221,16 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
} }
#[cfg(test)]
pub fn new_for_tests(backend_handler: Backend, ldap_base_dn: &str) -> Self {
Self::new(
AccessControlledBackendHandler::new(backend_handler),
ldap_base_dn.to_string(),
vec![],
vec![],
)
}
#[instrument(skip_all, level = "debug")] #[instrument(skip_all, level = "debug")]
pub async fn do_bind(&mut self, request: &LdapBindRequest) -> (LdapResultCode, String) { pub async fn do_bind(&mut self, request: &LdapBindRequest) -> (LdapResultCode, String) {
debug!("DN: {}", &request.dn); debug!("DN: {}", &request.dn);
@ -219,7 +244,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
}; };
let LdapBindCred::Simple(password) = &request.cred; let LdapBindCred::Simple(password) = &request.cred;
match self match self
.backend_handler .get_login_handler()
.bind(BindRequest { .bind(BindRequest {
name: user_id.clone(), name: user_id.clone(),
password: password.clone(), password: password.clone(),
@ -227,25 +252,11 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
.await .await
{ {
Ok(()) => { Ok(()) => {
let user_groups = self.backend_handler.get_user_groups(&user_id).await; self.user_info = self
let is_in_group = |name| { .backend_handler
user_groups .get_permissions_for_user(user_id)
.as_ref() .await
.map(|groups| groups.iter().any(|g| g.display_name == name)) .ok();
.unwrap_or(false)
};
self.user_info = Some(ValidationResults {
user: user_id,
permission: if is_in_group("lldap_admin") {
Permission::Admin
} else if is_in_group("lldap_password_manager") {
Permission::PasswordManager
} else if is_in_group("lldap_strict_readonly") {
Permission::Readonly
} else {
Permission::Regular
},
});
debug!("Success!"); debug!("Success!");
(LdapResultCode::Success, "".to_string()) (LdapResultCode::Success, "".to_string())
} }
@ -253,7 +264,12 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
} }
async fn change_password(&mut self, user: &UserId, password: &str) -> Result<()> { async fn change_password<B: OpaqueHandler>(
&self,
backend_handler: &B,
user: &UserId,
password: &str,
) -> Result<()> {
use lldap_auth::*; use lldap_auth::*;
let mut rng = rand::rngs::OsRng; let mut rng = rand::rngs::OsRng;
let registration_start_request = let registration_start_request =
@ -262,7 +278,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
username: user.to_string(), username: user.to_string(),
registration_start_request: registration_start_request.message, registration_start_request: registration_start_request.message,
}; };
let registration_start_response = self.backend_handler.registration_start(req).await?; let registration_start_response = backend_handler.registration_start(req).await?;
let registration_finish = opaque::client::registration::finish_registration( let registration_finish = opaque::client::registration::finish_registration(
registration_start_request.state, registration_start_request.state,
registration_start_response.registration_response, registration_start_response.registration_response,
@ -272,7 +288,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
server_data: registration_start_response.server_data, server_data: registration_start_response.server_data,
registration_upload: registration_finish.message, registration_upload: registration_finish.message,
}; };
self.backend_handler.registration_finish(req).await?; backend_handler.registration_finish(req).await?;
Ok(()) Ok(())
} }
@ -294,6 +310,8 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
Ok(uid) => { Ok(uid) => {
let user_is_admin = self let user_is_admin = self
.backend_handler .backend_handler
.get_readable_handler(credentials, &uid)
.expect("Unexpected permission error")
.get_user_groups(&uid) .get_user_groups(&uid)
.await .await
.map_err(|e| LdapError { .map_err(|e| LdapError {
@ -313,7 +331,10 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
&credentials.user, &uid &credentials.user, &uid
), ),
}) })
} else if let Err(e) = self.change_password(&uid, password).await { } else if let Err(e) = self
.change_password(self.get_opaque_handler(), &uid, password)
.await
{
Err(LdapError { Err(LdapError {
code: LdapResultCode::Other, code: LdapResultCode::Other,
message: format!("Error while changing the password: {:#?}", e), message: format!("Error while changing the password: {:#?}", e),
@ -351,18 +372,6 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
} }
fn get_user_permission_filter(&self) -> LdapResult<Option<UserId>> {
let user_info = self.user_info.as_ref().ok_or_else(|| LdapError {
code: LdapResultCode::InsufficentAccessRights,
message: "No user currently bound".to_string(),
})?;
Ok(if user_info.is_admin_or_readonly() {
None
} else {
Some(user_info.user.clone())
})
}
pub async fn do_search_or_dse( pub async fn do_search_or_dse(
&mut self, &mut self,
request: &LdapSearchRequest, request: &LdapSearchRequest,
@ -382,22 +391,22 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
async fn do_search_internal( async fn do_search_internal(
&mut self, &self,
backend_handler: &impl UserAndGroupListerBackendHandler,
request: &LdapSearchRequest, request: &LdapSearchRequest,
user_filter: &Option<&UserId>,
) -> LdapResult<(Option<Vec<UserAndGroups>>, Option<Vec<Group>>)> { ) -> LdapResult<(Option<Vec<UserAndGroups>>, Option<Vec<Group>>)> {
let dn_parts = parse_distinguished_name(&request.base.to_ascii_lowercase())?; let dn_parts = parse_distinguished_name(&request.base.to_ascii_lowercase())?;
let scope = get_search_scope(&self.ldap_info.base_dn, &dn_parts); let scope = get_search_scope(&self.ldap_info.base_dn, &dn_parts);
debug!(?request.base, ?scope); debug!(?request.base, ?scope);
// Disambiguate the lifetimes. // Disambiguate the lifetimes.
fn cast<'a, T, R, B: 'a>(x: T) -> T fn cast<'a, T, R>(x: T) -> T
where where
T: Fn(&'a mut B, &'a LdapFilter) -> R + 'a, T: Fn(&'a LdapFilter) -> R + 'a,
{ {
x x
} }
let get_user_list = cast(|backend_handler: &mut Backend, filter: &LdapFilter| async { let get_user_list = cast(|filter: &LdapFilter| async {
let need_groups = request let need_groups = request
.attrs .attrs
.iter() .iter()
@ -407,47 +416,27 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
filter, filter,
need_groups, need_groups,
&request.base, &request.base,
user_filter,
backend_handler, backend_handler,
) )
.await .await
}); });
let get_group_list = cast(|backend_handler: &mut Backend, filter: &LdapFilter| async { let get_group_list = cast(|filter: &LdapFilter| async {
get_groups_list( get_groups_list(&self.ldap_info, filter, &request.base, backend_handler).await
&self.ldap_info,
filter,
&request.base,
user_filter,
backend_handler,
)
.await
}); });
Ok(match scope { Ok(match scope {
SearchScope::Global => ( SearchScope::Global => (
Some(get_user_list(&mut self.backend_handler, &request.filter).await?), Some(get_user_list(&request.filter).await?),
Some(get_group_list(&mut self.backend_handler, &request.filter).await?), Some(get_group_list(&request.filter).await?),
),
SearchScope::Users => (
Some(get_user_list(&mut self.backend_handler, &request.filter).await?),
None,
),
SearchScope::Groups => (
None,
Some(get_group_list(&mut self.backend_handler, &request.filter).await?),
), ),
SearchScope::Users => (Some(get_user_list(&request.filter).await?), None),
SearchScope::Groups => (None, Some(get_group_list(&request.filter).await?)),
SearchScope::User(filter) => { SearchScope::User(filter) => {
let filter = LdapFilter::And(vec![request.filter.clone(), filter]); let filter = LdapFilter::And(vec![request.filter.clone(), filter]);
( (Some(get_user_list(&filter).await?), None)
Some(get_user_list(&mut self.backend_handler, &filter).await?),
None,
)
} }
SearchScope::Group(filter) => { SearchScope::Group(filter) => {
let filter = LdapFilter::And(vec![request.filter.clone(), filter]); let filter = LdapFilter::And(vec![request.filter.clone(), filter]);
( (None, Some(get_group_list(&filter).await?))
None,
Some(get_group_list(&mut self.backend_handler, &filter).await?),
)
} }
SearchScope::Unknown => { SearchScope::Unknown => {
warn!( warn!(
@ -468,10 +457,15 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
#[instrument(skip_all, level = "debug")] #[instrument(skip_all, level = "debug")]
pub async fn do_search(&mut self, request: &LdapSearchRequest) -> LdapResult<Vec<LdapOp>> { pub async fn do_search(&self, request: &LdapSearchRequest) -> LdapResult<Vec<LdapOp>> {
let user_filter = self.get_user_permission_filter()?; let user_info = self.user_info.as_ref().ok_or_else(|| LdapError {
let user_filter = user_filter.as_ref(); code: LdapResultCode::InsufficentAccessRights,
let (users, groups) = self.do_search_internal(request, &user_filter).await?; message: "No user currently bound".to_string(),
})?;
let backend_handler = self
.backend_handler
.get_user_restricted_lister_handler(user_info);
let (users, groups) = self.do_search_internal(&backend_handler, request).await?;
let mut results = Vec::new(); let mut results = Vec::new();
if let Some(users) = users { if let Some(users) = users {
@ -486,7 +480,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
groups, groups,
&request.attrs, &request.attrs,
&self.ldap_info, &self.ldap_info,
&user_filter, &backend_handler.user_filter,
)); ));
} }
if results.is_empty() || matches!(results[results.len() - 1], LdapOp::SearchResultEntry(_)) if results.is_empty() || matches!(results[results.len() - 1], LdapOp::SearchResultEntry(_))
@ -497,17 +491,14 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
async fn do_create_user(&self, request: LdapAddRequest) -> LdapResult<Vec<LdapOp>> { async fn do_create_user(&self, request: LdapAddRequest) -> LdapResult<Vec<LdapOp>> {
if !self let backend_handler = self
.user_info .user_info
.as_ref() .as_ref()
.map(|u| u.is_admin()) .and_then(|u| self.backend_handler.get_admin_handler(u))
.unwrap_or(false) .ok_or_else(|| LdapError {
{
return Err(LdapError {
code: LdapResultCode::InsufficentAccessRights, code: LdapResultCode::InsufficentAccessRights,
message: "Unauthorized write".to_string(), message: "Unauthorized write".to_string(),
}); })?;
}
let user_id = get_user_id_from_distinguished_name( let user_id = get_user_id_from_distinguished_name(
&request.dn, &request.dn,
&self.ldap_info.base_dn, &self.ldap_info.base_dn,
@ -552,7 +543,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
.map(Vec::as_slice) .map(Vec::as_slice)
.map(decode_attribute_value) .map(decode_attribute_value)
}; };
self.backend_handler backend_handler
.create_user(CreateUserRequest { .create_user(CreateUserRequest {
user_id, user_id,
email: get_attribute("mail") email: get_attribute("mail")
@ -693,16 +684,22 @@ mod tests {
async fn bind(&self, request: BindRequest) -> Result<()>; async fn bind(&self, request: BindRequest) -> Result<()>;
} }
#[async_trait] #[async_trait]
impl GroupBackendHandler for TestBackendHandler { impl GroupListerBackendHandler for TestBackendHandler {
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>; async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>;
}
#[async_trait]
impl GroupBackendHandler for TestBackendHandler {
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>; async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>;
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;
async fn create_group(&self, group_name: &str) -> Result<GroupId>; async fn create_group(&self, group_name: &str) -> Result<GroupId>;
async fn delete_group(&self, group_id: GroupId) -> Result<()>; async fn delete_group(&self, group_id: GroupId) -> Result<()>;
} }
#[async_trait] #[async_trait]
impl UserBackendHandler for TestBackendHandler { impl UserListerBackendHandler for TestBackendHandler {
async fn list_users(&self, filters: Option<UserRequestFilter>, get_groups: bool) -> Result<Vec<UserAndGroups>>; async fn list_users(&self, filters: Option<UserRequestFilter>, get_groups: bool) -> Result<Vec<UserAndGroups>>;
}
#[async_trait]
impl UserBackendHandler for TestBackendHandler {
async fn get_user_details(&self, user_id: &UserId) -> Result<User>; async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
@ -768,8 +765,7 @@ mod tests {
}); });
Ok(set) Ok(set)
}); });
let mut ldap_handler = let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=Example,dc=com");
LdapHandler::new(mock, "dc=Example,dc=com".to_string(), vec![], vec![]);
let request = LdapBindRequest { let request = LdapBindRequest {
dn: "uid=test,ou=people,dc=example,dc=coM".to_string(), dn: "uid=test,ou=people,dc=example,dc=coM".to_string(),
cred: LdapBindCred::Simple("pass".to_string()), cred: LdapBindCred::Simple("pass".to_string()),
@ -812,8 +808,7 @@ mod tests {
mock.expect_get_user_groups() mock.expect_get_user_groups()
.with(eq(UserId::new("bob"))) .with(eq(UserId::new("bob")))
.return_once(|_| Ok(HashSet::new())); .return_once(|_| Ok(HashSet::new()));
let mut ldap_handler = let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=eXample,dc=com");
LdapHandler::new(mock, "dc=eXample,dc=com".to_string(), vec![], vec![]);
let request = LdapOp::BindRequest(LdapBindRequest { let request = LdapOp::BindRequest(LdapBindRequest {
dn: "uid=bob,ou=people,dc=example,dc=com".to_string(), dn: "uid=bob,ou=people,dc=example,dc=com".to_string(),
@ -855,8 +850,7 @@ mod tests {
}); });
Ok(set) Ok(set)
}); });
let mut ldap_handler = let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=example,dc=com");
LdapHandler::new(mock, "dc=example,dc=com".to_string(), vec![], vec![]);
let request = LdapBindRequest { let request = LdapBindRequest {
dn: "uid=test,ou=people,dc=example,dc=com".to_string(), dn: "uid=test,ou=people,dc=example,dc=com".to_string(),
@ -997,8 +991,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_bind_invalid_dn() { async fn test_bind_invalid_dn() {
let mock = MockTestBackendHandler::new(); let mock = MockTestBackendHandler::new();
let mut ldap_handler = let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=example,dc=com");
LdapHandler::new(mock, "dc=example,dc=com".to_string(), vec![], vec![]);
let request = LdapBindRequest { let request = LdapBindRequest {
dn: "cn=bob,dc=example,dc=com".to_string(), dn: "cn=bob,dc=example,dc=com".to_string(),

View File

@ -3,7 +3,10 @@ use crate::{
handler::{BackendHandler, LoginHandler}, handler::{BackendHandler, LoginHandler},
opaque_handler::OpaqueHandler, opaque_handler::OpaqueHandler,
}, },
infra::{configuration::Configuration, ldap_handler::LdapHandler}, infra::{
access_control::AccessControlledBackendHandler, configuration::Configuration,
ldap_handler::LdapHandler,
},
}; };
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_server::ServerBuilder; use actix_server::ServerBuilder;
@ -73,7 +76,7 @@ where
let mut resp = FramedWrite::new(w, LdapCodec); let mut resp = FramedWrite::new(w, LdapCodec);
let mut session = LdapHandler::new( let mut session = LdapHandler::new(
backend_handler, AccessControlledBackendHandler::new(backend_handler),
ldap_base_dn, ldap_base_dn,
ignored_user_attributes, ignored_user_attributes,
ignored_group_attributes, ignored_group_attributes,
@ -145,7 +148,7 @@ pub fn build_ldap_server<Backend>(
server_builder: ServerBuilder, server_builder: ServerBuilder,
) -> Result<ServerBuilder> ) -> Result<ServerBuilder>
where where
Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static, Backend: BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static,
{ {
let context = ( let context = (
backend_handler, backend_handler,

View File

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

View File

@ -4,7 +4,7 @@ use std::collections::HashSet;
use crate::domain::{error::Result, types::UserId}; use crate::domain::{error::Result, types::UserId};
#[async_trait] #[async_trait]
pub trait TcpBackendHandler { pub trait TcpBackendHandler: Sync {
async fn get_jwt_blacklist(&self) -> anyhow::Result<HashSet<u64>>; async fn get_jwt_blacklist(&self) -> anyhow::Result<HashSet<u64>>;
async fn create_refresh_token(&self, user: &UserId) -> Result<(String, chrono::Duration)>; async fn create_refresh_token(&self, user: &UserId) -> Result<(String, chrono::Duration)>;
async fn check_token(&self, refresh_token_hash: u64, user: &UserId) -> Result<bool>; async fn check_token(&self, refresh_token_hash: u64, user: &UserId) -> Result<bool>;
@ -34,16 +34,22 @@ mockall::mock! {
async fn bind(&self, request: BindRequest) -> Result<()>; async fn bind(&self, request: BindRequest) -> Result<()>;
} }
#[async_trait] #[async_trait]
impl GroupBackendHandler for TestTcpBackendHandler { impl GroupListerBackendHandler for TestTcpBackendHandler {
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>; async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>>;
}
#[async_trait]
impl GroupBackendHandler for TestTcpBackendHandler {
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>; async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails>;
async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>;
async fn create_group(&self, group_name: &str) -> Result<GroupId>; async fn create_group(&self, group_name: &str) -> Result<GroupId>;
async fn delete_group(&self, group_id: GroupId) -> Result<()>; async fn delete_group(&self, group_id: GroupId) -> Result<()>;
} }
#[async_trait] #[async_trait]
impl UserBackendHandler for TestBackendHandler { impl UserListerBackendHandler for TestBackendHandler {
async fn list_users(&self, filters: Option<UserRequestFilter>, get_groups: bool) -> Result<Vec<UserAndGroups>>; async fn list_users(&self, filters: Option<UserRequestFilter>, get_groups: bool) -> Result<Vec<UserAndGroups>>;
}
#[async_trait]
impl UserBackendHandler for TestBackendHandler {
async fn get_user_details(&self, user_id: &UserId) -> Result<User>; async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;

View File

@ -5,6 +5,7 @@ use crate::{
opaque_handler::OpaqueHandler, opaque_handler::OpaqueHandler,
}, },
infra::{ infra::{
access_control::{AccessControlledBackendHandler, ReadonlyBackendHandler},
auth_service, auth_service,
configuration::{Configuration, MailOptions}, configuration::{Configuration, MailOptions},
logging::CustomRootSpanBuilder, logging::CustomRootSpanBuilder,
@ -74,11 +75,11 @@ fn http_config<Backend>(
server_url: String, server_url: String,
mail_options: MailOptions, mail_options: MailOptions,
) where ) where
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Sync + 'static, Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static,
{ {
let enable_password_reset = mail_options.enable_password_reset; let enable_password_reset = mail_options.enable_password_reset;
cfg.app_data(web::Data::new(AppState::<Backend> { cfg.app_data(web::Data::new(AppState::<Backend> {
backend_handler, backend_handler: AccessControlledBackendHandler::new(backend_handler),
jwt_key: Hmac::new_varkey(jwt_secret.unsecure().as_bytes()).unwrap(), jwt_key: Hmac::new_varkey(jwt_secret.unsecure().as_bytes()).unwrap(),
jwt_blacklist: RwLock::new(jwt_blacklist), jwt_blacklist: RwLock::new(jwt_blacklist),
server_url, server_url,
@ -110,20 +111,41 @@ fn http_config<Backend>(
} }
pub(crate) struct AppState<Backend> { pub(crate) struct AppState<Backend> {
pub backend_handler: Backend, pub backend_handler: AccessControlledBackendHandler<Backend>,
pub jwt_key: Hmac<Sha512>, pub jwt_key: Hmac<Sha512>,
pub jwt_blacklist: RwLock<HashSet<u64>>, pub jwt_blacklist: RwLock<HashSet<u64>>,
pub server_url: String, pub server_url: String,
pub mail_options: MailOptions, pub mail_options: MailOptions,
} }
impl<Backend: BackendHandler> AppState<Backend> {
pub fn get_readonly_handler(&self) -> &impl ReadonlyBackendHandler {
self.backend_handler.unsafe_get_handler()
}
}
impl<Backend: TcpBackendHandler> AppState<Backend> {
pub fn get_tcp_handler(&self) -> &impl TcpBackendHandler {
self.backend_handler.unsafe_get_handler()
}
}
impl<Backend: OpaqueHandler> AppState<Backend> {
pub fn get_opaque_handler(&self) -> &impl OpaqueHandler {
self.backend_handler.unsafe_get_handler()
}
}
impl<Backend: LoginHandler> AppState<Backend> {
pub fn get_login_handler(&self) -> &impl LoginHandler {
self.backend_handler.unsafe_get_handler()
}
}
pub async fn build_tcp_server<Backend>( pub async fn build_tcp_server<Backend>(
config: &Configuration, config: &Configuration,
backend_handler: Backend, backend_handler: Backend,
server_builder: ServerBuilder, server_builder: ServerBuilder,
) -> Result<ServerBuilder> ) -> Result<ServerBuilder>
where where
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Sync + 'static, Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static,
{ {
let jwt_secret = config.jwt_secret.clone(); let jwt_secret = config.jwt_secret.clone();
let jwt_blacklist = backend_handler let jwt_blacklist = backend_handler

View File

@ -7,7 +7,10 @@ use std::time::Duration;
use crate::{ use crate::{
domain::{ domain::{
handler::{CreateUserRequest, GroupBackendHandler, GroupRequestFilter, UserBackendHandler}, handler::{
CreateUserRequest, GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter,
UserBackendHandler,
},
sql_backend_handler::SqlBackendHandler, sql_backend_handler::SqlBackendHandler,
sql_opaque_handler::register_password, sql_opaque_handler::register_password,
}, },