server: extract ldap operations to a crate

This commit is contained in:
Valentin Tolmer
2025-04-04 21:52:19 -05:00
committed by nitnelave
parent a49ddeaa02
commit ee21d83056
28 changed files with 239 additions and 136 deletions
+6
View File
@@ -102,6 +102,9 @@ path = "../crates/domain-handlers"
[dependencies.lldap_frontend_options]
path = "../crates/frontend-options"
[dependencies.lldap_ldap]
path = "../crates/ldap"
[dependencies.lldap_opaque_handler]
path = "../crates/opaque-handler"
@@ -189,6 +192,9 @@ features = ["test"]
path = "../crates/opaque-handler"
features = ["test"]
[dev-dependencies.lldap_test_utils]
path = "../crates/test-utils"
[dev-dependencies.reqwest]
version = "*"
default-features = false
-41
View File
@@ -1,41 +0,0 @@
use anyhow::{Context as AnyhowContext, Result, bail};
use lldap_domain::types::{AttributeType, AttributeValue, JpegPhoto};
pub fn deserialize_attribute_value(
value: &[String],
typ: AttributeType,
is_list: bool,
) -> Result<AttributeValue> {
if !is_list && value.len() != 1 {
bail!("Attribute is not a list, but multiple values were provided",);
}
let parse_int = |value: &String| -> Result<i64> {
value
.parse::<i64>()
.with_context(|| format!("Invalid integer value {}", value))
};
let parse_date = |value: &String| -> Result<chrono::NaiveDateTime> {
Ok(chrono::DateTime::parse_from_rfc3339(value)
.with_context(|| format!("Invalid date value {}", value))?
.naive_utc())
};
let parse_photo = |value: &String| -> Result<JpegPhoto> {
JpegPhoto::try_from(value.as_str()).context("Provided image is not a valid JPEG")
};
Ok(match (typ, is_list) {
(AttributeType::String, false) => value[0].clone().into(),
(AttributeType::String, true) => value.to_vec().into(),
(AttributeType::Integer, false) => (parse_int(&value[0])?).into(),
(AttributeType::Integer, true) => {
(value.iter().map(parse_int).collect::<Result<Vec<_>>>()?).into()
}
(AttributeType::DateTime, false) => (parse_date(&value[0])?).into(),
(AttributeType::DateTime, true) => {
(value.iter().map(parse_date).collect::<Result<Vec<_>>>()?).into()
}
(AttributeType::JpegPhoto, false) => (parse_photo(&value[0])?).into(),
(AttributeType::JpegPhoto, true) => {
(value.iter().map(parse_photo).collect::<Result<Vec<_>>>()?).into()
}
})
}
-17
View File
@@ -1,17 +0,0 @@
use ldap3_proto::LdapResultCode;
#[derive(Debug, PartialEq)]
pub struct LdapError {
pub code: LdapResultCode,
pub message: String,
}
impl std::fmt::Display for LdapError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for LdapError {}
pub type LdapResult<T> = std::result::Result<T, LdapError>;
-330
View File
@@ -1,330 +0,0 @@
use chrono::TimeZone;
use ldap3_proto::{
LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, proto::LdapOp,
};
use tracing::{debug, instrument, warn};
use crate::domain::{
deserialize::deserialize_attribute_value,
ldap::{
error::{LdapError, LdapResult},
utils::{
ExpandedAttributes, GroupFieldType, LdapInfo, expand_attribute_wildcards,
get_custom_attribute, get_group_id_from_distinguished_name_or_plain_name,
get_user_id_from_distinguished_name_or_plain_name, map_group_field,
},
},
};
use lldap_domain::{
public_schema::PublicSchema,
types::{AttributeName, AttributeType, Group, GroupId, LdapObjectClass, UserId, Uuid},
};
use lldap_domain_handlers::handler::{GroupListerBackendHandler, GroupRequestFilter};
pub fn get_group_attribute(
group: &Group,
base_dn_str: &str,
attribute: &AttributeName,
user_filter: &Option<UserId>,
ignored_group_attributes: &[AttributeName],
schema: &PublicSchema,
) -> Option<Vec<Vec<u8>>> {
let attribute_values = match map_group_field(attribute, schema) {
GroupFieldType::ObjectClass => {
let mut classes = vec![b"groupOfUniqueNames".to_vec()];
classes.extend(
schema
.get_schema()
.extra_group_object_classes
.iter()
.map(|c| c.as_str().as_bytes().to_vec()),
);
classes
}
// Always returned as part of the base response.
GroupFieldType::Dn => return None,
GroupFieldType::EntryDn => {
vec![format!("uid={},ou=groups,{}", group.display_name, base_dn_str).into_bytes()]
}
GroupFieldType::GroupId => {
vec![group.id.0.to_string().into_bytes()]
}
GroupFieldType::DisplayName => vec![group.display_name.to_string().into_bytes()],
GroupFieldType::CreationDate => vec![
chrono::Utc
.from_utc_datetime(&group.creation_date)
.to_rfc3339()
.into_bytes(),
],
GroupFieldType::Member => group
.users
.iter()
.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(),
GroupFieldType::Uuid => vec![group.uuid.to_string().into_bytes()],
GroupFieldType::Attribute(attr, _, _) => get_custom_attribute(&group.attributes, &attr)?,
GroupFieldType::NoMatch => match attribute.as_str() {
"1.1" => return None,
// We ignore the operational attribute wildcard
"+" => return None,
"*" => {
panic!(
"Matched {}, * should have been expanded into attribute list and * removed",
attribute
)
}
_ => {
if ignored_group_attributes.contains(attribute) {
return None;
}
get_custom_attribute(
&group.attributes,
attribute,
).or_else(||{warn!(
r#"Ignoring unrecognized group attribute: {}. To disable this warning, add it to "ignored_group_attributes" in the config."#,
attribute
);None})?
}
},
};
if attribute_values.len() == 1 && attribute_values[0].is_empty() {
None
} else {
Some(attribute_values)
}
}
const ALL_GROUP_ATTRIBUTE_KEYS: &[&str] = &[
"objectclass",
"uid",
"cn",
"member",
"uniquemember",
"entryuuid",
];
fn expand_group_attribute_wildcards(attributes: &[String]) -> ExpandedAttributes {
expand_attribute_wildcards(attributes, ALL_GROUP_ATTRIBUTE_KEYS)
}
fn make_ldap_search_group_result_entry(
group: Group,
base_dn_str: &str,
mut expanded_attributes: ExpandedAttributes,
user_filter: &Option<UserId>,
ignored_group_attributes: &[AttributeName],
schema: &PublicSchema,
) -> LdapSearchResultEntry {
if expanded_attributes.include_custom_attributes {
expanded_attributes.attribute_keys.extend(
group
.attributes
.iter()
.map(|a| (a.name.clone(), a.name.to_string())),
);
}
LdapSearchResultEntry {
dn: format!("cn={},ou=groups,{}", group.display_name, base_dn_str),
attributes: expanded_attributes
.attribute_keys
.into_iter()
.filter_map(|(attribute, name)| {
let values = get_group_attribute(
&group,
base_dn_str,
&attribute,
user_filter,
ignored_group_attributes,
schema,
)?;
Some(LdapPartialAttribute {
atype: name,
vals: values,
})
})
.collect::<Vec<LdapPartialAttribute>>(),
}
}
fn get_group_attribute_equality_filter(
field: &AttributeName,
typ: AttributeType,
is_list: bool,
value: &str,
) -> GroupRequestFilter {
let value_lc = value.to_ascii_lowercase();
let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, is_list);
let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, is_list);
match (serialized_value, serialized_value_lc) {
(Ok(v), Ok(v_lc)) => GroupRequestFilter::Or(vec![
GroupRequestFilter::AttributeEquality(field.clone(), v),
GroupRequestFilter::AttributeEquality(field.clone(), v_lc),
]),
(Ok(_), Err(e)) => {
warn!("Invalid value for attribute {} (lowercased): {}", field, e);
GroupRequestFilter::from(false)
}
(Err(e), _) => {
warn!("Invalid value for attribute {}: {}", field, e);
GroupRequestFilter::from(false)
}
}
}
fn convert_group_filter(
ldap_info: &LdapInfo,
filter: &LdapFilter,
schema: &PublicSchema,
) -> LdapResult<GroupRequestFilter> {
let rec = |f| convert_group_filter(ldap_info, f, schema);
match filter {
LdapFilter::Equality(field, value) => {
let field = AttributeName::from(field.as_str());
let value_lc = value.to_ascii_lowercase();
match map_group_field(&field, schema) {
GroupFieldType::GroupId => Ok(value_lc
.parse::<i32>()
.map(|id| GroupRequestFilter::GroupId(GroupId(id)))
.unwrap_or_else(|_| {
warn!("Given group id is not a valid integer: {}", value_lc);
GroupRequestFilter::from(false)
})),
GroupFieldType::DisplayName => Ok(GroupRequestFilter::DisplayName(value_lc.into())),
GroupFieldType::Uuid => Uuid::try_from(value_lc.as_str())
.map(GroupRequestFilter::Uuid)
.map_err(|e| LdapError {
code: LdapResultCode::Other,
message: format!("Invalid UUID: {:#}", e),
}),
GroupFieldType::Member => Ok(get_user_id_from_distinguished_name_or_plain_name(
&value_lc,
&ldap_info.base_dn,
&ldap_info.base_dn_str,
)
.map(GroupRequestFilter::Member)
.unwrap_or_else(|e| {
warn!("Invalid member filter on group: {}", e);
GroupRequestFilter::from(false)
})),
GroupFieldType::ObjectClass => Ok(GroupRequestFilter::from(
matches!(value_lc.as_str(), "groupofuniquenames" | "groupofnames")
|| schema
.get_schema()
.extra_group_object_classes
.contains(&LdapObjectClass::from(value_lc)),
)),
GroupFieldType::Dn | GroupFieldType::EntryDn => {
Ok(get_group_id_from_distinguished_name_or_plain_name(
value_lc.as_str(),
&ldap_info.base_dn,
&ldap_info.base_dn_str,
)
.map(GroupRequestFilter::DisplayName)
.unwrap_or_else(|_| {
warn!("Invalid dn filter on group: {}", value_lc);
GroupRequestFilter::from(false)
}))
}
GroupFieldType::NoMatch => {
if !ldap_info.ignored_group_attributes.contains(&field) {
warn!(
r#"Ignoring unknown group attribute "{}" in filter.\n\
To disable this warning, add it to "ignored_group_attributes" in the config."#,
field
);
}
Ok(GroupRequestFilter::from(false))
}
GroupFieldType::Attribute(field, typ, is_list) => Ok(
get_group_attribute_equality_filter(&field, typ, is_list, value),
),
GroupFieldType::CreationDate => Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: "Creation date filter for groups not supported".to_owned(),
}),
}
}
LdapFilter::And(filters) => Ok(GroupRequestFilter::And(
filters.iter().map(rec).collect::<LdapResult<_>>()?,
)),
LdapFilter::Or(filters) => Ok(GroupRequestFilter::Or(
filters.iter().map(rec).collect::<LdapResult<_>>()?,
)),
LdapFilter::Not(filter) => Ok(GroupRequestFilter::Not(Box::new(rec(filter)?))),
LdapFilter::Present(field) => {
let field = AttributeName::from(field.as_str());
Ok(match map_group_field(&field, schema) {
GroupFieldType::Attribute(name, _, _) => {
GroupRequestFilter::CustomAttributePresent(name)
}
GroupFieldType::NoMatch => GroupRequestFilter::from(false),
_ => GroupRequestFilter::from(true),
})
}
LdapFilter::Substring(field, substring_filter) => {
let field = AttributeName::from(field.as_str());
match map_group_field(&field, schema) {
GroupFieldType::DisplayName => Ok(GroupRequestFilter::DisplayNameSubString(
substring_filter.clone().into(),
)),
GroupFieldType::NoMatch => Ok(GroupRequestFilter::from(false)),
_ => Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: format!(
"Unsupported group attribute for substring filter: \"{}\"",
field
),
}),
}
}
_ => Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: format!("Unsupported group filter: {:?}", filter),
}),
}
}
#[instrument(skip_all, level = "debug", fields(ldap_filter))]
pub async fn get_groups_list<Backend: GroupListerBackendHandler>(
ldap_info: &LdapInfo,
ldap_filter: &LdapFilter,
base: &str,
backend: &Backend,
schema: &PublicSchema,
) -> LdapResult<Vec<Group>> {
let filters = convert_group_filter(ldap_info, ldap_filter, schema)?;
debug!(?filters);
backend
.list_groups(Some(filters))
.await
.map_err(|e| LdapError {
code: LdapResultCode::Other,
message: format!(r#"Error while listing groups "{}": {:#}"#, base, e),
})
}
pub fn convert_groups_to_ldap_op<'a>(
groups: Vec<Group>,
attributes: &'a [String],
ldap_info: &'a LdapInfo,
user_filter: &'a Option<UserId>,
schema: &'a PublicSchema,
) -> impl Iterator<Item = LdapOp> + 'a {
let expanded_attributes = if groups.is_empty() {
None
} else {
Some(expand_group_attribute_wildcards(attributes))
};
groups.into_iter().map(move |g| {
LdapOp::SearchResultEntry(make_ldap_search_group_result_entry(
g,
&ldap_info.base_dn_str,
expanded_attributes.clone().unwrap(),
user_filter,
&ldap_info.ignored_group_attributes,
schema,
))
})
}
-4
View File
@@ -1,4 +0,0 @@
pub mod error;
pub mod group;
pub mod user;
pub mod utils;
-357
View File
@@ -1,357 +0,0 @@
use chrono::TimeZone;
use ldap3_proto::{
LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, proto::LdapOp,
};
use tracing::{debug, instrument, warn};
use crate::domain::{
deserialize::deserialize_attribute_value,
ldap::{
error::{LdapError, LdapResult},
utils::{
ExpandedAttributes, LdapInfo, UserFieldType, expand_attribute_wildcards,
get_custom_attribute, get_group_id_from_distinguished_name_or_plain_name,
get_user_id_from_distinguished_name_or_plain_name, map_user_field,
},
},
};
use lldap_domain::{
public_schema::PublicSchema,
types::{
AttributeName, AttributeType, GroupDetails, LdapObjectClass, User, UserAndGroups, UserId,
},
};
use lldap_domain_handlers::handler::{UserListerBackendHandler, UserRequestFilter};
use lldap_domain_model::model::UserColumn;
pub fn get_user_attribute(
user: &User,
attribute: &AttributeName,
base_dn_str: &str,
groups: Option<&[GroupDetails]>,
ignored_user_attributes: &[AttributeName],
schema: &PublicSchema,
) -> Option<Vec<Vec<u8>>> {
let attribute_values = match map_user_field(attribute, schema) {
UserFieldType::ObjectClass => {
let mut classes = vec![
b"inetOrgPerson".to_vec(),
b"posixAccount".to_vec(),
b"mailAccount".to_vec(),
b"person".to_vec(),
];
classes.extend(
schema
.get_schema()
.extra_user_object_classes
.iter()
.map(|c| c.as_str().as_bytes().to_vec()),
);
classes
}
// dn is always returned as part of the base response.
UserFieldType::Dn => return None,
UserFieldType::EntryDn => {
vec![format!("uid={},ou=people,{}", &user.user_id, base_dn_str).into_bytes()]
}
UserFieldType::MemberOf => groups
.into_iter()
.flatten()
.map(|id_and_name| {
format!("cn={},ou=groups,{}", &id_and_name.display_name, base_dn_str).into_bytes()
})
.collect(),
UserFieldType::PrimaryField(UserColumn::UserId) => {
vec![user.user_id.to_string().into_bytes()]
}
UserFieldType::PrimaryField(UserColumn::Email) => vec![user.email.to_string().into_bytes()],
UserFieldType::PrimaryField(
UserColumn::LowercaseEmail
| UserColumn::PasswordHash
| UserColumn::TotpSecret
| UserColumn::MfaType,
) => panic!("Should not get here"),
UserFieldType::PrimaryField(UserColumn::Uuid) => vec![user.uuid.to_string().into_bytes()],
UserFieldType::PrimaryField(UserColumn::DisplayName) => {
vec![user.display_name.clone()?.into_bytes()]
}
UserFieldType::PrimaryField(UserColumn::CreationDate) => vec![
chrono::Utc
.from_utc_datetime(&user.creation_date)
.to_rfc3339()
.into_bytes(),
],
UserFieldType::Attribute(attr, _, _) => get_custom_attribute(&user.attributes, &attr)?,
UserFieldType::NoMatch => match attribute.as_str() {
"1.1" => return None,
// We ignore the operational attribute wildcard.
"+" => return None,
"*" => {
panic!(
"Matched {}, * should have been expanded into attribute list and * removed",
attribute
)
}
_ => {
if ignored_user_attributes.contains(attribute) {
return None;
}
get_custom_attribute(&user.attributes, attribute).or_else(|| {
warn!(
r#"Ignoring unrecognized user attribute: {}. To disable this warning, add it to "ignored_user_attributes" in the config."#,
attribute
);
None
})?
}
},
};
if attribute_values.len() == 1 && attribute_values[0].is_empty() {
None
} else {
Some(attribute_values)
}
}
const ALL_USER_ATTRIBUTE_KEYS: &[&str] = &[
"objectclass",
"uid",
"mail",
"givenname",
"sn",
"cn",
"jpegPhoto",
"createtimestamp",
"entryuuid",
];
fn make_ldap_search_user_result_entry(
user: User,
base_dn_str: &str,
mut expanded_attributes: ExpandedAttributes,
groups: Option<&[GroupDetails]>,
ignored_user_attributes: &[AttributeName],
schema: &PublicSchema,
) -> LdapSearchResultEntry {
if expanded_attributes.include_custom_attributes {
expanded_attributes.attribute_keys.extend(
user.attributes
.iter()
.map(|a| (a.name.clone(), a.name.to_string())),
);
}
LdapSearchResultEntry {
dn: format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str),
attributes: expanded_attributes
.attribute_keys
.into_iter()
.filter_map(|(attribute, name)| {
let values = get_user_attribute(
&user,
&attribute,
base_dn_str,
groups,
ignored_user_attributes,
schema,
)?;
Some(LdapPartialAttribute {
atype: name,
vals: values,
})
})
.collect::<Vec<LdapPartialAttribute>>(),
}
}
fn get_user_attribute_equality_filter(
field: &AttributeName,
typ: AttributeType,
is_list: bool,
value: &str,
) -> UserRequestFilter {
let value_lc = value.to_ascii_lowercase();
let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, is_list);
let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, is_list);
match (serialized_value, serialized_value_lc) {
(Ok(v), Ok(v_lc)) => UserRequestFilter::Or(vec![
UserRequestFilter::AttributeEquality(field.clone(), v),
UserRequestFilter::AttributeEquality(field.clone(), v_lc),
]),
(Ok(_), Err(e)) => {
warn!("Invalid value for attribute {} (lowercased): {}", field, e);
UserRequestFilter::from(false)
}
(Err(e), _) => {
warn!("Invalid value for attribute {}: {}", field, e);
UserRequestFilter::from(false)
}
}
}
fn convert_user_filter(
ldap_info: &LdapInfo,
filter: &LdapFilter,
schema: &PublicSchema,
) -> LdapResult<UserRequestFilter> {
let rec = |f| convert_user_filter(ldap_info, f, schema);
match filter {
LdapFilter::And(filters) => Ok(UserRequestFilter::And(
filters.iter().map(rec).collect::<LdapResult<_>>()?,
)),
LdapFilter::Or(filters) => Ok(UserRequestFilter::Or(
filters.iter().map(rec).collect::<LdapResult<_>>()?,
)),
LdapFilter::Not(filter) => Ok(UserRequestFilter::Not(Box::new(rec(filter)?))),
LdapFilter::Equality(field, value) => {
let field = AttributeName::from(field.as_str());
let value_lc = value.to_ascii_lowercase();
match map_user_field(&field, schema) {
UserFieldType::PrimaryField(UserColumn::UserId) => {
Ok(UserRequestFilter::UserId(UserId::new(&value_lc)))
}
UserFieldType::PrimaryField(UserColumn::Email) => Ok(UserRequestFilter::Equality(
UserColumn::LowercaseEmail,
value_lc,
)),
UserFieldType::PrimaryField(field) => {
Ok(UserRequestFilter::Equality(field, value_lc))
}
UserFieldType::Attribute(field, typ, is_list) => Ok(
get_user_attribute_equality_filter(&field, typ, is_list, value),
),
UserFieldType::NoMatch => {
if !ldap_info.ignored_user_attributes.contains(&field) {
warn!(
r#"Ignoring unknown user attribute "{}" in filter.\n\
To disable this warning, add it to "ignored_user_attributes" in the config"#,
field
);
}
Ok(UserRequestFilter::from(false))
}
UserFieldType::ObjectClass => Ok(UserRequestFilter::from(
matches!(
value_lc.as_str(),
"person" | "inetorgperson" | "posixaccount" | "mailaccount"
) || schema
.get_schema()
.extra_user_object_classes
.contains(&LdapObjectClass::from(value_lc)),
)),
UserFieldType::MemberOf => Ok(get_group_id_from_distinguished_name_or_plain_name(
&value_lc,
&ldap_info.base_dn,
&ldap_info.base_dn_str,
)
.map(UserRequestFilter::MemberOf)
.unwrap_or_else(|e| {
warn!("Invalid memberOf filter: {}", e);
UserRequestFilter::from(false)
})),
UserFieldType::EntryDn | UserFieldType::Dn => {
Ok(get_user_id_from_distinguished_name_or_plain_name(
value_lc.as_str(),
&ldap_info.base_dn,
&ldap_info.base_dn_str,
)
.map(UserRequestFilter::UserId)
.unwrap_or_else(|_| {
warn!("Invalid dn filter on user: {}", value_lc);
UserRequestFilter::from(false)
}))
}
}
}
LdapFilter::Present(field) => {
let field = AttributeName::from(field.as_str());
Ok(match map_user_field(&field, schema) {
UserFieldType::Attribute(name, _, _) => {
UserRequestFilter::CustomAttributePresent(name)
}
UserFieldType::NoMatch => UserRequestFilter::from(false),
_ => UserRequestFilter::from(true),
})
}
LdapFilter::Substring(field, substring_filter) => {
let field = AttributeName::from(field.as_str());
match map_user_field(&field, schema) {
UserFieldType::PrimaryField(UserColumn::UserId) => Ok(
UserRequestFilter::UserIdSubString(substring_filter.clone().into()),
),
UserFieldType::Attribute(_, _, _)
| UserFieldType::ObjectClass
| UserFieldType::MemberOf
| UserFieldType::Dn
| UserFieldType::EntryDn
| UserFieldType::PrimaryField(UserColumn::CreationDate)
| UserFieldType::PrimaryField(UserColumn::Uuid) => Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: format!(
"Unsupported user attribute for substring filter: {:?}",
field
),
}),
UserFieldType::NoMatch => Ok(UserRequestFilter::from(false)),
UserFieldType::PrimaryField(UserColumn::Email) => Ok(UserRequestFilter::SubString(
UserColumn::LowercaseEmail,
substring_filter.clone().into(),
)),
UserFieldType::PrimaryField(field) => Ok(UserRequestFilter::SubString(
field,
substring_filter.clone().into(),
)),
}
}
_ => Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: format!("Unsupported user filter: {:?}", filter),
}),
}
}
fn expand_user_attribute_wildcards(attributes: &[String]) -> ExpandedAttributes {
expand_attribute_wildcards(attributes, ALL_USER_ATTRIBUTE_KEYS)
}
#[instrument(skip_all, level = "debug", fields(ldap_filter, request_groups))]
pub async fn get_user_list<Backend: UserListerBackendHandler>(
ldap_info: &LdapInfo,
ldap_filter: &LdapFilter,
request_groups: bool,
base: &str,
backend: &Backend,
schema: &PublicSchema,
) -> LdapResult<Vec<UserAndGroups>> {
let filters = convert_user_filter(ldap_info, ldap_filter, schema)?;
debug!(?filters);
backend
.list_users(Some(filters), request_groups)
.await
.map_err(|e| LdapError {
code: LdapResultCode::Other,
message: format!(r#"Error while searching user "{}": {:#}"#, base, e),
})
}
pub fn convert_users_to_ldap_op<'a>(
users: Vec<UserAndGroups>,
attributes: &'a [String],
ldap_info: &'a LdapInfo,
schema: &'a PublicSchema,
) -> impl Iterator<Item = LdapOp> + 'a {
let expanded_attributes = if users.is_empty() {
None
} else {
Some(expand_user_attribute_wildcards(attributes))
};
users.into_iter().map(move |u| {
LdapOp::SearchResultEntry(make_ldap_search_user_result_entry(
u.user,
&ldap_info.base_dn_str,
expanded_attributes.clone().unwrap(),
u.groups.as_deref(),
&ldap_info.ignored_user_attributes,
schema,
))
})
}
-371
View File
@@ -1,371 +0,0 @@
use std::collections::BTreeMap;
use chrono::TimeZone;
use ldap3_proto::LdapResultCode;
use tracing::{debug, instrument, warn};
use crate::domain::ldap::error::{LdapError, LdapResult};
use lldap_domain::{
public_schema::PublicSchema,
types::{
Attribute, AttributeName, AttributeType, AttributeValue, Cardinality, GroupName, UserId,
},
};
use lldap_domain_model::model::UserColumn;
fn make_dn_pair<I>(mut iter: I) -> LdapResult<(String, String)>
where
I: Iterator<Item = String>,
{
(|| {
let pair = (
iter.next().ok_or_else(|| "Empty DN element".to_string())?,
iter.next().ok_or_else(|| "Missing DN value".to_string())?,
);
if let Some(e) = iter.next() {
Err(format!(
r#"Too many elements in distinguished name: "{}", "{}", "{}""#,
pair.0, pair.1, e
))
} else {
Ok(pair)
}
})()
.map_err(|e| LdapError {
code: LdapResultCode::InvalidDNSyntax,
message: e,
})
}
pub fn parse_distinguished_name(dn: &str) -> LdapResult<Vec<(String, String)>> {
assert!(dn == dn.to_ascii_lowercase());
dn.split(',')
.map(|s| make_dn_pair(s.split('=').map(str::trim).map(String::from)))
.collect()
}
pub enum UserOrGroupName {
User(UserId),
Group(GroupName),
BadSubStree,
UnexpectedFormat,
InvalidSyntax(LdapError),
}
impl UserOrGroupName {
pub fn into_ldap_error(self, input: &str, expected_format: String) -> LdapError {
LdapError {
code: LdapResultCode::InvalidDNSyntax,
message: match self {
UserOrGroupName::BadSubStree => "Not a subtree of the base tree".to_string(),
UserOrGroupName::InvalidSyntax(err) => return err,
UserOrGroupName::UnexpectedFormat
| UserOrGroupName::User(_)
| UserOrGroupName::Group(_) => format!(
r#"Unexpected DN format. Got "{}", expected: {}"#,
input, expected_format
),
},
}
}
}
pub fn get_user_or_group_id_from_distinguished_name(
dn: &str,
base_tree: &[(String, String)],
) -> UserOrGroupName {
let parts = match parse_distinguished_name(dn) {
Ok(p) => p,
Err(e) => return UserOrGroupName::InvalidSyntax(e),
};
if !is_subtree(&parts, base_tree) {
return UserOrGroupName::BadSubStree;
} else if parts.len() == base_tree.len() + 2
&& parts[1].0 == "ou"
&& (parts[0].0 == "cn" || parts[0].0 == "uid")
{
if parts[1].1 == "groups" {
return UserOrGroupName::Group(GroupName::from(parts[0].1.clone()));
} else if parts[1].1 == "people" {
return UserOrGroupName::User(UserId::from(parts[0].1.clone()));
}
}
UserOrGroupName::UnexpectedFormat
}
pub fn get_user_id_from_distinguished_name(
dn: &str,
base_tree: &[(String, String)],
base_dn_str: &str,
) -> LdapResult<UserId> {
match get_user_or_group_id_from_distinguished_name(dn, base_tree) {
UserOrGroupName::User(user_id) => Ok(user_id),
err => Err(err.into_ldap_error(dn, format!(r#""uid=id,ou=people,{}""#, base_dn_str))),
}
}
pub fn get_group_id_from_distinguished_name(
dn: &str,
base_tree: &[(String, String)],
base_dn_str: &str,
) -> LdapResult<GroupName> {
match get_user_or_group_id_from_distinguished_name(dn, base_tree) {
UserOrGroupName::Group(group_name) => Ok(group_name),
err => Err(err.into_ldap_error(dn, format!(r#""uid=id,ou=groups,{}""#, base_dn_str))),
}
}
fn looks_like_distinguished_name(dn: &str) -> bool {
dn.contains('=') || dn.contains(',')
}
pub fn get_user_id_from_distinguished_name_or_plain_name(
dn: &str,
base_tree: &[(String, String)],
base_dn_str: &str,
) -> LdapResult<UserId> {
if !looks_like_distinguished_name(dn) {
Ok(UserId::from(dn))
} else {
get_user_id_from_distinguished_name(dn, base_tree, base_dn_str)
}
}
pub fn get_group_id_from_distinguished_name_or_plain_name(
dn: &str,
base_tree: &[(String, String)],
base_dn_str: &str,
) -> LdapResult<GroupName> {
if !looks_like_distinguished_name(dn) {
Ok(GroupName::from(dn))
} else {
get_group_id_from_distinguished_name(dn, base_tree, base_dn_str)
}
}
#[derive(Clone)]
pub struct ExpandedAttributes {
// Lowercase name to original name.
pub attribute_keys: BTreeMap<AttributeName, String>,
pub include_custom_attributes: bool,
}
#[instrument(skip(all_attribute_keys), level = "debug")]
pub fn expand_attribute_wildcards(
ldap_attributes: &[String],
all_attribute_keys: &[&'static str],
) -> ExpandedAttributes {
let mut include_custom_attributes = false;
let mut attributes_out: BTreeMap<_, _> = ldap_attributes
.iter()
.filter(|&s| s != "*" && s != "+" && s != "1.1")
.map(|s| (AttributeName::from(s), s.to_string()))
.collect();
attributes_out.extend(
if ldap_attributes.iter().any(|x| x == "*") || ldap_attributes.is_empty() {
include_custom_attributes = true;
all_attribute_keys
} else {
&[]
}
.iter()
.map(|&s| (AttributeName::from(s), s.to_string())),
);
debug!(?attributes_out);
ExpandedAttributes {
attribute_keys: attributes_out,
include_custom_attributes,
}
}
pub fn is_subtree(subtree: &[(String, String)], base_tree: &[(String, String)]) -> bool {
for (k, v) in subtree {
assert!(k == &k.to_ascii_lowercase());
assert!(v == &v.to_ascii_lowercase());
}
for (k, v) in base_tree {
assert!(k == &k.to_ascii_lowercase());
assert!(v == &v.to_ascii_lowercase());
}
if subtree.len() < base_tree.len() {
return false;
}
let size_diff = subtree.len() - base_tree.len();
for i in 0..base_tree.len() {
if subtree[size_diff + i] != base_tree[i] {
return false;
}
}
true
}
pub enum UserFieldType {
NoMatch,
ObjectClass,
MemberOf,
Dn,
EntryDn,
PrimaryField(UserColumn),
Attribute(AttributeName, AttributeType, bool),
}
pub fn map_user_field(field: &AttributeName, schema: &PublicSchema) -> UserFieldType {
match field.as_str() {
"memberof" | "ismemberof" => UserFieldType::MemberOf,
"objectclass" => UserFieldType::ObjectClass,
"dn" | "distinguishedname" => UserFieldType::Dn,
"entrydn" => UserFieldType::EntryDn,
"uid" | "user_id" | "id" => UserFieldType::PrimaryField(UserColumn::UserId),
"mail" | "email" => UserFieldType::PrimaryField(UserColumn::Email),
"cn" | "displayname" | "display_name" => {
UserFieldType::PrimaryField(UserColumn::DisplayName)
}
"givenname" | "first_name" | "firstname" => UserFieldType::Attribute(
AttributeName::from("first_name"),
AttributeType::String,
false,
),
"sn" | "last_name" | "lastname" => UserFieldType::Attribute(
AttributeName::from("last_name"),
AttributeType::String,
false,
),
"avatar" | "jpegphoto" => UserFieldType::Attribute(
AttributeName::from("avatar"),
AttributeType::JpegPhoto,
false,
),
"creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => {
UserFieldType::PrimaryField(UserColumn::CreationDate)
}
"entryuuid" | "uuid" => UserFieldType::PrimaryField(UserColumn::Uuid),
_ => schema
.get_schema()
.user_attributes
.get_attribute_type(field)
.map(|(t, is_list)| UserFieldType::Attribute(field.clone(), t, is_list))
.unwrap_or(UserFieldType::NoMatch),
}
}
pub enum GroupFieldType {
NoMatch,
GroupId,
DisplayName,
CreationDate,
ObjectClass,
Dn,
// Like Dn, but returned as part of the attributes.
EntryDn,
Member,
Uuid,
Attribute(AttributeName, AttributeType, bool),
}
pub fn map_group_field(field: &AttributeName, schema: &PublicSchema) -> GroupFieldType {
match field.as_str() {
"dn" | "distinguishedname" => GroupFieldType::Dn,
"entrydn" => GroupFieldType::EntryDn,
"objectclass" => GroupFieldType::ObjectClass,
"cn" | "displayname" | "uid" | "display_name" | "id" => GroupFieldType::DisplayName,
"creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => {
GroupFieldType::CreationDate
}
"member" | "uniquemember" => GroupFieldType::Member,
"entryuuid" | "uuid" => GroupFieldType::Uuid,
"group_id" | "groupid" => GroupFieldType::GroupId,
_ => schema
.get_schema()
.group_attributes
.get_attribute_type(field)
.map(|(t, is_list)| GroupFieldType::Attribute(field.clone(), t, is_list))
.unwrap_or(GroupFieldType::NoMatch),
}
}
pub struct LdapInfo {
pub base_dn: Vec<(String, String)>,
pub base_dn_str: String,
pub ignored_user_attributes: Vec<AttributeName>,
pub ignored_group_attributes: Vec<AttributeName>,
}
pub fn get_custom_attribute(
attributes: &[Attribute],
attribute_name: &AttributeName,
) -> Option<Vec<Vec<u8>>> {
let convert_date = |date| {
chrono::Utc
.from_utc_datetime(date)
.to_rfc3339()
.into_bytes()
};
attributes
.iter()
.find(|a| &a.name == attribute_name)
.map(|attribute| match &attribute.value {
AttributeValue::String(Cardinality::Singleton(s)) => {
vec![s.clone().into_bytes()]
}
AttributeValue::String(Cardinality::Unbounded(l)) => {
l.iter().map(|s| s.clone().into_bytes()).collect()
}
AttributeValue::Integer(Cardinality::Singleton(i)) => {
// LDAP integers are encoded as strings.
vec![i.to_string().into_bytes()]
}
AttributeValue::Integer(Cardinality::Unbounded(l)) => l
.iter()
// LDAP integers are encoded as strings.
.map(|i| i.to_string().into_bytes())
.collect(),
AttributeValue::JpegPhoto(Cardinality::Singleton(p)) => {
vec![p.clone().into_bytes()]
}
AttributeValue::JpegPhoto(Cardinality::Unbounded(l)) => {
l.iter().map(|p| p.clone().into_bytes()).collect()
}
AttributeValue::DateTime(Cardinality::Singleton(dt)) => vec![convert_date(dt)],
AttributeValue::DateTime(Cardinality::Unbounded(l)) => {
l.iter().map(convert_date).collect()
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_subtree() {
let subtree1 = &[
("ou".to_string(), "people".to_string()),
("dc".to_string(), "example".to_string()),
("dc".to_string(), "com".to_string()),
];
let root = &[
("dc".to_string(), "example".to_string()),
("dc".to_string(), "com".to_string()),
];
assert!(is_subtree(subtree1, root));
assert!(!is_subtree(&[], root));
}
#[test]
fn test_parse_distinguished_name() {
let parsed_dn = &[
("ou".to_string(), "people".to_string()),
("dc".to_string(), "example".to_string()),
("dc".to_string(), "com".to_string()),
];
assert_eq!(
parse_distinguished_name("ou=people,dc=example,dc=com").expect("parsing failed"),
parsed_dn
);
assert_eq!(
parse_distinguished_name(" ou = people , dc = example , dc = com ")
.expect("parsing failed"),
parsed_dn
);
}
}
-2
View File
@@ -1,5 +1,3 @@
pub mod deserialize;
pub mod ldap;
pub mod sql_backend_handler;
pub mod sql_group_backend_handler;
pub mod sql_migrations;
-1
View File
@@ -1,6 +1,5 @@
use crate::{domain::sql_tables::DbConnection, infra::configuration::Configuration};
use async_trait::async_trait;
use lldap_domain_handlers::handler::BackendHandler;
#[derive(Clone)]
+4 -6
View File
@@ -1,7 +1,4 @@
use crate::{
domain::deserialize::deserialize_attribute_value,
infra::graphql::api::{Context, field_error_callback},
};
use crate::infra::graphql::api::{Context, field_error_callback};
use anyhow::{Context as AnyhowContext, anyhow};
use juniper::{FieldError, FieldResult, GraphQLInputObject, GraphQLObject, graphql_object};
use lldap_access_control::{
@@ -9,6 +6,7 @@ use lldap_access_control::{
UserWriteableBackendHandler,
};
use lldap_domain::{
deserialize::deserialize_attribute_value,
public_schema::PublicSchema,
requests::{
CreateAttributeRequest, CreateGroupRequest, CreateUserRequest, UpdateGroupRequest,
@@ -779,15 +777,15 @@ fn deserialize_attribute(
#[cfg(test)]
mod tests {
use super::*;
use crate::infra::{graphql::query::Query, test_utils::MockTestBackendHandler};
use crate::infra::graphql::query::Query;
use juniper::{
DefaultScalarValue, EmptySubscription, GraphQLType, InputValue, RootNode, Variables,
execute, graphql_value,
};
use lldap_auth::access_control::{Permission, ValidationResults};
use lldap_domain::types::{AttributeName, AttributeType};
use lldap_test_utils::MockTestBackendHandler;
use mockall::predicate::eq;
use pretty_assertions::assert_eq;
+5 -10
View File
@@ -1,23 +1,18 @@
use std::sync::Arc;
use crate::{
domain::{
deserialize::deserialize_attribute_value,
ldap::utils::{UserFieldType, map_user_field},
},
infra::graphql::api::{Context, field_error_callback},
};
use crate::infra::graphql::api::{Context, field_error_callback};
use anyhow::Context as AnyhowContext;
use chrono::TimeZone;
use juniper::{FieldResult, GraphQLInputObject, graphql_object};
use lldap_access_control::{ReadonlyBackendHandler, UserReadableBackendHandler};
use lldap_domain::{
deserialize::deserialize_attribute_value,
public_schema::PublicSchema,
types::{AttributeType, Cardinality, GroupDetails, GroupId, LdapObjectClass, UserId},
};
use lldap_domain_handlers::handler::{BackendHandler, ReadSchemaBackendHandler};
use lldap_domain_model::model::UserColumn;
use lldap_ldap::{UserFieldType, map_user_field};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::{Instrument, Span, debug, debug_span};
type DomainRequestFilter = lldap_domain_handlers::handler::UserRequestFilter;
@@ -784,7 +779,6 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
#[cfg(test)]
mod tests {
use super::*;
use crate::infra::test_utils::{MockTestBackendHandler, setup_default_schema};
use chrono::TimeZone;
use juniper::{
DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, RootNode, Variables,
@@ -795,6 +789,7 @@ mod tests {
schema::{AttributeList, Schema},
types::{AttributeName, AttributeType, LdapObjectClass},
};
use lldap_test_utils::{MockTestBackendHandler, setup_default_schema};
use mockall::predicate::eq;
use pretty_assertions::assert_eq;
use std::collections::HashSet;
-242
View File
@@ -1,242 +0,0 @@
use crate::domain::ldap::error::{LdapError, LdapResult};
use ldap3_proto::proto::{LdapCompareRequest, LdapOp, LdapResult as LdapResultOp, LdapResultCode};
use lldap_domain::types::AttributeName;
pub fn compare(
request: LdapCompareRequest,
search_results: Vec<LdapOp>,
base_dn_str: &str,
) -> LdapResult<Vec<LdapOp>> {
if search_results.len() > 2 {
// SearchResultEntry + SearchResultDone
return Err(LdapError {
code: LdapResultCode::OperationsError,
message: "Too many search results".to_string(),
});
}
let requested_attribute = AttributeName::from(&request.atype);
match search_results.first() {
Some(LdapOp::SearchResultEntry(entry)) => {
let available = entry.attributes.iter().any(|attr| {
AttributeName::from(&attr.atype) == requested_attribute
&& attr.vals.contains(&request.val)
});
Ok(vec![LdapOp::CompareResult(LdapResultOp {
code: if available {
LdapResultCode::CompareTrue
} else {
LdapResultCode::CompareFalse
},
matcheddn: request.dn,
message: "".to_string(),
referral: vec![],
})])
}
Some(LdapOp::SearchResultDone(_)) => Ok(vec![LdapOp::CompareResult(LdapResultOp {
code: LdapResultCode::NoSuchObject,
matcheddn: base_dn_str.to_string(),
message: "".to_string(),
referral: vec![],
})]),
None => Err(LdapError {
code: LdapResultCode::OperationsError,
message: "Search request returned nothing".to_string(),
}),
_ => Err(LdapError {
code: LdapResultCode::OperationsError,
message: "Unexpected results from search".to_string(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::infra::{
ldap::handler::tests::setup_bound_admin_handler, test_utils::MockTestBackendHandler,
};
use chrono::TimeZone;
use lldap_domain::{
types::{Group, GroupId, User, UserAndGroups, UserId},
uuid,
};
use lldap_domain_handlers::handler::{GroupRequestFilter, UserRequestFilter};
use pretty_assertions::assert_eq;
use tokio;
#[tokio::test]
async fn test_compare_user() {
let mut mock = MockTestBackendHandler::new();
mock.expect_list_users().returning(|f, g| {
assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob"))));
assert!(!g);
Ok(vec![UserAndGroups {
user: User {
user_id: UserId::new("bob"),
email: "bob@bobmail.bob".into(),
..Default::default()
},
groups: None,
}])
});
mock.expect_list_groups().returning(|_| Ok(vec![]));
let ldap_handler = setup_bound_admin_handler(mock).await;
let dn = "uid=bob,ou=people,dc=example,dc=com";
let request = LdapCompareRequest {
dn: dn.to_string(),
atype: "uid".to_owned(),
val: b"bob".to_vec(),
};
assert_eq!(
ldap_handler.do_compare(request).await,
Ok(vec![LdapOp::CompareResult(LdapResultOp {
code: LdapResultCode::CompareTrue,
matcheddn: dn.to_string(),
message: "".to_string(),
referral: vec![],
})])
);
// Non-canonical attribute.
let request = LdapCompareRequest {
dn: dn.to_string(),
atype: "eMail".to_owned(),
val: b"bob@bobmail.bob".to_vec(),
};
assert_eq!(
ldap_handler.do_compare(request).await,
Ok(vec![LdapOp::CompareResult(LdapResultOp {
code: LdapResultCode::CompareTrue,
matcheddn: dn.to_string(),
message: "".to_string(),
referral: vec![],
})])
);
}
#[tokio::test]
async fn test_compare_group() {
let mut mock = MockTestBackendHandler::new();
mock.expect_list_users().returning(|_, _| Ok(vec![]));
mock.expect_list_groups().returning(|f| {
assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".into())));
Ok(vec![Group {
id: GroupId(1),
display_name: "group".into(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
users: vec![UserId::new("bob")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: Vec::new(),
}])
});
let ldap_handler = setup_bound_admin_handler(mock).await;
let dn = "uid=group,ou=groups,dc=example,dc=com";
let request = LdapCompareRequest {
dn: dn.to_string(),
atype: "uid".to_owned(),
val: b"group".to_vec(),
};
assert_eq!(
ldap_handler.do_compare(request).await,
Ok(vec![LdapOp::CompareResult(LdapResultOp {
code: LdapResultCode::CompareTrue,
matcheddn: dn.to_string(),
message: "".to_string(),
referral: vec![],
})])
);
}
#[tokio::test]
async fn test_compare_not_found() {
let mut mock = MockTestBackendHandler::new();
mock.expect_list_users().returning(|f, g| {
assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob"))));
assert!(!g);
Ok(vec![])
});
mock.expect_list_groups().returning(|_| Ok(vec![]));
let ldap_handler = setup_bound_admin_handler(mock).await;
let dn = "uid=bob,ou=people,dc=example,dc=com";
let request = LdapCompareRequest {
dn: dn.to_string(),
atype: "uid".to_owned(),
val: b"bob".to_vec(),
};
assert_eq!(
ldap_handler.do_compare(request).await,
Ok(vec![LdapOp::CompareResult(LdapResultOp {
code: LdapResultCode::NoSuchObject,
matcheddn: "dc=example,dc=com".to_owned(),
message: "".to_string(),
referral: vec![],
})])
);
}
#[tokio::test]
async fn test_compare_no_match() {
let mut mock = MockTestBackendHandler::new();
mock.expect_list_users().returning(|f, g| {
assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob"))));
assert!(!g);
Ok(vec![UserAndGroups {
user: User {
user_id: UserId::new("bob"),
email: "bob@bobmail.bob".into(),
..Default::default()
},
groups: None,
}])
});
mock.expect_list_groups().returning(|_| Ok(vec![]));
let ldap_handler = setup_bound_admin_handler(mock).await;
let dn = "uid=bob,ou=people,dc=example,dc=com";
let request = LdapCompareRequest {
dn: dn.to_string(),
atype: "mail".to_owned(),
val: b"bob@bob".to_vec(),
};
assert_eq!(
ldap_handler.do_compare(request).await,
Ok(vec![LdapOp::CompareResult(LdapResultOp {
code: LdapResultCode::CompareFalse,
matcheddn: dn.to_string(),
message: "".to_string(),
referral: vec![],
})])
);
}
#[tokio::test]
async fn test_compare_group_member() {
let mut mock = MockTestBackendHandler::new();
mock.expect_list_users().returning(|_, _| Ok(vec![]));
mock.expect_list_groups().returning(|f| {
assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".into())));
Ok(vec![Group {
id: GroupId(1),
display_name: "group".into(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
users: vec![UserId::new("bob")],
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
attributes: Vec::new(),
}])
});
let ldap_handler = setup_bound_admin_handler(mock).await;
let dn = "uid=group,ou=groups,dc=example,dc=com";
let request = LdapCompareRequest {
dn: dn.to_string(),
atype: "uniqueMember".to_owned(),
val: b"uid=bob,ou=people,dc=example,dc=com".to_vec(),
};
assert_eq!(
ldap_handler.do_compare(request).await,
Ok(vec![LdapOp::CompareResult(LdapResultOp {
code: LdapResultCode::CompareTrue,
matcheddn: dn.to_owned(),
message: "".to_string(),
referral: vec![],
})])
);
}
}
-274
View File
@@ -1,274 +0,0 @@
use crate::{
domain::{
deserialize,
ldap::{
error::{LdapError, LdapResult},
utils::{LdapInfo, UserOrGroupName, get_user_or_group_id_from_distinguished_name},
},
},
infra::ldap::handler::make_add_response,
};
use ldap3_proto::proto::{
LdapAddRequest, LdapAttribute, LdapOp, LdapPartialAttribute, LdapResultCode,
};
use lldap_access_control::AdminBackendHandler;
use lldap_domain::{
requests::{CreateGroupRequest, CreateUserRequest},
types::{Attribute, AttributeName, AttributeType, Email, GroupName, UserId},
};
use std::collections::HashMap;
use tracing::instrument;
#[instrument(skip_all, level = "debug")]
pub(crate) async fn create_user_or_group(
backend_handler: &impl AdminBackendHandler,
ldap_info: &LdapInfo,
request: LdapAddRequest,
) -> LdapResult<Vec<LdapOp>> {
let base_dn_str = &ldap_info.base_dn_str;
match get_user_or_group_id_from_distinguished_name(&request.dn, &ldap_info.base_dn) {
UserOrGroupName::User(user_id) => {
create_user(backend_handler, user_id, request.attributes).await
}
UserOrGroupName::Group(group_name) => {
create_group(backend_handler, group_name, request.attributes).await
}
err => Err(err.into_ldap_error(
&request.dn,
format!(
r#""uid=id,ou=people,{}" or "uid=id,ou=groups,{}""#,
base_dn_str, base_dn_str
),
)),
}
}
#[instrument(skip_all, level = "debug")]
async fn create_user(
backend_handler: &impl AdminBackendHandler,
user_id: UserId,
attributes: Vec<LdapAttribute>,
) -> LdapResult<Vec<LdapOp>> {
fn parse_attribute(mut attr: LdapPartialAttribute) -> LdapResult<(String, Vec<u8>)> {
if attr.vals.len() > 1 {
Err(LdapError {
code: LdapResultCode::ConstraintViolation,
message: format!("Expected a single value for attribute {}", attr.atype),
})
} else {
attr.atype.make_ascii_lowercase();
match attr.vals.pop() {
Some(val) => Ok((attr.atype, val)),
None => Err(LdapError {
code: LdapResultCode::ConstraintViolation,
message: format!("Missing value for attribute {}", attr.atype),
}),
}
}
}
let attributes: HashMap<String, Vec<u8>> = attributes
.into_iter()
.filter(|a| !a.atype.eq_ignore_ascii_case("objectclass"))
.map(parse_attribute)
.collect::<LdapResult<_>>()?;
fn decode_attribute_value(val: &[u8]) -> LdapResult<String> {
std::str::from_utf8(val)
.map_err(|e| LdapError {
code: LdapResultCode::ConstraintViolation,
message: format!(
"Attribute value is invalid UTF-8: {:#?} (value {:?})",
e, val
),
})
.map(str::to_owned)
}
let get_attribute = |name| {
attributes
.get(name)
.map(Vec::as_slice)
.map(decode_attribute_value)
};
let make_encoded_attribute = |name: &str, typ: AttributeType, value: String| {
Ok(Attribute {
name: AttributeName::from(name),
value: deserialize::deserialize_attribute_value(&[value], typ, false).map_err(|e| {
LdapError {
code: LdapResultCode::ConstraintViolation,
message: format!("Invalid attribute value: {}", e),
}
})?,
})
};
let mut new_user_attributes: Vec<Attribute> = Vec::new();
if let Some(first_name) = get_attribute("givenname").transpose()? {
new_user_attributes.push(make_encoded_attribute(
"first_name",
AttributeType::String,
first_name,
)?);
}
if let Some(last_name) = get_attribute("sn").transpose()? {
new_user_attributes.push(make_encoded_attribute(
"last_name",
AttributeType::String,
last_name,
)?);
}
if let Some(avatar) = get_attribute("avatar").transpose()? {
new_user_attributes.push(make_encoded_attribute(
"avatar",
AttributeType::JpegPhoto,
avatar,
)?);
}
backend_handler
.create_user(CreateUserRequest {
user_id,
email: Email::from(
get_attribute("mail")
.or_else(|| get_attribute("email"))
.transpose()?
.unwrap_or_default(),
),
display_name: get_attribute("cn").transpose()?,
attributes: new_user_attributes,
})
.await
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!("Could not create user: {:#?}", e),
})?;
Ok(vec![make_add_response(
LdapResultCode::Success,
String::new(),
)])
}
#[instrument(skip_all, level = "debug")]
async fn create_group(
backend_handler: &impl AdminBackendHandler,
group_name: GroupName,
_attributes: Vec<LdapAttribute>,
) -> LdapResult<Vec<LdapOp>> {
backend_handler
.create_group(CreateGroupRequest {
display_name: group_name,
attributes: Vec::new(),
})
.await
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!("Could not create group: {:#?}", e),
})?;
Ok(vec![make_add_response(
LdapResultCode::Success,
String::new(),
)])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::infra::{
ldap::handler::tests::setup_bound_admin_handler, test_utils::MockTestBackendHandler,
};
use lldap_domain::types::*;
use mockall::predicate::eq;
use pretty_assertions::assert_eq;
use tokio;
#[tokio::test]
async fn test_create_user() {
let mut mock = MockTestBackendHandler::new();
mock.expect_create_user()
.with(eq(CreateUserRequest {
user_id: UserId::new("bob"),
email: "".into(),
display_name: Some("Bob".to_string()),
..Default::default()
}))
.times(1)
.return_once(|_| Ok(()));
let ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapAddRequest {
dn: "uid=bob,ou=people,dc=example,dc=com".to_owned(),
attributes: vec![LdapPartialAttribute {
atype: "cn".to_owned(),
vals: vec![b"Bob".to_vec()],
}],
};
assert_eq!(
ldap_handler.create_user_or_group(request).await,
Ok(vec![make_add_response(
LdapResultCode::Success,
String::new()
)])
);
}
#[tokio::test]
async fn test_create_group() {
let mut mock = MockTestBackendHandler::new();
mock.expect_create_group()
.with(eq(CreateGroupRequest {
display_name: GroupName::new("bob"),
..Default::default()
}))
.times(1)
.return_once(|_| Ok(GroupId(5)));
let ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapAddRequest {
dn: "uid=bob,ou=groups,dc=example,dc=com".to_owned(),
attributes: vec![LdapPartialAttribute {
atype: "cn".to_owned(),
vals: vec![b"Bobby".to_vec()],
}],
};
assert_eq!(
ldap_handler.create_user_or_group(request).await,
Ok(vec![make_add_response(
LdapResultCode::Success,
String::new()
)])
);
}
#[tokio::test]
async fn test_create_user_multiple_object_class() {
let mut mock = MockTestBackendHandler::new();
mock.expect_create_user()
.with(eq(CreateUserRequest {
user_id: UserId::new("bob"),
email: "".into(),
display_name: Some("Bob".to_string()),
..Default::default()
}))
.times(1)
.return_once(|_| Ok(()));
let ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapAddRequest {
dn: "uid=bob,ou=people,dc=example,dc=com".to_owned(),
attributes: vec![
LdapPartialAttribute {
atype: "cn".to_owned(),
vals: vec![b"Bob".to_vec()],
},
LdapPartialAttribute {
atype: "objectClass".to_owned(),
vals: vec![
b"top".to_vec(),
b"person".to_vec(),
b"inetOrgPerson".to_vec(),
],
},
],
};
assert_eq!(
ldap_handler.create_user_or_group(request).await,
Ok(vec![make_add_response(
LdapResultCode::Success,
String::new()
)])
);
}
}
-308
View File
@@ -1,308 +0,0 @@
use crate::domain::ldap::{
error::{LdapError, LdapResult},
utils::{LdapInfo, UserOrGroupName, get_user_or_group_id_from_distinguished_name},
};
use ldap3_proto::proto::{LdapOp, LdapResult as LdapResultOp, LdapResultCode};
use lldap_access_control::AdminBackendHandler;
use lldap_domain::types::{GroupName, UserId};
use lldap_domain_handlers::handler::GroupRequestFilter;
use lldap_domain_model::error::DomainError;
use tracing::instrument;
pub(crate) fn make_del_response(code: LdapResultCode, message: String) -> LdapOp {
LdapOp::DelResponse(LdapResultOp {
code,
matcheddn: "".to_string(),
message,
referral: vec![],
})
}
#[instrument(skip_all, level = "debug")]
pub(crate) async fn delete_user_or_group(
backend_handler: &impl AdminBackendHandler,
ldap_info: &LdapInfo,
request: String,
) -> LdapResult<Vec<LdapOp>> {
let base_dn_str = &ldap_info.base_dn_str;
match get_user_or_group_id_from_distinguished_name(&request, &ldap_info.base_dn) {
UserOrGroupName::User(user_id) => delete_user(backend_handler, user_id).await,
UserOrGroupName::Group(group_name) => delete_group(backend_handler, group_name).await,
err => Err(err.into_ldap_error(
&request,
format!(
r#""uid=id,ou=people,{}" or "uid=id,ou=groups,{}""#,
base_dn_str, base_dn_str
),
)),
}
}
#[instrument(skip_all, level = "debug")]
async fn delete_user(
backend_handler: &impl AdminBackendHandler,
user_id: UserId,
) -> LdapResult<Vec<LdapOp>> {
backend_handler
.get_user_details(&user_id)
.await
.map_err(|err| match err {
DomainError::EntityNotFound(_) => LdapError {
code: LdapResultCode::NoSuchObject,
message: "Could not find user".to_string(),
},
e => LdapError {
code: LdapResultCode::OperationsError,
message: format!("Error while finding user: {:?}", e),
},
})?;
backend_handler
.delete_user(&user_id)
.await
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!("Error while deleting user: {:?}", e),
})?;
Ok(vec![make_del_response(
LdapResultCode::Success,
String::new(),
)])
}
#[instrument(skip_all, level = "debug")]
async fn delete_group(
backend_handler: &impl AdminBackendHandler,
group_name: GroupName,
) -> LdapResult<Vec<LdapOp>> {
let groups = backend_handler
.list_groups(Some(GroupRequestFilter::DisplayName(group_name.clone())))
.await
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!("Error while finding group: {:?}", e),
})?;
let group_id = groups
.iter()
.find(|g| g.display_name == group_name)
.map(|g| g.id)
.ok_or_else(|| LdapError {
code: LdapResultCode::NoSuchObject,
message: "Could not find group".to_string(),
})?;
backend_handler
.delete_group(group_id)
.await
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!("Error while deleting group: {:?}", e),
})?;
Ok(vec![make_del_response(
LdapResultCode::Success,
String::new(),
)])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::infra::{
ldap::handler::tests::setup_bound_admin_handler, test_utils::MockTestBackendHandler,
};
use chrono::TimeZone;
use lldap_domain::{
types::{Group, GroupId, User},
uuid,
};
use lldap_domain_model::error::DomainError;
use mockall::predicate::eq;
use pretty_assertions::assert_eq;
use tokio;
#[tokio::test]
async fn test_delete_user() {
let mut mock = MockTestBackendHandler::new();
mock.expect_get_user_details()
.with(eq(UserId::new("bob")))
.return_once(|_| {
Ok(User {
user_id: UserId::new("bob"),
..Default::default()
})
});
mock.expect_delete_user()
.with(eq(UserId::new("bob")))
.times(1)
.return_once(|_| Ok(()));
let mut ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned());
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_del_response(
LdapResultCode::Success,
String::new()
)])
);
}
#[tokio::test]
async fn test_delete_group() {
let mut mock = MockTestBackendHandler::new();
mock.expect_list_groups()
.with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from(
"bob",
)))))
.return_once(|_| {
Ok(vec![Group {
id: GroupId(34),
display_name: GroupName::from("bob"),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
users: Vec::new(),
attributes: Vec::new(),
}])
});
mock.expect_delete_group()
.with(eq(GroupId(34)))
.times(1)
.return_once(|_| Ok(()));
let mut ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned());
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_del_response(
LdapResultCode::Success,
String::new()
)])
);
}
#[tokio::test]
async fn test_delete_user_not_found() {
let mut mock = MockTestBackendHandler::new();
mock.expect_get_user_details()
.with(eq(UserId::new("bob")))
.return_once(|_| Err(DomainError::EntityNotFound("No such user".to_string())));
let mut ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned());
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_del_response(
LdapResultCode::NoSuchObject,
"Could not find user".to_string()
)])
);
}
#[tokio::test]
async fn test_delete_user_lookup_error() {
let mut mock = MockTestBackendHandler::new();
mock.expect_get_user_details()
.with(eq(UserId::new("bob")))
.return_once(|_| Err(DomainError::InternalError("WTF?".to_string())));
let mut ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned());
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_del_response(
LdapResultCode::OperationsError,
r#"Error while finding user: InternalError("WTF?")"#.to_string()
)])
);
}
#[tokio::test]
async fn test_delete_user_deletion_error() {
let mut mock = MockTestBackendHandler::new();
mock.expect_get_user_details()
.with(eq(UserId::new("bob")))
.return_once(|_| {
Ok(User {
user_id: UserId::new("bob"),
..Default::default()
})
});
mock.expect_delete_user()
.with(eq(UserId::new("bob")))
.times(1)
.return_once(|_| Err(DomainError::InternalError("WTF?".to_string())));
let mut ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned());
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_del_response(
LdapResultCode::OperationsError,
r#"Error while deleting user: InternalError("WTF?")"#.to_string()
)])
);
}
#[tokio::test]
async fn test_delete_group_not_found() {
let mut mock = MockTestBackendHandler::new();
mock.expect_list_groups()
.with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from(
"bob",
)))))
.return_once(|_| Ok(vec![]));
let mut ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned());
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_del_response(
LdapResultCode::NoSuchObject,
"Could not find group".to_string()
)])
);
}
#[tokio::test]
async fn test_delete_group_lookup_error() {
let mut mock = MockTestBackendHandler::new();
mock.expect_list_groups()
.with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from(
"bob",
)))))
.return_once(|_| Err(DomainError::InternalError("WTF?".to_string())));
let mut ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned());
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_del_response(
LdapResultCode::OperationsError,
r#"Error while finding group: InternalError("WTF?")"#.to_string()
)])
);
}
#[tokio::test]
async fn test_delete_group_deletion_error() {
let mut mock = MockTestBackendHandler::new();
mock.expect_list_groups()
.with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from(
"bob",
)))))
.return_once(|_| {
Ok(vec![Group {
id: GroupId(34),
display_name: GroupName::from("bob"),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
users: Vec::new(),
attributes: Vec::new(),
}])
});
mock.expect_delete_group()
.with(eq(GroupId(34)))
.times(1)
.return_once(|_| Err(DomainError::InternalError("WTF?".to_string())));
let mut ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned());
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_del_response(
LdapResultCode::OperationsError,
r#"Error while deleting group: InternalError("WTF?")"#.to_string()
)])
);
}
}
-445
View File
@@ -1,445 +0,0 @@
use crate::{
domain::ldap::{
error::{LdapError, LdapResult},
utils::{LdapInfo, parse_distinguished_name},
},
infra::ldap::{
compare, create, delete, modify,
password::{self, do_password_modification},
search::{
self, is_root_dse_request, make_search_error, make_search_request, make_search_success,
root_dse_response,
},
},
};
use ldap3_proto::proto::{
LdapAddRequest, LdapBindRequest, LdapBindResponse, LdapCompareRequest, LdapExtendedRequest,
LdapExtendedResponse, LdapFilter, LdapModifyRequest, LdapOp, LdapPasswordModifyRequest,
LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, OID_PASSWORD_MODIFY, OID_WHOAMI,
};
use lldap_access_control::AccessControlledBackendHandler;
use lldap_auth::access_control::ValidationResults;
use lldap_domain::types::AttributeName;
use lldap_domain_handlers::handler::{BackendHandler, LoginHandler};
use lldap_opaque_handler::OpaqueHandler;
use tracing::{debug, instrument};
use super::delete::make_del_response;
pub(crate) fn make_add_response(code: LdapResultCode, message: String) -> LdapOp {
LdapOp::AddResponse(LdapResultOp {
code,
matcheddn: "".to_string(),
message,
referral: vec![],
})
}
pub(crate) fn make_extended_response(code: LdapResultCode, message: String) -> LdapOp {
LdapOp::ExtendedResponse(LdapExtendedResponse {
res: LdapResultOp {
code,
matcheddn: "".to_string(),
message,
referral: vec![],
},
name: None,
value: None,
})
}
pub(crate) fn make_modify_response(code: LdapResultCode, message: String) -> LdapOp {
LdapOp::ModifyResponse(LdapResultOp {
code,
matcheddn: "".to_string(),
message,
referral: vec![],
})
}
pub struct LdapHandler<Backend> {
user_info: Option<ValidationResults>,
backend_handler: AccessControlledBackendHandler<Backend>,
ldap_info: LdapInfo,
session_uuid: uuid::Uuid,
}
impl<Backend> LdapHandler<Backend> {
pub fn session_uuid(&self) -> &uuid::Uuid {
&self.session_uuid
}
}
impl<Backend: LoginHandler> LdapHandler<Backend> {
pub fn get_login_handler(&self) -> &(impl LoginHandler + use<Backend>) {
self.backend_handler.unsafe_get_handler()
}
}
impl<Backend: OpaqueHandler> LdapHandler<Backend> {
pub fn get_opaque_handler(&self) -> &(impl OpaqueHandler + use<Backend>) {
self.backend_handler.unsafe_get_handler()
}
}
enum Credentials<'s> {
Bound(&'s ValidationResults),
Unbound(Vec<LdapOp>),
}
impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend> {
pub fn new(
backend_handler: AccessControlledBackendHandler<Backend>,
mut ldap_base_dn: String,
ignored_user_attributes: Vec<AttributeName>,
ignored_group_attributes: Vec<AttributeName>,
session_uuid: uuid::Uuid,
) -> Self {
ldap_base_dn.make_ascii_lowercase();
Self {
user_info: None,
backend_handler,
ldap_info: LdapInfo {
base_dn: parse_distinguished_name(&ldap_base_dn).unwrap_or_else(|_| {
panic!(
"Invalid value for ldap_base_dn in configuration: {}",
ldap_base_dn
)
}),
base_dn_str: ldap_base_dn,
ignored_user_attributes,
ignored_group_attributes,
},
session_uuid,
}
}
#[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![],
uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(),
)
}
fn get_credentials(&self) -> Credentials<'_> {
match self.user_info.as_ref() {
Some(user_info) => Credentials::Bound(user_info),
None => Credentials::Unbound(vec![make_extended_response(
LdapResultCode::InsufficentAccessRights,
"No user currently bound".to_string(),
)]),
}
}
pub async fn do_search_or_dse(&self, request: &LdapSearchRequest) -> LdapResult<Vec<LdapOp>> {
if is_root_dse_request(request) {
debug!("rootDSE request");
return Ok(vec![
root_dse_response(&self.ldap_info.base_dn_str),
make_search_success(),
]);
}
self.do_search(request).await
}
#[instrument(skip_all, level = "debug")]
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);
search::do_search(&backend_handler, &self.ldap_info, request).await
}
#[instrument(skip_all, level = "debug", fields(dn = %request.dn))]
pub async fn do_bind(&mut self, request: &LdapBindRequest) -> Vec<LdapOp> {
let (code, message) =
match password::do_bind(&self.ldap_info, request, self.get_login_handler()).await {
Ok(user_id) => {
self.user_info = self
.backend_handler
.get_permissions_for_user(user_id)
.await
.ok();
debug!("Success!");
(LdapResultCode::Success, "".to_string())
}
Err(err) => (err.code, err.message),
};
vec![LdapOp::BindResponse(LdapBindResponse {
res: LdapResultOp {
code,
matcheddn: "".to_string(),
message,
referral: vec![],
},
saslcreds: None,
})]
}
#[instrument(skip_all, level = "debug")]
async fn do_extended_request(&self, request: &LdapExtendedRequest) -> Vec<LdapOp> {
match request.name.as_str() {
OID_PASSWORD_MODIFY => match LdapPasswordModifyRequest::try_from(request) {
Ok(password_request) => {
let credentials = match self.get_credentials() {
Credentials::Bound(cred) => cred,
Credentials::Unbound(err) => return err,
};
do_password_modification(
credentials,
&self.ldap_info,
&self.backend_handler,
self.get_opaque_handler(),
&password_request,
)
.await
.unwrap_or_else(|e: LdapError| vec![make_extended_response(e.code, e.message)])
}
Err(e) => vec![make_extended_response(
LdapResultCode::ProtocolError,
format!("Error while parsing password modify request: {:#?}", e),
)],
},
OID_WHOAMI => {
let authz_id = self
.user_info
.as_ref()
.map(|user_info| {
format!(
"dn:uid={},ou=people,{}",
user_info.user.as_str(),
self.ldap_info.base_dn_str
)
})
.unwrap_or_default();
vec![make_extended_response(LdapResultCode::Success, authz_id)]
}
_ => vec![make_extended_response(
LdapResultCode::UnwillingToPerform,
format!("Unsupported extended operation: {}", &request.name),
)],
}
}
#[instrument(skip_all, level = "debug", fields(dn = %request.dn))]
pub async fn do_modify_request(&self, request: &LdapModifyRequest) -> Vec<LdapOp> {
let credentials = match self.get_credentials() {
Credentials::Bound(cred) => cred,
Credentials::Unbound(err) => return err,
};
modify::handle_modify_request(
self.get_opaque_handler(),
|credentials, user_id| {
self.backend_handler
.get_readable_handler(credentials, &user_id)
},
&self.ldap_info,
credentials,
request,
)
.await
.unwrap_or_else(|e: LdapError| vec![make_modify_response(e.code, e.message)])
}
#[instrument(skip_all, level = "debug")]
pub async fn create_user_or_group(&self, request: LdapAddRequest) -> LdapResult<Vec<LdapOp>> {
let backend_handler = self
.user_info
.as_ref()
.and_then(|u| self.backend_handler.get_admin_handler(u))
.ok_or_else(|| LdapError {
code: LdapResultCode::InsufficentAccessRights,
message: "Unauthorized write".to_string(),
})?;
create::create_user_or_group(backend_handler, &self.ldap_info, request).await
}
#[instrument(skip_all, level = "debug")]
pub async fn delete_user_or_group(&self, request: String) -> LdapResult<Vec<LdapOp>> {
let backend_handler = self
.user_info
.as_ref()
.and_then(|u| self.backend_handler.get_admin_handler(u))
.ok_or_else(|| LdapError {
code: LdapResultCode::InsufficentAccessRights,
message: "Unauthorized write".to_string(),
})?;
delete::delete_user_or_group(backend_handler, &self.ldap_info, request).await
}
#[instrument(skip_all, level = "debug")]
pub async fn do_compare(&self, request: LdapCompareRequest) -> LdapResult<Vec<LdapOp>> {
let req = make_search_request::<String>(
&self.ldap_info.base_dn_str,
LdapFilter::Equality("dn".to_string(), request.dn.to_string()),
vec![request.atype.clone()],
);
compare::compare(
request,
self.do_search(&req).await?,
&self.ldap_info.base_dn_str,
)
}
pub async fn handle_ldap_message(&mut self, ldap_op: LdapOp) -> Option<Vec<LdapOp>> {
Some(match ldap_op {
LdapOp::BindRequest(request) => self.do_bind(&request).await,
LdapOp::SearchRequest(request) => self
.do_search_or_dse(&request)
.await
.unwrap_or_else(|e: LdapError| vec![make_search_error(e.code, e.message)]),
LdapOp::UnbindRequest => {
debug!(
"Unbind request for {}",
self.user_info
.as_ref()
.map(|u| u.user.as_str())
.unwrap_or("<not bound>"),
);
self.user_info = None;
// No need to notify on unbind (per rfc4511)
return None;
}
LdapOp::ModifyRequest(request) => self.do_modify_request(&request).await,
LdapOp::ExtendedRequest(request) => self.do_extended_request(&request).await,
LdapOp::AddRequest(request) => self
.create_user_or_group(request)
.await
.unwrap_or_else(|e: LdapError| vec![make_add_response(e.code, e.message)]),
LdapOp::DelRequest(request) => self
.delete_user_or_group(request)
.await
.unwrap_or_else(|e: LdapError| vec![make_del_response(e.code, e.message)]),
LdapOp::CompareRequest(request) => self
.do_compare(request)
.await
.unwrap_or_else(|e: LdapError| vec![make_search_error(e.code, e.message)]),
op => vec![make_extended_response(
LdapResultCode::UnwillingToPerform,
format!("Unsupported operation: {:#?}", op),
)],
})
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::infra::{
ldap::password::tests::make_bind_success,
test_utils::{MockTestBackendHandler, setup_default_schema},
};
use chrono::TimeZone;
use ldap3_proto::proto::{LdapBindCred, LdapWhoamiRequest};
use lldap_domain::{
types::{GroupDetails, GroupId, UserId},
uuid,
};
use lldap_domain_handlers::handler::*;
use mockall::predicate::eq;
use pretty_assertions::assert_eq;
use std::collections::HashSet;
use tokio;
pub fn make_user_search_request<S: Into<String>>(
filter: LdapFilter,
attrs: Vec<S>,
) -> LdapSearchRequest {
make_search_request::<S>("ou=people,Dc=example,dc=com", filter, attrs)
}
pub fn make_group_search_request<S: Into<String>>(
filter: LdapFilter,
attrs: Vec<S>,
) -> LdapSearchRequest {
make_search_request::<S>("ou=groups,dc=example,dc=com", filter, attrs)
}
pub async fn setup_bound_handler_with_group(
mut mock: MockTestBackendHandler,
group: &str,
) -> LdapHandler<MockTestBackendHandler> {
mock.expect_bind()
.with(eq(BindRequest {
name: UserId::new("test"),
password: "pass".to_string(),
}))
.return_once(|_| Ok(()));
let group = group.to_string();
mock.expect_get_user_groups()
.with(eq(UserId::new("test")))
.return_once(|_| {
let mut set = HashSet::new();
set.insert(GroupDetails {
group_id: GroupId(42),
display_name: group.into(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
attributes: Vec::new(),
});
Ok(set)
});
setup_default_schema(&mut mock);
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()),
};
assert_eq!(ldap_handler.do_bind(&request).await, make_bind_success());
ldap_handler
}
pub async fn setup_bound_readonly_handler(
mock: MockTestBackendHandler,
) -> LdapHandler<MockTestBackendHandler> {
setup_bound_handler_with_group(mock, "lldap_strict_readonly").await
}
pub async fn setup_bound_password_manager_handler(
mock: MockTestBackendHandler,
) -> LdapHandler<MockTestBackendHandler> {
setup_bound_handler_with_group(mock, "lldap_password_manager").await
}
pub async fn setup_bound_admin_handler(
mock: MockTestBackendHandler,
) -> LdapHandler<MockTestBackendHandler> {
setup_bound_handler_with_group(mock, "lldap_admin").await
}
#[tokio::test]
async fn test_whoami_empty() {
let mut ldap_handler =
LdapHandler::new_for_tests(MockTestBackendHandler::new(), "dc=example,dc=com");
let request = LdapOp::ExtendedRequest(LdapWhoamiRequest {}.into());
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_extended_response(
LdapResultCode::Success,
"".to_string(),
)])
);
}
#[tokio::test]
async fn test_whoami_bound() {
let mock = MockTestBackendHandler::new();
let mut ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapOp::ExtendedRequest(LdapWhoamiRequest {}.into());
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_extended_response(
LdapResultCode::Success,
"dn:uid=test,ou=people,dc=example,dc=com".to_string(),
)])
);
}
}
-7
View File
@@ -1,7 +0,0 @@
pub mod compare;
pub mod create;
pub mod delete;
pub mod handler;
pub mod modify;
pub mod password;
pub mod search;
-311
View File
@@ -1,311 +0,0 @@
use crate::{
domain::ldap::{
error::{LdapError, LdapResult},
utils::{LdapInfo, get_user_id_from_distinguished_name},
},
infra::ldap::{
handler::make_modify_response,
password::{self},
},
};
use ldap3_proto::proto::{LdapModify, LdapModifyRequest, LdapModifyType, LdapOp, LdapResultCode};
use lldap_access_control::UserReadableBackendHandler;
use lldap_auth::access_control::ValidationResults;
use lldap_domain::types::UserId;
use lldap_opaque_handler::OpaqueHandler;
async fn handle_modify_change(
opaque_handler: &impl OpaqueHandler,
user_id: UserId,
credentials: &ValidationResults,
user_is_admin: bool,
change: &LdapModify,
) -> LdapResult<()> {
if !change
.modification
.atype
.eq_ignore_ascii_case("userpassword")
|| change.operation != LdapModifyType::Replace
{
return Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: format!(
r#"Unsupported operation: `{:?}` for `{}`"#,
change.operation, change.modification.atype
),
});
}
if !credentials.can_change_password(&user_id, user_is_admin) {
return Err(LdapError {
code: LdapResultCode::InsufficentAccessRights,
message: format!(
r#"User `{}` cannot modify the password of user `{}`"#,
&credentials.user, &user_id
),
});
}
if let [value] = &change.modification.vals.as_slice() {
password::change_password(opaque_handler, user_id, value)
.await
.map_err(|e| LdapError {
code: LdapResultCode::Other,
message: format!("Error while changing the password: {:#?}", e),
})?;
} else {
return Err(LdapError {
code: LdapResultCode::InvalidAttributeSyntax,
message: format!(
r#"Wrong number of values for password attribute: {}"#,
change.modification.vals.len()
),
});
}
Ok(())
}
pub(crate) async fn handle_modify_request<'cred, UserBackendHandler>(
opaque_handler: &impl OpaqueHandler,
get_readable_handler: impl FnOnce(
&'cred ValidationResults,
UserId,
) -> Option<&'cred UserBackendHandler>,
ldap_info: &LdapInfo,
credentials: &'cred ValidationResults,
request: &LdapModifyRequest,
) -> LdapResult<Vec<LdapOp>>
where
// Note: ideally, get_readable_handler would take UserId by reference, but I couldn't make the lifetimes work.
UserBackendHandler: UserReadableBackendHandler + 'cred,
{
match get_user_id_from_distinguished_name(
&request.dn,
&ldap_info.base_dn,
&ldap_info.base_dn_str,
) {
Ok(uid) => {
let user_is_admin = get_readable_handler(credentials, uid.clone())
.ok_or_else(|| LdapError {
code: LdapResultCode::InsufficentAccessRights,
message: format!(
"User `{}` cannot modify user `{}`",
credentials.user.as_str(),
uid.as_str()
),
})?
.get_user_groups(&uid)
.await
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!("Internal error while requesting user's groups: {:#?}", e),
})?
.iter()
.any(|g| g.display_name == "lldap_admin".into());
for change in &request.changes {
handle_modify_change(
opaque_handler,
uid.clone(),
credentials,
user_is_admin,
change,
)
.await?
}
Ok(vec![make_modify_response(
LdapResultCode::Success,
String::new(),
)])
}
Err(e) => Err(LdapError {
code: LdapResultCode::InvalidDNSyntax,
message: format!("Invalid username: {}", e),
}),
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
use crate::infra::{
ldap::{
handler::tests::{
setup_bound_admin_handler, setup_bound_handler_with_group,
setup_bound_password_manager_handler,
},
password::tests::expect_password_change,
},
test_utils::MockTestBackendHandler,
};
use chrono::TimeZone;
use ldap3_proto::proto::LdapResult as LdapResultOp;
use lldap_domain::{
types::{GroupDetails, GroupId, GroupName, UserId},
uuid,
};
use mockall::predicate::eq;
use pretty_assertions::assert_eq;
use tokio;
fn setup_target_user_groups(
mock: &mut MockTestBackendHandler,
target_user: &str,
groups: Vec<&'static str>,
) {
mock.expect_get_user_groups()
.times(1)
.with(eq(UserId::from(target_user)))
.return_once(move |_| {
let mut g = HashSet::<GroupDetails>::new();
for group in groups {
g.insert(GroupDetails {
group_id: GroupId(42),
display_name: GroupName::from(group),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
attributes: Vec::new(),
});
}
Ok(g)
});
}
fn make_password_modify_request(target_user: &str) -> LdapModifyRequest {
LdapModifyRequest {
dn: format!("uid={},ou=people,dc=example,dc=com", target_user),
changes: vec![LdapModify {
operation: LdapModifyType::Replace,
modification: ldap3_proto::LdapPartialAttribute {
atype: "userPassword".to_string(),
vals: vec![b"tommy".to_vec()],
},
}],
}
}
fn make_modify_success_response() -> Vec<LdapOp> {
vec![LdapOp::ModifyResponse(LdapResultOp {
code: LdapResultCode::Success,
matcheddn: "".to_string(),
message: "".to_string(),
referral: vec![],
})]
}
fn make_modify_failure_response(code: LdapResultCode, message: &str) -> Vec<LdapOp> {
vec![LdapOp::ModifyResponse(LdapResultOp {
code,
matcheddn: "".to_string(),
message: message.to_string(),
referral: vec![],
})]
}
#[tokio::test]
async fn test_modify_password_of_regular_as_admin() {
let mut mock = MockTestBackendHandler::new();
setup_target_user_groups(&mut mock, "bob", Vec::new());
expect_password_change(&mut mock, "bob");
let ldap_handler = setup_bound_admin_handler(mock).await;
let request = make_password_modify_request("bob");
assert_eq!(
ldap_handler.do_modify_request(&request).await,
make_modify_success_response()
);
}
#[tokio::test]
async fn test_modify_password_of_regular_as_regular() {
let mut mock = MockTestBackendHandler::new();
setup_target_user_groups(&mut mock, "test", Vec::new());
expect_password_change(&mut mock, "test");
let ldap_handler = setup_bound_handler_with_group(mock, "regular").await;
let request = make_password_modify_request("test");
assert_eq!(
ldap_handler.do_modify_request(&request).await,
make_modify_success_response()
);
}
#[tokio::test]
async fn test_modify_password_of_regular_as_password_manager() {
let mut mock = MockTestBackendHandler::new();
setup_target_user_groups(&mut mock, "bob", Vec::new());
expect_password_change(&mut mock, "bob");
let ldap_handler = setup_bound_password_manager_handler(mock).await;
let request = make_password_modify_request("bob");
assert_eq!(
ldap_handler.do_modify_request(&request).await,
make_modify_success_response()
);
}
#[tokio::test]
async fn test_modify_password_of_admin_as_password_manager() {
let mut mock = MockTestBackendHandler::new();
setup_target_user_groups(&mut mock, "bob", vec!["lldap_admin"]);
let ldap_handler = setup_bound_password_manager_handler(mock).await;
let request = make_password_modify_request("bob");
assert_eq!(
ldap_handler.do_modify_request(&request).await,
make_modify_failure_response(
LdapResultCode::InsufficentAccessRights,
"User `test` cannot modify the password of user `bob`"
)
);
}
#[tokio::test]
async fn test_modify_password_of_other_regular_as_regular() {
let ldap_handler =
setup_bound_handler_with_group(MockTestBackendHandler::new(), "regular").await;
let request = make_password_modify_request("bob");
assert_eq!(
ldap_handler.do_modify_request(&request).await,
make_modify_failure_response(
LdapResultCode::InsufficentAccessRights,
"User `test` cannot modify user `bob`"
)
);
}
#[tokio::test]
async fn test_modify_password_of_admin_as_admin() {
let mut mock = MockTestBackendHandler::new();
setup_target_user_groups(&mut mock, "test", vec!["lldap_admin"]);
expect_password_change(&mut mock, "test");
let ldap_handler = setup_bound_admin_handler(mock).await;
let request = make_password_modify_request("test");
assert_eq!(
ldap_handler.do_modify_request(&request).await,
make_modify_success_response()
);
}
#[tokio::test]
async fn test_modify_password_invalid_number_of_values() {
let mut mock = MockTestBackendHandler::new();
setup_target_user_groups(&mut mock, "bob", Vec::new());
let ldap_handler = setup_bound_admin_handler(mock).await;
let request = {
let target_user = "bob";
LdapModifyRequest {
dn: format!("uid={},ou=people,dc=example,dc=com", target_user),
changes: vec![LdapModify {
operation: LdapModifyType::Replace,
modification: ldap3_proto::LdapPartialAttribute {
atype: "userPassword".to_string(),
vals: vec![b"tommy".to_vec(), b"other_value".to_vec()],
},
}],
}
};
assert_eq!(
ldap_handler.do_modify_request(&request).await,
make_modify_failure_response(
LdapResultCode::InvalidAttributeSyntax,
"Wrong number of values for password attribute: 2"
)
);
}
}
-573
View File
@@ -1,573 +0,0 @@
use crate::{
domain::ldap::{
error::{LdapError, LdapResult},
utils::{LdapInfo, get_user_id_from_distinguished_name},
},
infra::ldap::handler::make_extended_response,
};
use anyhow::Result;
use ldap3_proto::proto::{
LdapBindCred, LdapBindRequest, LdapOp, LdapPasswordModifyRequest, LdapResultCode,
};
use lldap_access_control::{AccessControlledBackendHandler, UserReadableBackendHandler};
use lldap_auth::access_control::ValidationResults;
use lldap_domain::types::UserId;
use lldap_domain_handlers::handler::{BackendHandler, BindRequest, LoginHandler};
use lldap_opaque_handler::OpaqueHandler;
pub(crate) async fn do_bind(
ldap_info: &LdapInfo,
request: &LdapBindRequest,
login_handler: &impl LoginHandler,
) -> LdapResult<UserId> {
if request.dn.is_empty() {
return Err(LdapError {
code: LdapResultCode::InappropriateAuthentication,
message: "Anonymous bind not allowed".to_string(),
});
}
let user_id = match get_user_id_from_distinguished_name(
&request.dn.to_ascii_lowercase(),
&ldap_info.base_dn,
&ldap_info.base_dn_str,
) {
Ok(s) => s,
Err(e) => {
return Err(LdapError {
code: LdapResultCode::NamingViolation,
message: e.to_string(),
});
}
};
let password = if let LdapBindCred::Simple(password) = &request.cred {
password
} else {
return Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: "SASL not supported".to_string(),
});
};
match login_handler
.bind(BindRequest {
name: user_id.clone(),
password: password.clone(),
})
.await
{
Ok(()) => Ok(user_id),
Err(_) => Err(LdapError {
code: LdapResultCode::InvalidCredentials,
message: "".to_string(),
}),
}
}
pub(crate) async fn change_password<B: OpaqueHandler>(
backend_handler: &B,
user: UserId,
password: &[u8],
) -> Result<()> {
use lldap_auth::*;
let mut rng = rand::rngs::OsRng;
let registration_start_request =
opaque::client::registration::start_registration(password, &mut rng)?;
let req = registration::ClientRegistrationStartRequest {
username: user.clone(),
registration_start_request: registration_start_request.message,
};
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,
&mut rng,
)?;
let req = registration::ClientRegistrationFinishRequest {
server_data: registration_start_response.server_data,
registration_upload: registration_finish.message,
};
backend_handler.registration_finish(req).await?;
Ok(())
}
pub(crate) async fn do_password_modification<Handler: BackendHandler>(
credentials: &ValidationResults,
ldap_info: &LdapInfo,
backend_handler: &AccessControlledBackendHandler<Handler>,
opaque_handler: &impl OpaqueHandler,
request: &LdapPasswordModifyRequest,
) -> LdapResult<Vec<LdapOp>> {
match (&request.user_identity, &request.new_password) {
(Some(user), Some(password)) => {
match get_user_id_from_distinguished_name(
&user.to_ascii_lowercase(),
&ldap_info.base_dn,
&ldap_info.base_dn_str,
) {
Ok(uid) => {
let user_is_admin = backend_handler
.get_readable_handler(credentials, &uid)
.expect("Unexpected permission error")
.get_user_groups(&uid)
.await
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!(
"Internal error while requesting user's groups: {:#?}",
e
),
})?
.iter()
.any(|g| g.display_name == "lldap_admin".into());
if !credentials.can_change_password(&uid, user_is_admin) {
Err(LdapError {
code: LdapResultCode::InsufficentAccessRights,
message: format!(
r#"User `{}` cannot modify the password of user `{}`"#,
&credentials.user, &uid
),
})
} else if let Err(e) =
change_password(opaque_handler, uid, password.as_bytes()).await
{
Err(LdapError {
code: LdapResultCode::Other,
message: format!("Error while changing the password: {:#?}", e),
})
} else {
Ok(vec![make_extended_response(
LdapResultCode::Success,
"".to_string(),
)])
}
}
Err(e) => Err(LdapError {
code: LdapResultCode::InvalidDNSyntax,
message: format!("Invalid username: {}", e),
}),
}
}
_ => Err(LdapError {
code: LdapResultCode::ConstraintViolation,
message: "Missing either user_id or password".to_string(),
}),
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::infra::{
ldap::handler::{
LdapHandler, make_modify_response,
tests::{
setup_bound_admin_handler, setup_bound_password_manager_handler,
setup_bound_readonly_handler,
},
},
test_utils::MockTestBackendHandler,
};
use chrono::TimeZone;
use ldap3_proto::proto::{
LdapBindResponse, LdapModify, LdapModifyRequest, LdapModifyType, LdapOp,
LdapResult as LdapResultOp,
};
use ldap3_proto::{LdapPartialAttribute, proto::LdapExtendedRequest};
use lldap_domain::{types::*, uuid};
use mockall::predicate::eq;
use pretty_assertions::assert_eq;
use std::collections::HashSet;
use tokio;
pub fn make_bind_result(code: LdapResultCode, message: &str) -> Vec<LdapOp> {
vec![LdapOp::BindResponse(LdapBindResponse {
res: LdapResultOp {
code,
matcheddn: "".to_string(),
message: message.to_string(),
referral: vec![],
},
saslcreds: None,
})]
}
pub fn make_bind_success() -> Vec<LdapOp> {
make_bind_result(LdapResultCode::Success, "")
}
pub fn expect_password_change(mock: &mut MockTestBackendHandler, user: &str) {
use lldap_auth::{opaque, registration};
let mut rng = rand::rngs::OsRng;
let registration_start_request =
opaque::client::registration::start_registration("password".as_bytes(), &mut rng)
.unwrap();
let request = registration::ClientRegistrationStartRequest {
username: user.into(),
registration_start_request: registration_start_request.message,
};
let start_response = opaque::server::registration::start_registration(
&opaque::server::ServerSetup::new(&mut rng),
request.registration_start_request,
&request.username,
)
.unwrap();
mock.expect_registration_start().times(1).return_once(|_| {
Ok(registration::ServerRegistrationStartResponse {
server_data: "".to_string(),
registration_response: start_response.message,
})
});
mock.expect_registration_finish()
.times(1)
.return_once(|_| Ok(()));
}
#[tokio::test]
async fn test_bind() {
let mut mock = MockTestBackendHandler::new();
mock.expect_bind()
.with(eq(lldap_domain_handlers::handler::BindRequest {
name: UserId::new("bob"),
password: "pass".to_string(),
}))
.times(1)
.return_once(|_| Ok(()));
mock.expect_get_user_groups()
.with(eq(UserId::new("bob")))
.return_once(|_| Ok(HashSet::new()));
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(),
cred: LdapBindCred::Simple("pass".to_string()),
});
assert_eq!(
ldap_handler.handle_ldap_message(request).await.unwrap(),
make_bind_success()
);
}
#[tokio::test]
async fn test_admin_bind() {
let mut mock = MockTestBackendHandler::new();
mock.expect_bind()
.with(eq(lldap_domain_handlers::handler::BindRequest {
name: UserId::new("test"),
password: "pass".to_string(),
}))
.times(1)
.return_once(|_| Ok(()));
mock.expect_get_user_groups()
.with(eq(UserId::new("test")))
.return_once(|_| {
let mut set = HashSet::new();
set.insert(GroupDetails {
group_id: GroupId(42),
display_name: "lldap_admin".into(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
attributes: Vec::new(),
});
Ok(set)
});
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()),
};
assert_eq!(ldap_handler.do_bind(&request).await, make_bind_success());
}
#[tokio::test]
async fn test_bind_invalid_dn() {
let mock = MockTestBackendHandler::new();
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(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await,
make_bind_result(
LdapResultCode::NamingViolation,
r#"Unexpected DN format. Got "cn=bob,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#
),
);
let request = LdapBindRequest {
dn: "uid=bob,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await,
make_bind_result(
LdapResultCode::NamingViolation,
r#"Unexpected DN format. Got "uid=bob,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#
),
);
let request = LdapBindRequest {
dn: "uid=bob,ou=groups,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await,
make_bind_result(
LdapResultCode::NamingViolation,
r#"Unexpected DN format. Got "uid=bob,ou=groups,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#
),
);
let request = LdapBindRequest {
dn: "uid=bob,ou=people,dc=example,dc=fr".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await,
make_bind_result(
LdapResultCode::NamingViolation,
r#"Not a subtree of the base tree"#
),
);
let request = LdapBindRequest {
dn: "uid=bob=test,ou=people,dc=example,dc=com".to_string(),
cred: LdapBindCred::Simple("pass".to_string()),
};
assert_eq!(
ldap_handler.do_bind(&request).await,
make_bind_result(
LdapResultCode::NamingViolation,
r#"Too many elements in distinguished name: "uid", "bob", "test""#
),
);
}
#[tokio::test]
async fn test_password_change() {
let mut mock = MockTestBackendHandler::new();
mock.expect_get_user_groups()
.with(eq(UserId::new("bob")))
.returning(|_| Ok(HashSet::new()));
expect_password_change(&mut mock, "bob");
let mut ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapOp::ExtendedRequest(
LdapPasswordModifyRequest {
user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()),
old_password: None,
new_password: Some("password".to_string()),
}
.into(),
);
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_extended_response(
LdapResultCode::Success,
"".to_string(),
)])
);
}
#[tokio::test]
async fn test_password_change_modify_request() {
let mut mock = MockTestBackendHandler::new();
mock.expect_get_user_groups()
.with(eq(UserId::new("bob")))
.returning(|_| Ok(HashSet::new()));
use lldap_auth::*;
let mut rng = rand::rngs::OsRng;
let registration_start_request =
opaque::client::registration::start_registration("password".as_bytes(), &mut rng)
.unwrap();
let request = registration::ClientRegistrationStartRequest {
username: "bob".into(),
registration_start_request: registration_start_request.message,
};
let start_response = opaque::server::registration::start_registration(
&opaque::server::ServerSetup::new(&mut rng),
request.registration_start_request,
&request.username,
)
.unwrap();
mock.expect_registration_start().times(1).return_once(|_| {
Ok(registration::ServerRegistrationStartResponse {
server_data: "".to_string(),
registration_response: start_response.message,
})
});
mock.expect_registration_finish()
.times(1)
.return_once(|_| Ok(()));
let mut ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapOp::ModifyRequest(LdapModifyRequest {
dn: "uid=bob,ou=people,dc=example,dc=com".to_string(),
changes: vec![LdapModify {
operation: LdapModifyType::Replace,
modification: LdapPartialAttribute {
atype: "userPassword".to_owned(),
vals: vec!["password".as_bytes().to_vec()],
},
}],
});
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_modify_response(
LdapResultCode::Success,
"".to_string(),
)])
);
}
#[tokio::test]
async fn test_password_change_password_manager() {
let mut mock = MockTestBackendHandler::new();
mock.expect_get_user_groups()
.with(eq(UserId::new("bob")))
.returning(|_| Ok(HashSet::new()));
use lldap_auth::*;
let mut rng = rand::rngs::OsRng;
let registration_start_request =
opaque::client::registration::start_registration("password".as_bytes(), &mut rng)
.unwrap();
let request = registration::ClientRegistrationStartRequest {
username: "bob".into(),
registration_start_request: registration_start_request.message,
};
let start_response = opaque::server::registration::start_registration(
&opaque::server::ServerSetup::new(&mut rng),
request.registration_start_request,
&request.username,
)
.unwrap();
mock.expect_registration_start().times(1).return_once(|_| {
Ok(registration::ServerRegistrationStartResponse {
server_data: "".to_string(),
registration_response: start_response.message,
})
});
mock.expect_registration_finish()
.times(1)
.return_once(|_| Ok(()));
let mut ldap_handler = setup_bound_password_manager_handler(mock).await;
let request = LdapOp::ExtendedRequest(
LdapPasswordModifyRequest {
user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()),
old_password: None,
new_password: Some("password".to_string()),
}
.into(),
);
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_extended_response(
LdapResultCode::Success,
"".to_string(),
)])
);
}
#[tokio::test]
async fn test_password_change_errors() {
let mut mock = MockTestBackendHandler::new();
mock.expect_get_user_groups()
.with(eq(UserId::new("bob")))
.returning(|_| Ok(HashSet::new()));
let mut ldap_handler = setup_bound_admin_handler(mock).await;
let request = LdapOp::ExtendedRequest(
LdapPasswordModifyRequest {
user_identity: None,
old_password: None,
new_password: None,
}
.into(),
);
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_extended_response(
LdapResultCode::ConstraintViolation,
"Missing either user_id or password".to_string(),
)])
);
let request = LdapOp::ExtendedRequest(
LdapPasswordModifyRequest {
user_identity: Some("uid=bob,ou=groups,ou=people,dc=example,dc=com".to_string()),
old_password: None,
new_password: Some("password".to_string()),
}
.into(),
);
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_extended_response(
LdapResultCode::InvalidDNSyntax,
r#"Invalid username: Unexpected DN format. Got "uid=bob,ou=groups,ou=people,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#.to_string(),
)])
);
let request = LdapOp::ExtendedRequest(LdapExtendedRequest {
name: "test".to_string(),
value: None,
});
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_extended_response(
LdapResultCode::UnwillingToPerform,
"Unsupported extended operation: test".to_string(),
)])
);
}
#[tokio::test]
async fn test_password_change_unauthorized_password_manager() {
let mut mock = MockTestBackendHandler::new();
let mut groups = HashSet::new();
groups.insert(GroupDetails {
group_id: GroupId(0),
display_name: "lldap_admin".into(),
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
attributes: Vec::new(),
});
mock.expect_get_user_groups()
.with(eq(UserId::new("bob")))
.times(1)
.return_once(|_| Ok(groups));
let mut ldap_handler = setup_bound_password_manager_handler(mock).await;
let request = LdapOp::ExtendedRequest(
LdapPasswordModifyRequest {
user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()),
old_password: Some("pass".to_string()),
new_password: Some("password".to_string()),
}
.into(),
);
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_extended_response(
LdapResultCode::InsufficentAccessRights,
"User `test` cannot modify the password of user `bob`".to_string(),
)])
);
}
#[tokio::test]
async fn test_password_change_unauthorized_readonly() {
let mut mock = MockTestBackendHandler::new();
mock.expect_get_user_groups()
.with(eq(UserId::new("bob")))
.times(1)
.return_once(|_| Ok(HashSet::new()));
let mut ldap_handler = setup_bound_readonly_handler(mock).await;
let request = LdapOp::ExtendedRequest(
LdapPasswordModifyRequest {
user_identity: Some("uid=bob,ou=people,dc=example,dc=com".to_string()),
old_password: Some("pass".to_string()),
new_password: Some("password".to_string()),
}
.into(),
);
assert_eq!(
ldap_handler.handle_ldap_message(request).await,
Some(vec![make_extended_response(
LdapResultCode::InsufficentAccessRights,
"User `test` cannot modify the password of user `bob`".to_string(),
)])
);
}
}
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -1,7 +1,4 @@
use crate::infra::{
configuration::{Configuration, LdapsOptions},
ldap::handler::LdapHandler,
};
use crate::infra::configuration::{Configuration, LdapsOptions};
use actix_rt::net::TcpStream;
use actix_server::ServerBuilder;
use actix_service::{ServiceFactoryExt, fn_service};
@@ -10,6 +7,7 @@ use ldap3_proto::{LdapCodec, control::LdapControl, proto::LdapMsg, proto::LdapOp
use lldap_access_control::AccessControlledBackendHandler;
use lldap_domain::types::AttributeName;
use lldap_domain_handlers::handler::{BackendHandler, LoginHandler};
use lldap_ldap::LdapHandler;
use lldap_opaque_handler::OpaqueHandler;
use rustls::PrivateKey;
use tokio_rustls::TlsAcceptor as RustlsTlsAcceptor;
-4
View File
@@ -6,13 +6,9 @@ pub mod db_cleaner;
pub mod graphql;
pub mod healthcheck;
pub mod jwt_sql_tables;
pub mod ldap;
pub mod ldap_server;
pub mod logging;
pub mod mail;
pub mod sql_backend_handler;
pub mod tcp_backend_handler;
pub mod tcp_server;
#[cfg(test)]
pub mod test_utils;
-133
View File
@@ -1,133 +0,0 @@
use lldap_domain::{
requests::{
CreateAttributeRequest, CreateGroupRequest, CreateUserRequest, UpdateGroupRequest,
UpdateUserRequest,
},
schema::{AttributeList, AttributeSchema, Schema},
types::{
AttributeName, AttributeType, Group, GroupDetails, GroupId, LdapObjectClass, User,
UserAndGroups, UserId,
},
};
use lldap_domain_handlers::handler::{
BackendHandler, BindRequest, GroupBackendHandler, GroupListerBackendHandler,
GroupRequestFilter, LoginHandler, ReadSchemaBackendHandler, SchemaBackendHandler,
UserBackendHandler, UserListerBackendHandler, UserRequestFilter,
};
use lldap_domain_model::error::Result;
use lldap_opaque_handler::{OpaqueHandler, login, registration};
use async_trait::async_trait;
use std::collections::HashSet;
mockall::mock! {
pub TestBackendHandler{}
impl Clone for TestBackendHandler {
fn clone(&self) -> Self;
}
#[async_trait]
impl LoginHandler for TestBackendHandler {
async fn bind(&self, request: BindRequest) -> Result<()>;
}
#[async_trait]
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, request: CreateGroupRequest) -> Result<GroupId>;
async fn delete_group(&self, group_id: GroupId) -> Result<()>;
}
#[async_trait]
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<()>;
async fn delete_user(&self, user_id: &UserId) -> Result<()>;
async fn get_user_groups(&self, user_id: &UserId) -> Result<HashSet<GroupDetails>>;
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_trait]
impl ReadSchemaBackendHandler for TestBackendHandler {
async fn get_schema(&self) -> Result<Schema>;
}
#[async_trait]
impl SchemaBackendHandler for TestBackendHandler {
async fn add_user_attribute(&self, request: CreateAttributeRequest) -> Result<()>;
async fn add_group_attribute(&self, request: CreateAttributeRequest) -> Result<()>;
async fn delete_user_attribute(&self, name: &AttributeName) -> Result<()>;
async fn delete_group_attribute(&self, name: &AttributeName) -> Result<()>;
async fn add_user_object_class(&self, request: &LdapObjectClass) -> Result<()>;
async fn add_group_object_class(&self, request: &LdapObjectClass) -> Result<()>;
async fn delete_user_object_class(&self, name: &LdapObjectClass) -> Result<()>;
async fn delete_group_object_class(&self, name: &LdapObjectClass) -> Result<()>;
}
#[async_trait]
impl BackendHandler for TestBackendHandler {}
#[async_trait]
impl OpaqueHandler for TestBackendHandler {
async fn login_start(
&self,
request: login::ClientLoginStartRequest
) -> Result<login::ServerLoginStartResponse>;
async fn login_finish(&self, request: login::ClientLoginFinishRequest) -> Result<UserId>;
async fn registration_start(
&self,
request: registration::ClientRegistrationStartRequest
) -> Result<registration::ServerRegistrationStartResponse>;
async fn registration_finish(
&self,
request: registration::ClientRegistrationFinishRequest
) -> Result<()>;
}
}
pub fn setup_default_schema(mock: &mut MockTestBackendHandler) {
mock.expect_get_schema().returning(|| {
Ok(Schema {
user_attributes: AttributeList {
attributes: vec![
AttributeSchema {
name: "avatar".into(),
attribute_type: AttributeType::JpegPhoto,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
is_readonly: false,
},
AttributeSchema {
name: "first_name".into(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
is_readonly: false,
},
AttributeSchema {
name: "last_name".into(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: true,
is_readonly: false,
},
],
},
group_attributes: AttributeList {
attributes: Vec::new(),
},
extra_user_object_classes: vec![LdapObjectClass::from("customUserClass")],
extra_group_object_classes: Vec::new(),
})
});
}