server: statically enforce access control

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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