server: Add support for creating a user through LDAP

This commit is contained in:
Valentin Tolmer 2022-10-20 10:01:34 +02:00 committed by nitnelave
parent 2477439ecc
commit 27144ee37e
2 changed files with 156 additions and 3 deletions

View File

@ -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<Self> {
// 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<Vec<u8>> for JpegPhoto { impl TryFrom<Vec<u8>> for JpegPhoto {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(bytes: Vec<u8>) -> anyhow::Result<Self> { fn try_from(bytes: Vec<u8>) -> anyhow::Result<Self> {

View File

@ -1,6 +1,10 @@
use std::collections::HashMap;
use crate::{ use crate::{
domain::{ domain::{
handler::{BackendHandler, BindRequest, LoginHandler, UserId}, handler::{
BackendHandler, BindRequest, CreateUserRequest, JpegPhoto, LoginHandler, UserId,
},
ldap::{ ldap::{
error::{LdapError, LdapResult}, error::{LdapError, LdapResult},
group::get_groups_list, group::get_groups_list,
@ -15,8 +19,8 @@ use crate::{
}; };
use anyhow::Result; use anyhow::Result;
use ldap3_proto::proto::{ use ldap3_proto::proto::{
LdapBindCred, LdapBindRequest, LdapBindResponse, LdapExtendedRequest, LdapExtendedResponse, LdapAddRequest, LdapBindCred, LdapBindRequest, LdapBindResponse, LdapExtendedRequest,
LdapFilter, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest, LdapExtendedResponse, LdapFilter, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest,
LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, LdapSearchResultEntry, LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, LdapSearchResultEntry,
LdapSearchScope, 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 { fn make_extended_response(code: LdapResultCode, message: String) -> LdapOp {
LdapOp::ExtendedResponse(LdapExtendedResponse { LdapOp::ExtendedResponse(LdapExtendedResponse {
res: LdapResultOp { res: LdapResultOp {
@ -432,6 +445,90 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
Ok(results) Ok(results)
} }
async fn do_create_user(&self, request: LdapAddRequest) -> LdapResult<Vec<LdapOp>> {
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<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>> = request
.attributes
.into_iter()
.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)
};
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<Vec<LdapOp>> { pub async fn handle_ldap_message(&mut self, ldap_op: LdapOp) -> Option<Vec<LdapOp>> {
Some(match ldap_op { Some(match ldap_op {
LdapOp::BindRequest(request) => { LdapOp::BindRequest(request) => {
@ -456,6 +553,10 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
return None; return None;
} }
LdapOp::ExtendedRequest(request) => self.do_extended_request(&request).await, LdapOp::ExtendedRequest(request) => 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( op => vec![make_extended_response(
LdapResultCode::UnwillingToPerform, LdapResultCode::UnwillingToPerform,
format!("Unsupported operation: {:#?}", op), 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() })
);
}
} }