mirror of
https://github.com/lldap/lldap.git
synced 2026-07-25 17:58:44 +00:00
server: extract ldap operations to a crate
This commit is contained in:
committed by
nitnelave
parent
a49ddeaa02
commit
ee21d83056
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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,
|
||||
))
|
||||
})
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod error;
|
||||
pub mod group;
|
||||
pub mod user;
|
||||
pub mod utils;
|
||||
@@ -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,
|
||||
))
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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![],
|
||||
})])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
)])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user