mirror of
https://github.com/nitnelave/lldap.git
synced 2023-04-12 14:25:13 +00:00
server: statically enforce access control
This commit is contained in:
parent
322bf26db5
commit
c9997d4c17
@ -122,13 +122,17 @@ pub struct UpdateGroupRequest {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait LoginHandler: Clone + Send {
|
||||
pub trait LoginHandler: Send + Sync {
|
||||
async fn bind(&self, request: BindRequest) -> Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait GroupBackendHandler {
|
||||
pub trait GroupListerBackendHandler {
|
||||
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 update_group(&self, request: UpdateGroupRequest) -> Result<()>;
|
||||
async fn create_group(&self, group_name: &str) -> Result<GroupId>;
|
||||
@ -136,12 +140,16 @@ pub trait GroupBackendHandler {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait UserBackendHandler {
|
||||
pub trait UserListerBackendHandler {
|
||||
async fn list_users(
|
||||
&self,
|
||||
filters: Option<UserRequestFilter>,
|
||||
get_groups: bool,
|
||||
) -> Result<Vec<UserAndGroups>>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait UserBackendHandler {
|
||||
async fn get_user_details(&self, user_id: &UserId) -> Result<User>;
|
||||
async fn create_user(&self, request: CreateUserRequest) -> Result<()>;
|
||||
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
|
||||
@ -152,7 +160,15 @@ pub trait UserBackendHandler {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait BackendHandler: Clone + Send + GroupBackendHandler + UserBackendHandler {}
|
||||
pub trait BackendHandler:
|
||||
Send
|
||||
+ Sync
|
||||
+ GroupBackendHandler
|
||||
+ UserBackendHandler
|
||||
+ UserListerBackendHandler
|
||||
+ GroupListerBackendHandler
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mockall::mock! {
|
||||
@ -161,16 +177,22 @@ mockall::mock! {
|
||||
fn clone(&self) -> Self;
|
||||
}
|
||||
#[async_trait]
|
||||
impl GroupBackendHandler for TestBackendHandler {
|
||||
impl GroupListerBackendHandler for TestBackendHandler {
|
||||
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 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 UserBackendHandler for TestBackendHandler {
|
||||
impl UserListerBackendHandler for TestBackendHandler {
|
||||
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 create_user(&self, request: CreateUserRequest) -> Result<()>;
|
||||
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
|
||||
|
@ -1,10 +1,10 @@
|
||||
use ldap3_proto::{
|
||||
proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry,
|
||||
};
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
use crate::domain::{
|
||||
handler::{BackendHandler, GroupRequestFilter},
|
||||
handler::{GroupListerBackendHandler, GroupRequestFilter},
|
||||
ldap::error::LdapError,
|
||||
types::{Group, GroupColumn, UserId, Uuid},
|
||||
};
|
||||
@ -21,7 +21,7 @@ pub fn get_group_attribute(
|
||||
group: &Group,
|
||||
base_dn_str: &str,
|
||||
attribute: &str,
|
||||
user_filter: &Option<&UserId>,
|
||||
user_filter: &Option<UserId>,
|
||||
ignored_group_attributes: &[String],
|
||||
) -> Option<Vec<Vec<u8>>> {
|
||||
let attribute = attribute.to_ascii_lowercase();
|
||||
@ -34,7 +34,7 @@ pub fn get_group_attribute(
|
||||
"member" | "uniquemember" => group
|
||||
.users
|
||||
.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())
|
||||
.collect(),
|
||||
"1.1" => return None,
|
||||
@ -81,7 +81,7 @@ fn make_ldap_search_group_result_entry(
|
||||
group: Group,
|
||||
base_dn_str: &str,
|
||||
attributes: &[String],
|
||||
user_filter: &Option<&UserId>,
|
||||
user_filter: &Option<UserId>,
|
||||
ignored_group_attributes: &[String],
|
||||
) -> LdapSearchResultEntry {
|
||||
let expanded_attributes = expand_group_attribute_wildcards(attributes);
|
||||
@ -201,25 +201,17 @@ fn convert_group_filter(
|
||||
}
|
||||
|
||||
#[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_filter: &LdapFilter,
|
||||
base: &str,
|
||||
user_filter: &Option<&UserId>,
|
||||
backend: &mut Backend,
|
||||
backend: &Backend,
|
||||
) -> LdapResult<Vec<Group>> {
|
||||
debug!(?ldap_filter);
|
||||
let filter = convert_group_filter(ldap_info, ldap_filter)?;
|
||||
let parsed_filters = match user_filter {
|
||||
None => filter,
|
||||
Some(u) => {
|
||||
info!("Unprivileged search, limiting results");
|
||||
GroupRequestFilter::And(vec![filter, GroupRequestFilter::Member((*u).clone())])
|
||||
}
|
||||
};
|
||||
debug!(?parsed_filters);
|
||||
let filters = convert_group_filter(ldap_info, ldap_filter)?;
|
||||
debug!(?filters);
|
||||
backend
|
||||
.list_groups(Some(parsed_filters))
|
||||
.list_groups(Some(filters))
|
||||
.await
|
||||
.map_err(|e| LdapError {
|
||||
code: LdapResultCode::Other,
|
||||
@ -231,7 +223,7 @@ pub fn convert_groups_to_ldap_op<'a>(
|
||||
groups: Vec<Group>,
|
||||
attributes: &'a [String],
|
||||
ldap_info: &'a LdapInfo,
|
||||
user_filter: &'a Option<&'a UserId>,
|
||||
user_filter: &'a Option<UserId>,
|
||||
) -> impl Iterator<Item = LdapOp> + 'a {
|
||||
groups.into_iter().map(move |g| {
|
||||
LdapOp::SearchResultEntry(make_ldap_search_group_result_entry(
|
||||
|
@ -2,10 +2,10 @@ use chrono::TimeZone;
|
||||
use ldap3_proto::{
|
||||
proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry,
|
||||
};
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
use crate::domain::{
|
||||
handler::{BackendHandler, UserRequestFilter},
|
||||
handler::{UserListerBackendHandler, UserRequestFilter},
|
||||
ldap::{
|
||||
error::LdapError,
|
||||
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")]
|
||||
pub async fn get_user_list<Backend: BackendHandler>(
|
||||
pub async fn get_user_list<Backend: UserListerBackendHandler>(
|
||||
ldap_info: &LdapInfo,
|
||||
ldap_filter: &LdapFilter,
|
||||
request_groups: bool,
|
||||
base: &str,
|
||||
user_filter: &Option<&UserId>,
|
||||
backend: &mut Backend,
|
||||
backend: &Backend,
|
||||
) -> LdapResult<Vec<UserAndGroups>> {
|
||||
debug!(?ldap_filter);
|
||||
let filters = convert_user_filter(ldap_info, ldap_filter)?;
|
||||
let parsed_filters = match user_filter {
|
||||
None => filters,
|
||||
Some(u) => {
|
||||
info!("Unprivileged search, limiting results");
|
||||
UserRequestFilter::And(vec![filters, UserRequestFilter::UserId((*u).clone())])
|
||||
}
|
||||
};
|
||||
debug!(?parsed_filters);
|
||||
debug!(?filters);
|
||||
backend
|
||||
.list_users(Some(parsed_filters), request_groups)
|
||||
.list_users(Some(filters), request_groups)
|
||||
.await
|
||||
.map_err(|e| LdapError {
|
||||
code: LdapResultCode::Other,
|
||||
|
@ -4,7 +4,7 @@ use async_trait::async_trait;
|
||||
pub use lldap_auth::{login, registration};
|
||||
|
||||
#[async_trait]
|
||||
pub trait OpaqueHandler: Clone + Send {
|
||||
pub trait OpaqueHandler: Send + Sync {
|
||||
async fn login_start(
|
||||
&self,
|
||||
request: login::ClientLoginStartRequest,
|
||||
|
@ -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 async_trait::async_trait;
|
||||
|
||||
@ -23,7 +23,8 @@ pub mod tests {
|
||||
use crate::{
|
||||
domain::{
|
||||
handler::{
|
||||
CreateUserRequest, GroupBackendHandler, UserBackendHandler, UserRequestFilter,
|
||||
CreateUserRequest, GroupBackendHandler, UserBackendHandler,
|
||||
UserListerBackendHandler, UserRequestFilter,
|
||||
},
|
||||
sql_tables::init_table,
|
||||
types::{GroupId, UserId},
|
||||
|
@ -1,6 +1,8 @@
|
||||
use crate::domain::{
|
||||
error::{DomainError, Result},
|
||||
handler::{GroupBackendHandler, GroupRequestFilter, UpdateGroupRequest},
|
||||
handler::{
|
||||
GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter, UpdateGroupRequest,
|
||||
},
|
||||
model::{self, GroupColumn, MembershipColumn},
|
||||
sql_backend_handler::SqlBackendHandler,
|
||||
types::{Group, GroupDetails, GroupId, Uuid},
|
||||
@ -57,7 +59,7 @@ fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GroupBackendHandler for SqlBackendHandler {
|
||||
impl GroupListerBackendHandler for SqlBackendHandler {
|
||||
#[instrument(skip_all, level = "debug", ret, err)]
|
||||
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>> {
|
||||
debug!(?filters);
|
||||
@ -94,7 +96,10 @@ impl GroupBackendHandler for SqlBackendHandler {
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GroupBackendHandler for SqlBackendHandler {
|
||||
#[instrument(skip_all, level = "debug", ret, err)]
|
||||
async fn get_group_details(&self, group_id: GroupId) -> Result<GroupDetails> {
|
||||
debug!(?group_id);
|
||||
|
@ -1,6 +1,9 @@
|
||||
use super::{
|
||||
use crate::domain::{
|
||||
error::{DomainError, Result},
|
||||
handler::{CreateUserRequest, UpdateUserRequest, UserBackendHandler, UserRequestFilter},
|
||||
handler::{
|
||||
CreateUserRequest, UpdateUserRequest, UserBackendHandler, UserListerBackendHandler,
|
||||
UserRequestFilter,
|
||||
},
|
||||
model::{self, GroupColumn, UserColumn},
|
||||
sql_backend_handler::SqlBackendHandler,
|
||||
types::{GroupDetails, GroupId, User, UserAndGroups, UserId, Uuid},
|
||||
@ -70,7 +73,7 @@ fn to_value(opt_name: &Option<String>) -> ActiveValue<Option<String>> {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserBackendHandler for SqlBackendHandler {
|
||||
impl UserListerBackendHandler for SqlBackendHandler {
|
||||
#[instrument(skip_all, level = "debug", ret, err)]
|
||||
async fn list_users(
|
||||
&self,
|
||||
@ -135,7 +138,10 @@ impl UserBackendHandler for SqlBackendHandler {
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserBackendHandler for SqlBackendHandler {
|
||||
#[instrument(skip_all, level = "debug", ret)]
|
||||
async fn get_user_details(&self, user_id: &UserId) -> Result<User> {
|
||||
debug!(?user_id);
|
||||
|
317
server/src/infra/access_control.rs
Normal file
317
server/src/infra/access_control.rs
Normal 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>
|
||||
{
|
||||
}
|
@ -30,6 +30,7 @@ use crate::{
|
||||
types::{GroupDetails, UserColumn, UserId},
|
||||
},
|
||||
infra::{
|
||||
access_control::{ReadonlyBackendHandler, UserReadableBackendHandler, ValidationResults},
|
||||
tcp_backend_handler::*,
|
||||
tcp_server::{error_to_http_response, AppState, TcpError, TcpResult},
|
||||
},
|
||||
@ -87,11 +88,10 @@ async fn get_refresh<Backend>(
|
||||
where
|
||||
Backend: TcpBackendHandler + BackendHandler + 'static,
|
||||
{
|
||||
let backend_handler = &data.backend_handler;
|
||||
let jwt_key = &data.jwt_key;
|
||||
let (refresh_token_hash, user) = get_refresh_token(request)?;
|
||||
let found = data
|
||||
.backend_handler
|
||||
.get_tcp_handler()
|
||||
.check_token(refresh_token_hash, &user)
|
||||
.await?;
|
||||
if !found {
|
||||
@ -99,7 +99,8 @@ where
|
||||
"Invalid refresh token".to_string(),
|
||||
)));
|
||||
}
|
||||
Ok(backend_handler
|
||||
Ok(data
|
||||
.get_readonly_handler()
|
||||
.get_user_groups(&user)
|
||||
.await
|
||||
.map(|groups| create_jwt(jwt_key, user.to_string(), groups))
|
||||
@ -145,7 +146,7 @@ where
|
||||
.get("user_id")
|
||||
.ok_or_else(|| TcpError::BadRequest("Missing user ID".to_string()))?;
|
||||
let user_results = data
|
||||
.backend_handler
|
||||
.get_readonly_handler()
|
||||
.list_users(
|
||||
Some(UserRequestFilter::Or(vec![
|
||||
UserRequestFilter::UserId(UserId::new(user_string)),
|
||||
@ -163,7 +164,7 @@ where
|
||||
}
|
||||
let user = &user_results[0].user;
|
||||
let token = match data
|
||||
.backend_handler
|
||||
.get_tcp_handler()
|
||||
.start_password_reset(&user.user_id)
|
||||
.await?
|
||||
{
|
||||
@ -216,7 +217,7 @@ where
|
||||
.get("token")
|
||||
.ok_or_else(|| TcpError::BadRequest("Missing reset token".to_owned()))?;
|
||||
let user_id = data
|
||||
.backend_handler
|
||||
.get_tcp_handler()
|
||||
.get_user_id_for_password_reset_token(token)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@ -224,7 +225,7 @@ where
|
||||
TcpError::NotFoundError("Wrong or expired reset token".to_owned())
|
||||
})?;
|
||||
let _ = data
|
||||
.backend_handler
|
||||
.get_tcp_handler()
|
||||
.delete_password_reset_token(token)
|
||||
.await;
|
||||
let groups = HashSet::new();
|
||||
@ -266,10 +267,10 @@ where
|
||||
Backend: TcpBackendHandler + BackendHandler + 'static,
|
||||
{
|
||||
let (refresh_token_hash, user) = get_refresh_token(request)?;
|
||||
data.backend_handler
|
||||
data.get_tcp_handler()
|
||||
.delete_refresh_token(refresh_token_hash)
|
||||
.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();
|
||||
for jwt in new_blacklisted_jwts {
|
||||
jwt_blacklist.insert(jwt);
|
||||
@ -320,7 +321,7 @@ async fn opaque_login_start<Backend>(
|
||||
where
|
||||
Backend: OpaqueHandler + 'static,
|
||||
{
|
||||
data.backend_handler
|
||||
data.get_opaque_handler()
|
||||
.login_start(request.into_inner())
|
||||
.await
|
||||
.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
|
||||
// token.
|
||||
let groups = data.backend_handler.get_user_groups(name).await?;
|
||||
let (refresh_token, max_age) = data.backend_handler.create_refresh_token(name).await?;
|
||||
let groups = data.get_readonly_handler().get_user_groups(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 refresh_token_plus_name = refresh_token + "+" + name.as_str();
|
||||
|
||||
@ -374,7 +375,7 @@ where
|
||||
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static,
|
||||
{
|
||||
let name = data
|
||||
.backend_handler
|
||||
.get_opaque_handler()
|
||||
.login_finish(request.into_inner())
|
||||
.await?;
|
||||
get_login_successful_response(&data, &name).await
|
||||
@ -405,7 +406,7 @@ where
|
||||
name: user_id.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
|
||||
}
|
||||
|
||||
@ -431,7 +432,7 @@ where
|
||||
{
|
||||
let name = request.name.clone();
|
||||
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
|
||||
}
|
||||
|
||||
@ -474,7 +475,7 @@ where
|
||||
.into_inner();
|
||||
let user_id = UserId::new(®istration_start_request.username);
|
||||
let user_is_admin = data
|
||||
.backend_handler
|
||||
.get_readonly_handler()
|
||||
.get_user_groups(&user_id)
|
||||
.await?
|
||||
.iter()
|
||||
@ -485,7 +486,7 @@ where
|
||||
));
|
||||
}
|
||||
Ok(data
|
||||
.backend_handler
|
||||
.get_opaque_handler()
|
||||
.registration_start(registration_start_request)
|
||||
.await?)
|
||||
}
|
||||
@ -512,7 +513,7 @@ async fn opaque_register_finish<Backend>(
|
||||
where
|
||||
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static,
|
||||
{
|
||||
data.backend_handler
|
||||
data.get_opaque_handler()
|
||||
.registration_finish(request.into_inner())
|
||||
.await?;
|
||||
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)]
|
||||
pub(crate) fn check_if_token_is_valid<Backend>(
|
||||
pub(crate) fn check_if_token_is_valid<Backend: BackendHandler>(
|
||||
state: &AppState<Backend>,
|
||||
token_str: &str,
|
||||
) -> 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) {
|
||||
return Err(ErrorUnauthorized("JWT was logged out"));
|
||||
}
|
||||
let is_in_group = |name| token.claims().groups.contains(name);
|
||||
Ok(ValidationResults {
|
||||
user: UserId::new(&token.claims().user),
|
||||
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
|
||||
},
|
||||
})
|
||||
Ok(state.backend_handler.get_permissions_from_groups(
|
||||
UserId::new(&token.claims().user),
|
||||
token.claims().groups.iter(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn configure_server<Backend>(cfg: &mut web::ServiceConfig, enable_password_reset: bool)
|
||||
|
@ -1,29 +1,77 @@
|
||||
use crate::{
|
||||
domain::handler::BackendHandler,
|
||||
domain::{handler::BackendHandler, types::UserId},
|
||||
infra::{
|
||||
auth_service::{check_if_token_is_valid, ValidationResults},
|
||||
access_control::{
|
||||
AccessControlledBackendHandler, AdminBackendHandler, ReadonlyBackendHandler,
|
||||
UserReadableBackendHandler, UserWriteableBackendHandler, ValidationResults,
|
||||
},
|
||||
auth_service::check_if_token_is_valid,
|
||||
cli::ExportGraphQLSchemaOpts,
|
||||
graphql::{mutation::Mutation, query::Query},
|
||||
tcp_server::AppState,
|
||||
},
|
||||
};
|
||||
use actix_web::{web, Error, HttpResponse};
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
use juniper::{EmptySubscription, RootNode};
|
||||
use juniper::{EmptySubscription, FieldError, RootNode};
|
||||
use juniper_actix::{graphiql_handler, graphql_handler, playground_handler};
|
||||
|
||||
use super::{mutation::Mutation, query::Query};
|
||||
use tracing::debug;
|
||||
|
||||
pub struct Context<Handler: BackendHandler> {
|
||||
pub handler: Box<Handler>,
|
||||
pub handler: AccessControlledBackendHandler<Handler>,
|
||||
pub validation_result: ValidationResults,
|
||||
}
|
||||
|
||||
pub fn field_error_callback<'a>(
|
||||
span: &'a tracing::Span,
|
||||
error_message: &'a str,
|
||||
) -> impl 'a + FnOnce() -> FieldError {
|
||||
move || {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
FieldError::from(error_message)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> Context<Handler> {
|
||||
#[cfg(test)]
|
||||
pub fn new_for_tests(handler: Handler, validation_result: ValidationResults) -> Self {
|
||||
Self {
|
||||
handler: AccessControlledBackendHandler::new(handler),
|
||||
validation_result,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_admin_handler(&self) -> Option<&impl AdminBackendHandler> {
|
||||
self.handler.get_admin_handler(&self.validation_result)
|
||||
}
|
||||
|
||||
pub fn get_readonly_handler(&self) -> Option<&impl ReadonlyBackendHandler> {
|
||||
self.handler.get_readonly_handler(&self.validation_result)
|
||||
}
|
||||
|
||||
pub fn get_writeable_handler(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
) -> Option<&impl UserWriteableBackendHandler> {
|
||||
self.handler
|
||||
.get_writeable_handler(&self.validation_result, user_id)
|
||||
}
|
||||
|
||||
pub fn get_readable_handler(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
) -> Option<&impl UserReadableBackendHandler> {
|
||||
self.handler
|
||||
.get_readable_handler(&self.validation_result, user_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Handler: BackendHandler> juniper::Context for Context<Handler> {}
|
||||
|
||||
type Schema<Handler> =
|
||||
RootNode<'static, Query<Handler>, Mutation<Handler>, EmptySubscription<Context<Handler>>>;
|
||||
|
||||
fn schema<Handler: BackendHandler + Sync>() -> Schema<Handler> {
|
||||
fn schema<Handler: BackendHandler>() -> Schema<Handler> {
|
||||
Schema::new(
|
||||
Query::<Handler>::new(),
|
||||
Mutation::<Handler>::new(),
|
||||
@ -58,7 +106,7 @@ async fn playground_route() -> Result<HttpResponse, Error> {
|
||||
playground_handler("/api/graphql", None).await
|
||||
}
|
||||
|
||||
async fn graphql_route<Handler: BackendHandler + Sync>(
|
||||
async fn graphql_route<Handler: BackendHandler + Clone>(
|
||||
req: actix_web::HttpRequest,
|
||||
mut payload: actix_web::web::Payload,
|
||||
data: web::Data<AppState<Handler>>,
|
||||
@ -67,7 +115,7 @@ async fn graphql_route<Handler: BackendHandler + Sync>(
|
||||
let bearer = BearerAuth::from_request(&req, &mut payload.0).await?;
|
||||
let validation_result = check_if_token_is_valid(&data, bearer.token())?;
|
||||
let context = Context::<Handler> {
|
||||
handler: Box::new(data.backend_handler.clone()),
|
||||
handler: data.backend_handler.clone(),
|
||||
validation_result,
|
||||
};
|
||||
graphql_handler(&schema(), &context, req, payload).await
|
||||
@ -75,7 +123,7 @@ async fn graphql_route<Handler: BackendHandler + Sync>(
|
||||
|
||||
pub fn configure_endpoint<Backend>(cfg: &mut web::ServiceConfig)
|
||||
where
|
||||
Backend: BackendHandler + Sync + 'static,
|
||||
Backend: BackendHandler + Clone + 'static,
|
||||
{
|
||||
let json_config = web::JsonConfig::default()
|
||||
.limit(4096)
|
||||
|
@ -1,6 +1,15 @@
|
||||
use crate::domain::{
|
||||
handler::{BackendHandler, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest},
|
||||
types::{GroupId, JpegPhoto, UserId},
|
||||
use crate::{
|
||||
domain::{
|
||||
handler::{BackendHandler, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest},
|
||||
types::{GroupId, JpegPhoto, UserId},
|
||||
},
|
||||
infra::{
|
||||
access_control::{
|
||||
AdminBackendHandler, ReadonlyBackendHandler, UserReadableBackendHandler,
|
||||
UserWriteableBackendHandler,
|
||||
},
|
||||
graphql::api::field_error_callback,
|
||||
},
|
||||
};
|
||||
use anyhow::Context as AnyhowContext;
|
||||
use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject};
|
||||
@ -65,19 +74,18 @@ impl Success {
|
||||
}
|
||||
|
||||
#[graphql_object(context = Context<Handler>)]
|
||||
impl<Handler: BackendHandler + Sync> Mutation<Handler> {
|
||||
impl<Handler: BackendHandler> Mutation<Handler> {
|
||||
async fn create_user(
|
||||
context: &Context<Handler>,
|
||||
user: CreateUserInput,
|
||||
) -> FieldResult<super::query::User<Handler>> {
|
||||
let span = debug_span!("[GraphQL mutation] create_user");
|
||||
span.in_scope(|| {
|
||||
debug!(?user.id);
|
||||
debug!("{:?}", &user.id);
|
||||
});
|
||||
if !context.validation_result.is_admin() {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
return Err("Unauthorized user creation".into());
|
||||
}
|
||||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(&span, "Unauthorized user creation"))?;
|
||||
let user_id = UserId::new(&user.id);
|
||||
let avatar = user
|
||||
.avatar
|
||||
@ -87,8 +95,7 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
|
||||
.map(JpegPhoto::try_from)
|
||||
.transpose()
|
||||
.context("Provided image is not a valid JPEG")?;
|
||||
context
|
||||
.handler
|
||||
handler
|
||||
.create_user(CreateUserRequest {
|
||||
user_id: user_id.clone(),
|
||||
email: user.email,
|
||||
@ -99,8 +106,7 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
|
||||
})
|
||||
.instrument(span.clone())
|
||||
.await?;
|
||||
Ok(context
|
||||
.handler
|
||||
Ok(handler
|
||||
.get_user_details(&user_id)
|
||||
.instrument(span)
|
||||
.await
|
||||
@ -115,13 +121,11 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
|
||||
span.in_scope(|| {
|
||||
debug!(?name);
|
||||
});
|
||||
if !context.validation_result.is_admin() {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
return Err("Unauthorized group creation".into());
|
||||
}
|
||||
let group_id = context.handler.create_group(&name).await?;
|
||||
Ok(context
|
||||
.handler
|
||||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(&span, "Unauthorized group creation"))?;
|
||||
let group_id = handler.create_group(&name).await?;
|
||||
Ok(handler
|
||||
.get_group_details(group_id)
|
||||
.instrument(span)
|
||||
.await
|
||||
@ -137,10 +141,9 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
|
||||
debug!(?user.id);
|
||||
});
|
||||
let user_id = UserId::new(&user.id);
|
||||
if !context.validation_result.can_write(&user_id) {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
return Err("Unauthorized user update".into());
|
||||
}
|
||||
let handler = context
|
||||
.get_writeable_handler(&user_id)
|
||||
.ok_or_else(field_error_callback(&span, "Unauthorized user update"))?;
|
||||
let avatar = user
|
||||
.avatar
|
||||
.map(base64::decode)
|
||||
@ -149,8 +152,7 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
|
||||
.map(JpegPhoto::try_from)
|
||||
.transpose()
|
||||
.context("Provided image is not a valid JPEG")?;
|
||||
context
|
||||
.handler
|
||||
handler
|
||||
.update_user(UpdateUserRequest {
|
||||
user_id,
|
||||
email: user.email,
|
||||
@ -172,16 +174,14 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
|
||||
span.in_scope(|| {
|
||||
debug!(?group.id);
|
||||
});
|
||||
if !context.validation_result.is_admin() {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
return Err("Unauthorized group update".into());
|
||||
}
|
||||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(&span, "Unauthorized group update"))?;
|
||||
if group.id == 1 {
|
||||
span.in_scope(|| debug!("Cannot change admin group details"));
|
||||
return Err("Cannot change admin group details".into());
|
||||
}
|
||||
context
|
||||
.handler
|
||||
handler
|
||||
.update_group(UpdateGroupRequest {
|
||||
group_id: GroupId(group.id),
|
||||
display_name: group.display_name,
|
||||
@ -200,12 +200,13 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
|
||||
span.in_scope(|| {
|
||||
debug!(?user_id, ?group_id);
|
||||
});
|
||||
if !context.validation_result.is_admin() {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
return Err("Unauthorized group membership modification".into());
|
||||
}
|
||||
context
|
||||
.handler
|
||||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(
|
||||
&span,
|
||||
"Unauthorized group membership modification",
|
||||
))?;
|
||||
handler
|
||||
.add_user_to_group(&UserId::new(&user_id), GroupId(group_id))
|
||||
.instrument(span)
|
||||
.await?;
|
||||
@ -221,17 +222,18 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
|
||||
span.in_scope(|| {
|
||||
debug!(?user_id, ?group_id);
|
||||
});
|
||||
if !context.validation_result.is_admin() {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
return Err("Unauthorized group membership modification".into());
|
||||
}
|
||||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(
|
||||
&span,
|
||||
"Unauthorized group membership modification",
|
||||
))?;
|
||||
let user_id = UserId::new(&user_id);
|
||||
if context.validation_result.user == user_id && group_id == 1 {
|
||||
span.in_scope(|| debug!("Cannot remove admin rights for current user"));
|
||||
return Err("Cannot remove admin rights for current user".into());
|
||||
}
|
||||
context
|
||||
.handler
|
||||
handler
|
||||
.remove_user_from_group(&user_id, GroupId(group_id))
|
||||
.instrument(span)
|
||||
.await?;
|
||||
@ -244,19 +246,14 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
|
||||
debug!(?user_id);
|
||||
});
|
||||
let user_id = UserId::new(&user_id);
|
||||
if !context.validation_result.is_admin() {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
return Err("Unauthorized user deletion".into());
|
||||
}
|
||||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(&span, "Unauthorized user deletion"))?;
|
||||
if context.validation_result.user == user_id {
|
||||
span.in_scope(|| debug!("Cannot delete current user"));
|
||||
return Err("Cannot delete current user".into());
|
||||
}
|
||||
context
|
||||
.handler
|
||||
.delete_user(&user_id)
|
||||
.instrument(span)
|
||||
.await?;
|
||||
handler.delete_user(&user_id).instrument(span).await?;
|
||||
Ok(Success::new())
|
||||
}
|
||||
|
||||
@ -265,16 +262,14 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
|
||||
span.in_scope(|| {
|
||||
debug!(?group_id);
|
||||
});
|
||||
if !context.validation_result.is_admin() {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
return Err("Unauthorized group deletion".into());
|
||||
}
|
||||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(&span, "Unauthorized group deletion"))?;
|
||||
if group_id == 1 {
|
||||
span.in_scope(|| debug!("Cannot delete admin group"));
|
||||
return Err("Cannot delete admin group".into());
|
||||
}
|
||||
context
|
||||
.handler
|
||||
handler
|
||||
.delete_group(GroupId(group_id))
|
||||
.instrument(span)
|
||||
.await?;
|
||||
|
@ -1,7 +1,13 @@
|
||||
use crate::domain::{
|
||||
handler::BackendHandler,
|
||||
ldap::utils::map_user_field,
|
||||
types::{GroupDetails, GroupId, UserColumn, UserId},
|
||||
use crate::{
|
||||
domain::{
|
||||
handler::BackendHandler,
|
||||
ldap::utils::map_user_field,
|
||||
types::{GroupDetails, GroupId, UserColumn, UserId},
|
||||
},
|
||||
infra::{
|
||||
access_control::{ReadonlyBackendHandler, UserReadableBackendHandler},
|
||||
graphql::api::field_error_callback,
|
||||
},
|
||||
};
|
||||
use chrono::TimeZone;
|
||||
use juniper::{graphql_object, FieldResult, GraphQLInputObject};
|
||||
@ -112,7 +118,7 @@ impl<Handler: BackendHandler> Query<Handler> {
|
||||
}
|
||||
|
||||
#[graphql_object(context = Context<Handler>)]
|
||||
impl<Handler: BackendHandler + Sync> Query<Handler> {
|
||||
impl<Handler: BackendHandler> Query<Handler> {
|
||||
fn api_version() -> &'static str {
|
||||
"1.0"
|
||||
}
|
||||
@ -123,12 +129,13 @@ impl<Handler: BackendHandler + Sync> Query<Handler> {
|
||||
debug!(?user_id);
|
||||
});
|
||||
let user_id = UserId::new(&user_id);
|
||||
if !context.validation_result.can_read(&user_id) {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
return Err("Unauthorized access to user data".into());
|
||||
}
|
||||
Ok(context
|
||||
.handler
|
||||
let handler = context
|
||||
.get_readable_handler(&user_id)
|
||||
.ok_or_else(field_error_callback(
|
||||
&span,
|
||||
"Unauthorized access to user data",
|
||||
))?;
|
||||
Ok(handler
|
||||
.get_user_details(&user_id)
|
||||
.instrument(span)
|
||||
.await
|
||||
@ -143,12 +150,13 @@ impl<Handler: BackendHandler + Sync> Query<Handler> {
|
||||
span.in_scope(|| {
|
||||
debug!(?filters);
|
||||
});
|
||||
if !context.validation_result.is_admin_or_readonly() {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
return Err("Unauthorized access to user list".into());
|
||||
}
|
||||
Ok(context
|
||||
.handler
|
||||
let handler = context
|
||||
.get_readonly_handler()
|
||||
.ok_or_else(field_error_callback(
|
||||
&span,
|
||||
"Unauthorized access to user list",
|
||||
))?;
|
||||
Ok(handler
|
||||
.list_users(filters.map(TryInto::try_into).transpose()?, false)
|
||||
.instrument(span)
|
||||
.await
|
||||
@ -157,12 +165,13 @@ impl<Handler: BackendHandler + Sync> Query<Handler> {
|
||||
|
||||
async fn groups(context: &Context<Handler>) -> FieldResult<Vec<Group<Handler>>> {
|
||||
let span = debug_span!("[GraphQL query] groups");
|
||||
if !context.validation_result.is_admin_or_readonly() {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
return Err("Unauthorized access to group list".into());
|
||||
}
|
||||
Ok(context
|
||||
.handler
|
||||
let handler = context
|
||||
.get_readonly_handler()
|
||||
.ok_or_else(field_error_callback(
|
||||
&span,
|
||||
"Unauthorized access to group list",
|
||||
))?;
|
||||
Ok(handler
|
||||
.list_groups(None)
|
||||
.instrument(span)
|
||||
.await
|
||||
@ -174,12 +183,13 @@ impl<Handler: BackendHandler + Sync> Query<Handler> {
|
||||
span.in_scope(|| {
|
||||
debug!(?group_id);
|
||||
});
|
||||
if !context.validation_result.is_admin_or_readonly() {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
return Err("Unauthorized access to group data".into());
|
||||
}
|
||||
Ok(context
|
||||
.handler
|
||||
let handler = context
|
||||
.get_readonly_handler()
|
||||
.ok_or_else(field_error_callback(
|
||||
&span,
|
||||
"Unauthorized access to group data",
|
||||
))?;
|
||||
Ok(handler
|
||||
.get_group_details(GroupId(group_id))
|
||||
.instrument(span)
|
||||
.await
|
||||
@ -205,7 +215,7 @@ impl<Handler: BackendHandler> Default for User<Handler> {
|
||||
}
|
||||
|
||||
#[graphql_object(context = Context<Handler>)]
|
||||
impl<Handler: BackendHandler + Sync> User<Handler> {
|
||||
impl<Handler: BackendHandler> User<Handler> {
|
||||
fn id(&self) -> &str {
|
||||
self.user.user_id.as_str()
|
||||
}
|
||||
@ -244,8 +254,10 @@ impl<Handler: BackendHandler + Sync> User<Handler> {
|
||||
span.in_scope(|| {
|
||||
debug!(user_id = ?self.user.user_id);
|
||||
});
|
||||
Ok(context
|
||||
.handler
|
||||
let handler = context
|
||||
.get_readable_handler(&self.user.user_id)
|
||||
.expect("We shouldn't be able to get there without readable permission");
|
||||
Ok(handler
|
||||
.get_user_groups(&self.user.user_id)
|
||||
.instrument(span)
|
||||
.await
|
||||
@ -283,7 +295,7 @@ pub struct Group<Handler: BackendHandler> {
|
||||
}
|
||||
|
||||
#[graphql_object(context = Context<Handler>)]
|
||||
impl<Handler: BackendHandler + Sync> Group<Handler> {
|
||||
impl<Handler: BackendHandler> Group<Handler> {
|
||||
fn id(&self) -> i32 {
|
||||
self.group_id
|
||||
}
|
||||
@ -302,12 +314,13 @@ impl<Handler: BackendHandler + Sync> Group<Handler> {
|
||||
span.in_scope(|| {
|
||||
debug!(name = %self.display_name);
|
||||
});
|
||||
if !context.validation_result.is_admin_or_readonly() {
|
||||
span.in_scope(|| debug!("Unauthorized"));
|
||||
return Err("Unauthorized access to group data".into());
|
||||
}
|
||||
Ok(context
|
||||
.handler
|
||||
let handler = context
|
||||
.get_readonly_handler()
|
||||
.ok_or_else(field_error_callback(
|
||||
&span,
|
||||
"Unauthorized access to group data",
|
||||
))?;
|
||||
Ok(handler
|
||||
.list_users(
|
||||
Some(DomainRequestFilter::MemberOfId(GroupId(self.group_id))),
|
||||
false,
|
||||
@ -347,7 +360,9 @@ impl<Handler: BackendHandler> From<DomainGroup> for Group<Handler> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{domain::handler::MockTestBackendHandler, infra::auth_service::ValidationResults};
|
||||
use crate::{
|
||||
domain::handler::MockTestBackendHandler, infra::access_control::ValidationResults,
|
||||
};
|
||||
use chrono::TimeZone;
|
||||
use juniper::{
|
||||
execute, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType,
|
||||
@ -406,10 +421,8 @@ mod tests {
|
||||
.with(eq(UserId::new("bob")))
|
||||
.return_once(|_| Ok(groups));
|
||||
|
||||
let context = Context::<MockTestBackendHandler> {
|
||||
handler: Box::new(mock),
|
||||
validation_result: ValidationResults::admin(),
|
||||
};
|
||||
let context =
|
||||
Context::<MockTestBackendHandler>::new_for_tests(mock, ValidationResults::admin());
|
||||
|
||||
let schema = schema(Query::<MockTestBackendHandler>::new());
|
||||
assert_eq!(
|
||||
@ -486,10 +499,8 @@ mod tests {
|
||||
])
|
||||
});
|
||||
|
||||
let context = Context::<MockTestBackendHandler> {
|
||||
handler: Box::new(mock),
|
||||
validation_result: ValidationResults::admin(),
|
||||
};
|
||||
let context =
|
||||
Context::<MockTestBackendHandler>::new_for_tests(mock, ValidationResults::admin());
|
||||
|
||||
let schema = schema(Query::<MockTestBackendHandler>::new());
|
||||
assert_eq!(
|
||||
|
@ -12,7 +12,10 @@ use crate::{
|
||||
opaque_handler::OpaqueHandler,
|
||||
types::{Group, JpegPhoto, UserAndGroups, UserId},
|
||||
},
|
||||
infra::auth_service::{Permission, ValidationResults},
|
||||
infra::access_control::{
|
||||
AccessControlledBackendHandler, AdminBackendHandler, UserAndGroupListerBackendHandler,
|
||||
UserReadableBackendHandler, ValidationResults,
|
||||
},
|
||||
};
|
||||
use anyhow::Result;
|
||||
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>,
|
||||
backend_handler: Backend,
|
||||
backend_handler: AccessControlledBackendHandler<Backend>,
|
||||
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> {
|
||||
pub fn new(
|
||||
backend_handler: Backend,
|
||||
backend_handler: AccessControlledBackendHandler<Backend>,
|
||||
mut ldap_base_dn: String,
|
||||
ignored_user_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")]
|
||||
pub async fn do_bind(&mut self, request: &LdapBindRequest) -> (LdapResultCode, String) {
|
||||
debug!("DN: {}", &request.dn);
|
||||
@ -219,7 +244,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
};
|
||||
let LdapBindCred::Simple(password) = &request.cred;
|
||||
match self
|
||||
.backend_handler
|
||||
.get_login_handler()
|
||||
.bind(BindRequest {
|
||||
name: user_id.clone(),
|
||||
password: password.clone(),
|
||||
@ -227,25 +252,11 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
let user_groups = self.backend_handler.get_user_groups(&user_id).await;
|
||||
let is_in_group = |name| {
|
||||
user_groups
|
||||
.as_ref()
|
||||
.map(|groups| groups.iter().any(|g| g.display_name == name))
|
||||
.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
|
||||
},
|
||||
});
|
||||
self.user_info = self
|
||||
.backend_handler
|
||||
.get_permissions_for_user(user_id)
|
||||
.await
|
||||
.ok();
|
||||
debug!("Success!");
|
||||
(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::*;
|
||||
let mut rng = rand::rngs::OsRng;
|
||||
let registration_start_request =
|
||||
@ -262,7 +278,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
username: user.to_string(),
|
||||
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(
|
||||
registration_start_request.state,
|
||||
registration_start_response.registration_response,
|
||||
@ -272,7 +288,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
server_data: registration_start_response.server_data,
|
||||
registration_upload: registration_finish.message,
|
||||
};
|
||||
self.backend_handler.registration_finish(req).await?;
|
||||
backend_handler.registration_finish(req).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -294,6 +310,8 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
Ok(uid) => {
|
||||
let user_is_admin = self
|
||||
.backend_handler
|
||||
.get_readable_handler(credentials, &uid)
|
||||
.expect("Unexpected permission error")
|
||||
.get_user_groups(&uid)
|
||||
.await
|
||||
.map_err(|e| LdapError {
|
||||
@ -313,7 +331,10 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
&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 {
|
||||
code: LdapResultCode::Other,
|
||||
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(
|
||||
&mut self,
|
||||
request: &LdapSearchRequest,
|
||||
@ -382,22 +391,22 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
}
|
||||
|
||||
async fn do_search_internal(
|
||||
&mut self,
|
||||
&self,
|
||||
backend_handler: &impl UserAndGroupListerBackendHandler,
|
||||
request: &LdapSearchRequest,
|
||||
user_filter: &Option<&UserId>,
|
||||
) -> LdapResult<(Option<Vec<UserAndGroups>>, Option<Vec<Group>>)> {
|
||||
let dn_parts = parse_distinguished_name(&request.base.to_ascii_lowercase())?;
|
||||
let scope = get_search_scope(&self.ldap_info.base_dn, &dn_parts);
|
||||
debug!(?request.base, ?scope);
|
||||
// Disambiguate the lifetimes.
|
||||
fn cast<'a, T, R, B: 'a>(x: T) -> T
|
||||
fn cast<'a, T, R>(x: T) -> T
|
||||
where
|
||||
T: Fn(&'a mut B, &'a LdapFilter) -> R + 'a,
|
||||
T: Fn(&'a LdapFilter) -> R + 'a,
|
||||
{
|
||||
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
|
||||
.attrs
|
||||
.iter()
|
||||
@ -407,47 +416,27 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
filter,
|
||||
need_groups,
|
||||
&request.base,
|
||||
user_filter,
|
||||
backend_handler,
|
||||
)
|
||||
.await
|
||||
});
|
||||
let get_group_list = cast(|backend_handler: &mut Backend, filter: &LdapFilter| async {
|
||||
get_groups_list(
|
||||
&self.ldap_info,
|
||||
filter,
|
||||
&request.base,
|
||||
user_filter,
|
||||
backend_handler,
|
||||
)
|
||||
.await
|
||||
let get_group_list = cast(|filter: &LdapFilter| async {
|
||||
get_groups_list(&self.ldap_info, filter, &request.base, backend_handler).await
|
||||
});
|
||||
Ok(match scope {
|
||||
SearchScope::Global => (
|
||||
Some(get_user_list(&mut self.backend_handler, &request.filter).await?),
|
||||
Some(get_group_list(&mut self.backend_handler, &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?),
|
||||
Some(get_user_list(&request.filter).await?),
|
||||
Some(get_group_list(&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) => {
|
||||
let filter = LdapFilter::And(vec![request.filter.clone(), filter]);
|
||||
(
|
||||
Some(get_user_list(&mut self.backend_handler, &filter).await?),
|
||||
None,
|
||||
)
|
||||
(Some(get_user_list(&filter).await?), None)
|
||||
}
|
||||
SearchScope::Group(filter) => {
|
||||
let filter = LdapFilter::And(vec![request.filter.clone(), filter]);
|
||||
(
|
||||
None,
|
||||
Some(get_group_list(&mut self.backend_handler, &filter).await?),
|
||||
)
|
||||
(None, Some(get_group_list(&filter).await?))
|
||||
}
|
||||
SearchScope::Unknown => {
|
||||
warn!(
|
||||
@ -468,10 +457,15 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
}
|
||||
|
||||
#[instrument(skip_all, level = "debug")]
|
||||
pub async fn do_search(&mut self, request: &LdapSearchRequest) -> LdapResult<Vec<LdapOp>> {
|
||||
let user_filter = self.get_user_permission_filter()?;
|
||||
let user_filter = user_filter.as_ref();
|
||||
let (users, groups) = self.do_search_internal(request, &user_filter).await?;
|
||||
pub async fn do_search(&self, request: &LdapSearchRequest) -> LdapResult<Vec<LdapOp>> {
|
||||
let user_info = self.user_info.as_ref().ok_or_else(|| LdapError {
|
||||
code: LdapResultCode::InsufficentAccessRights,
|
||||
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();
|
||||
if let Some(users) = users {
|
||||
@ -486,7 +480,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
groups,
|
||||
&request.attrs,
|
||||
&self.ldap_info,
|
||||
&user_filter,
|
||||
&backend_handler.user_filter,
|
||||
));
|
||||
}
|
||||
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>> {
|
||||
if !self
|
||||
let backend_handler = self
|
||||
.user_info
|
||||
.as_ref()
|
||||
.map(|u| u.is_admin())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(LdapError {
|
||||
.and_then(|u| self.backend_handler.get_admin_handler(u))
|
||||
.ok_or_else(|| LdapError {
|
||||
code: LdapResultCode::InsufficentAccessRights,
|
||||
message: "Unauthorized write".to_string(),
|
||||
});
|
||||
}
|
||||
})?;
|
||||
let user_id = get_user_id_from_distinguished_name(
|
||||
&request.dn,
|
||||
&self.ldap_info.base_dn,
|
||||
@ -552,7 +543,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
.map(Vec::as_slice)
|
||||
.map(decode_attribute_value)
|
||||
};
|
||||
self.backend_handler
|
||||
backend_handler
|
||||
.create_user(CreateUserRequest {
|
||||
user_id,
|
||||
email: get_attribute("mail")
|
||||
@ -693,16 +684,22 @@ mod tests {
|
||||
async fn bind(&self, request: BindRequest) -> Result<()>;
|
||||
}
|
||||
#[async_trait]
|
||||
impl GroupBackendHandler for TestBackendHandler {
|
||||
impl GroupListerBackendHandler for TestBackendHandler {
|
||||
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 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 UserBackendHandler for TestBackendHandler {
|
||||
impl UserListerBackendHandler for TestBackendHandler {
|
||||
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 create_user(&self, request: CreateUserRequest) -> Result<()>;
|
||||
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
|
||||
@ -768,8 +765,7 @@ mod tests {
|
||||
});
|
||||
Ok(set)
|
||||
});
|
||||
let mut ldap_handler =
|
||||
LdapHandler::new(mock, "dc=Example,dc=com".to_string(), vec![], vec![]);
|
||||
let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=Example,dc=com");
|
||||
let request = LdapBindRequest {
|
||||
dn: "uid=test,ou=people,dc=example,dc=coM".to_string(),
|
||||
cred: LdapBindCred::Simple("pass".to_string()),
|
||||
@ -812,8 +808,7 @@ mod tests {
|
||||
mock.expect_get_user_groups()
|
||||
.with(eq(UserId::new("bob")))
|
||||
.return_once(|_| Ok(HashSet::new()));
|
||||
let mut ldap_handler =
|
||||
LdapHandler::new(mock, "dc=eXample,dc=com".to_string(), vec![], vec![]);
|
||||
let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=eXample,dc=com");
|
||||
|
||||
let request = LdapOp::BindRequest(LdapBindRequest {
|
||||
dn: "uid=bob,ou=people,dc=example,dc=com".to_string(),
|
||||
@ -855,8 +850,7 @@ mod tests {
|
||||
});
|
||||
Ok(set)
|
||||
});
|
||||
let mut ldap_handler =
|
||||
LdapHandler::new(mock, "dc=example,dc=com".to_string(), vec![], vec![]);
|
||||
let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=example,dc=com");
|
||||
|
||||
let request = LdapBindRequest {
|
||||
dn: "uid=test,ou=people,dc=example,dc=com".to_string(),
|
||||
@ -997,8 +991,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_bind_invalid_dn() {
|
||||
let mock = MockTestBackendHandler::new();
|
||||
let mut ldap_handler =
|
||||
LdapHandler::new(mock, "dc=example,dc=com".to_string(), vec![], vec![]);
|
||||
let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=example,dc=com");
|
||||
|
||||
let request = LdapBindRequest {
|
||||
dn: "cn=bob,dc=example,dc=com".to_string(),
|
||||
|
@ -3,7 +3,10 @@ use crate::{
|
||||
handler::{BackendHandler, LoginHandler},
|
||||
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_server::ServerBuilder;
|
||||
@ -73,7 +76,7 @@ where
|
||||
let mut resp = FramedWrite::new(w, LdapCodec);
|
||||
|
||||
let mut session = LdapHandler::new(
|
||||
backend_handler,
|
||||
AccessControlledBackendHandler::new(backend_handler),
|
||||
ldap_base_dn,
|
||||
ignored_user_attributes,
|
||||
ignored_group_attributes,
|
||||
@ -145,7 +148,7 @@ pub fn build_ldap_server<Backend>(
|
||||
server_builder: ServerBuilder,
|
||||
) -> Result<ServerBuilder>
|
||||
where
|
||||
Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static,
|
||||
Backend: BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static,
|
||||
{
|
||||
let context = (
|
||||
backend_handler,
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub mod access_control;
|
||||
pub mod auth_service;
|
||||
pub mod cli;
|
||||
pub mod configuration;
|
||||
|
@ -4,7 +4,7 @@ use std::collections::HashSet;
|
||||
use crate::domain::{error::Result, types::UserId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait TcpBackendHandler {
|
||||
pub trait TcpBackendHandler: Sync {
|
||||
async fn get_jwt_blacklist(&self) -> anyhow::Result<HashSet<u64>>;
|
||||
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>;
|
||||
@ -34,16 +34,22 @@ mockall::mock! {
|
||||
async fn bind(&self, request: BindRequest) -> Result<()>;
|
||||
}
|
||||
#[async_trait]
|
||||
impl GroupBackendHandler for TestTcpBackendHandler {
|
||||
impl GroupListerBackendHandler for TestTcpBackendHandler {
|
||||
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 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 UserBackendHandler for TestBackendHandler {
|
||||
impl UserListerBackendHandler for TestBackendHandler {
|
||||
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 create_user(&self, request: CreateUserRequest) -> Result<()>;
|
||||
async fn update_user(&self, request: UpdateUserRequest) -> Result<()>;
|
||||
|
@ -5,6 +5,7 @@ use crate::{
|
||||
opaque_handler::OpaqueHandler,
|
||||
},
|
||||
infra::{
|
||||
access_control::{AccessControlledBackendHandler, ReadonlyBackendHandler},
|
||||
auth_service,
|
||||
configuration::{Configuration, MailOptions},
|
||||
logging::CustomRootSpanBuilder,
|
||||
@ -74,11 +75,11 @@ fn http_config<Backend>(
|
||||
server_url: String,
|
||||
mail_options: MailOptions,
|
||||
) where
|
||||
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Sync + 'static,
|
||||
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static,
|
||||
{
|
||||
let enable_password_reset = mail_options.enable_password_reset;
|
||||
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_blacklist: RwLock::new(jwt_blacklist),
|
||||
server_url,
|
||||
@ -110,20 +111,41 @@ fn http_config<Backend>(
|
||||
}
|
||||
|
||||
pub(crate) struct AppState<Backend> {
|
||||
pub backend_handler: Backend,
|
||||
pub backend_handler: AccessControlledBackendHandler<Backend>,
|
||||
pub jwt_key: Hmac<Sha512>,
|
||||
pub jwt_blacklist: RwLock<HashSet<u64>>,
|
||||
pub server_url: String,
|
||||
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>(
|
||||
config: &Configuration,
|
||||
backend_handler: Backend,
|
||||
server_builder: ServerBuilder,
|
||||
) -> Result<ServerBuilder>
|
||||
where
|
||||
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Sync + 'static,
|
||||
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static,
|
||||
{
|
||||
let jwt_secret = config.jwt_secret.clone();
|
||||
let jwt_blacklist = backend_handler
|
||||
|
@ -7,7 +7,10 @@ use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
domain::{
|
||||
handler::{CreateUserRequest, GroupBackendHandler, GroupRequestFilter, UserBackendHandler},
|
||||
handler::{
|
||||
CreateUserRequest, GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter,
|
||||
UserBackendHandler,
|
||||
},
|
||||
sql_backend_handler::SqlBackendHandler,
|
||||
sql_opaque_handler::register_password,
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user