diff --git a/server/src/infra/ldap/create.rs b/server/src/infra/ldap/create.rs new file mode 100644 index 0000000..d9142eb --- /dev/null +++ b/server/src/infra/ldap/create.rs @@ -0,0 +1,258 @@ +use crate::{ + domain::{ + deserialize, + ldap::{ + error::{LdapError, LdapResult}, + utils::{LdapInfo, UserOrGroupName, get_user_or_group_id_from_distinguished_name}, + }, + }, + infra::{access_control::AdminBackendHandler, ldap::handler::make_add_error}, +}; +use ldap3_proto::proto::{ + LdapAddRequest, LdapAttribute, LdapOp, LdapPartialAttribute, LdapResultCode, +}; +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> { + 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, +) -> LdapResult> { + fn parse_attribute(mut attr: LdapPartialAttribute) -> LdapResult<(String, Vec)> { + 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> = attributes + .into_iter() + .filter(|a| !a.atype.eq_ignore_ascii_case("objectclass")) + .map(parse_attribute) + .collect::>()?; + fn decode_attribute_value(val: &[u8]) -> LdapResult { + 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 = 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_error(LdapResultCode::Success, String::new())]) +} + +#[instrument(skip_all, level = "debug")] +async fn create_group( + backend_handler: &impl AdminBackendHandler, + group_name: GroupName, + _attributes: Vec, +) -> LdapResult> { + 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_error(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_error(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_error(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_error(LdapResultCode::Success, String::new())]) + ); + } +} diff --git a/server/src/infra/ldap/handler.rs b/server/src/infra/ldap/handler.rs index 344502b..0d71a81 100644 --- a/server/src/infra/ldap/handler.rs +++ b/server/src/infra/ldap/handler.rs @@ -1,18 +1,16 @@ use crate::{ domain::{ - deserialize, ldap::{ error::{LdapError, LdapResult}, utils::{ - LdapInfo, UserOrGroupName, get_user_id_from_distinguished_name, - get_user_or_group_id_from_distinguished_name, parse_distinguished_name, + LdapInfo, get_user_id_from_distinguished_name, parse_distinguished_name, }, }, opaque_handler::OpaqueHandler, }, infra::{ access_control::{ - AccessControlledBackendHandler, AdminBackendHandler, UserReadableBackendHandler, + AccessControlledBackendHandler, UserReadableBackendHandler, }, ldap::{ password::{self, do_password_modification}, @@ -21,25 +19,22 @@ use crate::{ make_search_success, root_dse_response, }, compare, + create, }, }, }; use ldap3_proto::proto::{ - LdapAddRequest, LdapAttribute, LdapBindRequest, LdapBindResponse, LdapCompareRequest, + LdapAddRequest, LdapBindRequest, LdapBindResponse, LdapCompareRequest, LdapExtendedRequest, LdapExtendedResponse, LdapFilter, LdapModify, LdapModifyRequest, - LdapModifyType, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest, + LdapModifyType, LdapOp, LdapPasswordModifyRequest, LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, OID_PASSWORD_MODIFY, OID_WHOAMI, }; use lldap_auth::access_control::ValidationResults; -use lldap_domain::{ - requests::{CreateGroupRequest, CreateUserRequest}, - types::{Attribute, AttributeName, AttributeType, Email, GroupName, UserId}, -}; +use lldap_domain::types::{AttributeName, UserId}; use lldap_domain_handlers::handler::{BackendHandler, LoginHandler}; -use std::collections::HashMap; use tracing::{debug, instrument}; -fn make_add_error(code: LdapResultCode, message: String) -> LdapOp { +pub(crate) fn make_add_error(code: LdapResultCode, message: String) -> LdapOp { LdapOp::AddResponse(LdapResultOp { code, matcheddn: "".to_string(), @@ -338,7 +333,7 @@ impl LdapHandler LdapResult> { + pub async fn create_user_or_group(&self, request: LdapAddRequest) -> LdapResult> { let backend_handler = self .user_info .as_ref() @@ -347,143 +342,7 @@ impl LdapHandler { - self.do_create_user(backend_handler, user_id, request.attributes) - .await - } - UserOrGroupName::Group(group_name) => { - self.do_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 do_create_user( - &self, - backend_handler: &impl AdminBackendHandler, - user_id: UserId, - attributes: Vec, - ) -> LdapResult> { - fn parse_attribute(mut attr: LdapPartialAttribute) -> LdapResult<(String, Vec)> { - 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> = attributes - .into_iter() - .filter(|a| !a.atype.eq_ignore_ascii_case("objectclass")) - .map(parse_attribute) - .collect::>()?; - fn decode_attribute_value(val: &[u8]) -> LdapResult { - 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 = 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_error(LdapResultCode::Success, String::new())]) - } - - #[instrument(skip_all, level = "debug")] - async fn do_create_group( - &self, - backend_handler: &impl AdminBackendHandler, - group_name: GroupName, - _attributes: Vec, - ) -> LdapResult> { - 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_error(LdapResultCode::Success, String::new())]) + create::create_user_or_group(backend_handler, &self.ldap_info, request).await } #[instrument(skip_all, level = "debug")] @@ -518,7 +377,7 @@ impl LdapHandler self.do_modify_request(&request).await, LdapOp::ExtendedRequest(request) => self.do_extended_request(&request).await, LdapOp::AddRequest(request) => self - .do_create_user_or_group(request) + .create_user_or_group(request) .await .unwrap_or_else(|e: LdapError| vec![make_add_error(e.code, e.message)]), LdapOp::CompareRequest(request) => self @@ -642,91 +501,4 @@ pub mod tests { )]) ); } - - #[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.do_create_user_or_group(request).await, - Ok(vec![make_add_error(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.do_create_user_or_group(request).await, - Ok(vec![make_add_error(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.do_create_user_or_group(request).await, - Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) - ); - } - } diff --git a/server/src/infra/ldap/mod.rs b/server/src/infra/ldap/mod.rs index 8195949..d99478c 100644 --- a/server/src/infra/ldap/mod.rs +++ b/server/src/infra/ldap/mod.rs @@ -1,4 +1,5 @@ pub mod compare; +pub mod create; pub mod handler; pub mod password; pub mod search;