diff --git a/server/src/domain/handler.rs b/server/src/domain/handler.rs index b394494..8b1a622 100644 --- a/server/src/domain/handler.rs +++ b/server/src/domain/handler.rs @@ -98,6 +98,16 @@ impl From<&JpegPhoto> for sea_query::Value { } } +impl TryFrom<&[u8]> for JpegPhoto { + type Error = anyhow::Error; + fn try_from(bytes: &[u8]) -> anyhow::Result { + // Confirm that it's a valid Jpeg, then store only the bytes. + image::io::Reader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg) + .decode()?; + Ok(JpegPhoto(bytes.to_vec())) + } +} + impl TryFrom> for JpegPhoto { type Error = anyhow::Error; fn try_from(bytes: Vec) -> anyhow::Result { diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 34ec1a5..bddfcfd 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -1,6 +1,10 @@ +use std::collections::HashMap; + use crate::{ domain::{ - handler::{BackendHandler, BindRequest, LoginHandler, UserId}, + handler::{ + BackendHandler, BindRequest, CreateUserRequest, JpegPhoto, LoginHandler, UserId, + }, ldap::{ error::{LdapError, LdapResult}, group::get_groups_list, @@ -15,8 +19,8 @@ use crate::{ }; use anyhow::Result; use ldap3_proto::proto::{ - LdapBindCred, LdapBindRequest, LdapBindResponse, LdapExtendedRequest, LdapExtendedResponse, - LdapFilter, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest, + LdapAddRequest, LdapBindCred, LdapBindRequest, LdapBindResponse, LdapExtendedRequest, + LdapExtendedResponse, LdapFilter, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest, LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, LdapSearchResultEntry, LdapSearchScope, }; @@ -82,6 +86,15 @@ fn make_search_error(code: LdapResultCode, message: String) -> LdapOp { }) } +fn make_add_error(code: LdapResultCode, message: String) -> LdapOp { + LdapOp::AddResponse(LdapResultOp { + code, + matcheddn: "".to_string(), + message, + referral: vec![], + }) +} + fn make_extended_response(code: LdapResultCode, message: String) -> LdapOp { LdapOp::ExtendedResponse(LdapExtendedResponse { res: LdapResultOp { @@ -432,6 +445,90 @@ impl LdapHandler LdapResult> { + if !self + .user_info + .as_ref() + .map(|u| u.is_admin()) + .unwrap_or(false) + { + return Err(LdapError { + code: LdapResultCode::InsufficentAccessRights, + message: "Unauthorized write".to_string(), + }); + } + let user_id = get_user_id_from_distinguished_name( + &request.dn, + &self.ldap_info.base_dn, + &self.ldap_info.base_dn_str, + )?; + 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> = request + .attributes + .into_iter() + .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) + }; + self.backend_handler + .create_user(CreateUserRequest { + user_id, + email: get_attribute("mail") + .or_else(|| get_attribute("email")) + .transpose()? + .unwrap_or_default(), + display_name: get_attribute("cn").transpose()?, + first_name: get_attribute("givenname").transpose()?, + last_name: get_attribute("sn").transpose()?, + avatar: attributes + .get("avatar") + .map(Vec::as_slice) + .map(JpegPhoto::try_from) + .transpose() + .map_err(|e| LdapError { + code: LdapResultCode::ConstraintViolation, + message: format!("Invalid JPEG photo: {:#?}", e), + })?, + }) + .await + .map_err(|e| LdapError { + code: LdapResultCode::OperationsError, + message: format!("Could not create user: {:#?}", e), + })?; + Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) + } + pub async fn handle_ldap_message(&mut self, ldap_op: LdapOp) -> Option> { Some(match ldap_op { LdapOp::BindRequest(request) => { @@ -456,6 +553,10 @@ impl LdapHandler self.do_extended_request(&request).await, + LdapOp::AddRequest(request) => self + .do_create_user(request) + .await + .unwrap_or_else(|e: LdapError| vec![make_add_error(e.code, e.message)]), op => vec![make_extended_response( LdapResultCode::UnwillingToPerform, format!("Unsupported operation: {:#?}", op), @@ -1930,4 +2031,46 @@ 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: "".to_owned(), + 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(request).await, + Ok(vec![make_add_error(LdapResultCode::Success, String::new())]) + ); + } + + #[tokio::test] + async fn test_create_user_wrong_ou() { + let ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).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"Bob".to_vec()], + }], + }; + assert_eq!( + ldap_handler.do_create_user(request).await, + Err(LdapError{ code: LdapResultCode::InvalidDNSyntax, message: r#"Unexpected DN format. Got "uid=bob,ou=groups,dc=example,dc=com", expected: "uid=id,ou=people,dc=example,dc=com""#.to_string() }) + ); + } }