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]
|
#[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<()>;
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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},
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
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},
|
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(®istration_start_request.username);
|
let user_id = UserId::new(®istration_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)
|
||||||
|
@ -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)
|
||||||
|
@ -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?;
|
||||||
|
@ -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!(
|
||||||
|
@ -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(),
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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<()>;
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user