Merge branch 'main' into main

This commit is contained in:
nitnelave 2022-04-29 09:16:22 +02:00 committed by GitHub
commit 060a082368
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 215 additions and 66 deletions

View File

@ -20,10 +20,12 @@ jobs:
- id: skip_check - id: skip_check
uses: fkirc/skip-duplicate-actions@master uses: fkirc/skip-duplicate-actions@master
with: with:
concurrent_skipping: 'never' concurrent_skipping: 'outdated_runs'
skip_after_successful_duplicate: 'true' skip_after_successful_duplicate: 'true'
paths_ignore: '["**/*.md", "**/docs/**", "example_configs/**", "*.sh"]' paths_ignore: '["**/*.md", "**/docs/**", "example_configs/**", "*.sh"]'
do_not_skip: '["workflow_dispatch", "schedule"]' do_not_skip: '["workflow_dispatch", "schedule"]'
cancel_others: true
test: test:
name: cargo test name: cargo test
needs: pre_job needs: pre_job
@ -97,6 +99,7 @@ jobs:
coverage: coverage:
name: Code coverage name: Code coverage
needs: pre_job needs: pre_job
if: ${{ needs.pre_job.outputs.should_skip != 'true' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources

View File

@ -100,7 +100,7 @@ impl Component for App {
html! { html! {
<div class="container shadow-sm py-3"> <div class="container shadow-sm py-3">
{self.view_banner()} {self.view_banner()}
<div class="row justify-content-center"> <div class="row justify-content-center" style="padding-bottom: 80px;">
<div class="shadow-sm py-3" style="max-width: 1000px"> <div class="shadow-sm py-3" style="max-width: 1000px">
<Router<AppRoute> <Router<AppRoute>
render = Router::render(move |s| Self::dispatch_route(s, &link, is_admin)) render = Router::render(move |s| Self::dispatch_route(s, &link, is_admin))

View File

@ -0,0 +1,90 @@
# Configuration pour Dolibarr
This example will help you to create user in dolibarr from your users in your lldap server from a specific group and to login with the password from the lldap server.
## To connect ldap->dolibarr
In Dolibarr, install the LDAP module from Home -> Modules/Applications
Go to the configuration of this module and fill it like this:
- Users and groups synchronization: `LDAP -> Dolibarr`
- Contacts' synchronization: `No`
- Type: `OpenLdap`
- Version: `Version 3`
- Primary server: `ldap://example.com`
- Secondary server: `Empty`
- Server port: port `3890`
- Server DN: `dc=example,dc=com`
- Use TLS: `No`
- Administrator DN: `cn=admin,ou=people,dc=example,dc=com`
- Administrator password: `secret`
Click on modify then "TEST LDAP CONNECTION".
You should get this result on the bottom:
```
TCP connect to LDAP server successful (Server=ldap://example.com, Port=389)
Connect/Authenticate to LDAP server successful (Server=ldap://example.com, Port=389, Admin=cn=admin,ou=people,dc=example,dc=com, Password=**********)
LDAP server configured for version 3
```
And two new tabs will appear on the top:
Users and Groups
We will use only Users in this example to get the users we want to import.
The tab Groups would be to import groups.
Click on the Users tab and fill it like this:
- Users' DN: `ou=people,dc=example,dc=com`
- List of objectClass: `person`
- Search filter: `memberOf=cn=yournamegroup,ou=groups,dc=example,dc=com`
(or if you don't have a group for your users, leave the search filter empty)
- Full name: `cn`
- Name: `sn`
- First name: `givenname`
- Login `uid`
- Email address `mail`
Click on "MODIFY" and then on "TEST A LDAP SEARCH"
You should get the number of users in the group or all users if you didn't use a filter.
## To import ldap users into the dolibarr database (needed to login with those users):
Navigate to Users & Groups -> New Users
Click on the blank form "Users in LDAP database", you will get the list of the users in the group filled above. With the "GET" button, you will import the selected user.
## To enable LDAP login:
Modify your `conf.php` in your dolibarr folder in `htdocs/conf`
Replace
```
// Authentication settings
$dolibarr_main_authentication='dolibarr';
```
with:
```
// Authentication settings
// Only add "ldap" to only login using the ldap server, or/and "dolibar" to compare with local users. In any case, you need to have the user existing in dolibarr.
$dolibarr_main_authentication='ldap,dolibarr';
$dolibarr_main_auth_ldap_host='ldap://127.0.0.1:3890';
$dolibarr_main_auth_ldap_port='3890';
$dolibarr_main_auth_ldap_version='3';
$dolibarr_main_auth_ldap_servertype='openldap';
$dolibarr_main_auth_ldap_login_attribute='cn';
$dolibarr_main_auth_ldap_dn='ou=people,dc=example,dc=com';
$dolibarr_main_auth_ldap_admin_login='cn=admin,ou=people,dc=example,dc=com';
$dolibarr_main_auth_ldap_admin_pass='secret';
```
You can add this line to enable debug in case anything is wrong:
```
$dolibarr_main_auth_ldap_debug='true';
```

View File

@ -13,6 +13,9 @@ use ldap3_server::proto::{
}; };
use log::{debug, warn}; use log::{debug, warn};
#[derive(Debug, PartialEq, Eq, Clone)]
struct LdapDn(String);
fn make_dn_pair<I>(mut iter: I) -> Result<(String, String)> fn make_dn_pair<I>(mut iter: I) -> Result<(String, String)>
where where
I: Iterator<Item = String>, I: Iterator<Item = String>,
@ -94,24 +97,24 @@ fn get_user_id_from_distinguished_name(
} }
} }
fn get_user_attribute(user: &User, attribute: &str, dn: &str) -> Result<Vec<String>> { fn get_user_attribute(user: &User, attribute: &str, dn: &str) -> Result<Option<Vec<String>>> {
match attribute.to_lowercase().as_str() { Ok(Some(match attribute.to_lowercase().as_str() {
"objectclass" => Ok(vec![ "objectclass" => vec![
"inetOrgPerson".to_string(), "inetOrgPerson".to_string(),
"posixAccount".to_string(), "posixAccount".to_string(),
"mailAccount".to_string(), "mailAccount".to_string(),
"person".to_string(), "person".to_string(),
]), ],
"dn" => Ok(vec![dn.to_string()]), "dn" => vec![dn.to_string()],
"uid" => Ok(vec![user.user_id.to_string()]), "uid" => vec![user.user_id.to_string()],
"mail" => Ok(vec![user.email.clone()]), "mail" => vec![user.email.clone()],
"givenname" => Ok(vec![user.first_name.clone()]), "givenname" => vec![user.first_name.clone()],
"sn" => Ok(vec![user.last_name.clone()]), "sn" => vec![user.last_name.clone()],
"cn" | "displayname" => Ok(vec![user.display_name.clone()]), "cn" | "displayname" => vec![user.display_name.clone()],
"createtimestamp" | "modifytimestamp" => Ok(vec![user.creation_date.to_rfc3339()]), "createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339()],
"1.1" => Ok(vec![]), "1.1" => return Ok(None),
_ => bail!("Unsupported user attribute: {}", attribute), _ => bail!("Unsupported user attribute: {}", attribute),
} }))
} }
fn make_ldap_search_user_result_entry( fn make_ldap_search_user_result_entry(
@ -124,47 +127,63 @@ fn make_ldap_search_user_result_entry(
dn: dn.clone(), dn: dn.clone(),
attributes: attributes attributes: attributes
.iter() .iter()
.map(|a| { .filter_map(|a| {
Ok(LdapPartialAttribute { let values = match get_user_attribute(&user, a, &dn) {
Err(e) => return Some(Err(e)),
Ok(v) => v,
}?;
Some(Ok(LdapPartialAttribute {
atype: a.to_string(), atype: a.to_string(),
vals: get_user_attribute(&user, a, &dn)?, vals: values,
}) }))
}) })
.collect::<Result<Vec<LdapPartialAttribute>>>()?, .collect::<Result<Vec<LdapPartialAttribute>>>()?,
}) })
} }
fn get_group_attribute(group: &Group, base_dn_str: &str, attribute: &str) -> Result<Vec<String>> { fn get_group_attribute(
match attribute.to_lowercase().as_str() { group: &Group,
"objectclass" => Ok(vec!["groupOfUniqueNames".to_string()]), base_dn_str: &str,
"dn" => Ok(vec![format!( attribute: &str,
user_filter: &Option<&UserId>,
) -> Result<Option<Vec<String>>> {
Ok(Some(match attribute.to_lowercase().as_str() {
"objectclass" => vec!["groupOfUniqueNames".to_string()],
"dn" => vec![format!(
"cn={},ou=groups,{}", "cn={},ou=groups,{}",
group.display_name, base_dn_str group.display_name, base_dn_str
)]), )],
"cn" | "uid" => Ok(vec![group.display_name.clone()]), "cn" | "uid" => vec![group.display_name.clone()],
"member" | "uniquemember" => Ok(group "member" | "uniquemember" => group
.users .users
.iter() .iter()
.filter(|u| user_filter.map(|f| *u == f).unwrap_or(true))
.map(|u| format!("cn={},ou=people,{}", u, base_dn_str)) .map(|u| format!("cn={},ou=people,{}", u, base_dn_str))
.collect()), .collect(),
"1.1" => return Ok(None),
_ => bail!("Unsupported group attribute: {}", attribute), _ => bail!("Unsupported group attribute: {}", attribute),
} }))
} }
fn make_ldap_search_group_result_entry( fn make_ldap_search_group_result_entry(
group: Group, group: Group,
base_dn_str: &str, base_dn_str: &str,
attributes: &[String], attributes: &[String],
user_filter: &Option<&UserId>,
) -> Result<LdapSearchResultEntry> { ) -> Result<LdapSearchResultEntry> {
Ok(LdapSearchResultEntry { Ok(LdapSearchResultEntry {
dn: format!("cn={},ou=groups,{}", group.display_name, base_dn_str), dn: format!("cn={},ou=groups,{}", group.display_name, base_dn_str),
attributes: attributes attributes: attributes
.iter() .iter()
.map(|a| { .filter_map(|a| {
Ok(LdapPartialAttribute { let values = match get_group_attribute(&group, base_dn_str, a, user_filter) {
Err(e) => return Some(Err(e)),
Ok(v) => v,
}?;
Some(Ok(LdapPartialAttribute {
atype: a.to_string(), atype: a.to_string(),
vals: get_group_attribute(&group, base_dn_str, a)?, vals: values,
}) }))
}) })
.collect::<Result<Vec<LdapPartialAttribute>>>()?, .collect::<Result<Vec<LdapPartialAttribute>>>()?,
}) })
@ -265,17 +284,19 @@ fn root_dse_response(base_dn: &str) -> LdapOp {
} }
pub struct LdapHandler<Backend: BackendHandler + LoginHandler + OpaqueHandler> { pub struct LdapHandler<Backend: BackendHandler + LoginHandler + OpaqueHandler> {
dn: UserId, dn: LdapDn,
user_id: UserId,
backend_handler: Backend, backend_handler: Backend,
pub base_dn: Vec<(String, String)>, pub base_dn: Vec<(String, String)>,
base_dn_str: String, base_dn_str: String,
ldap_user_dn: UserId, ldap_user_dn: LdapDn,
} }
impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend> { impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend> {
pub fn new(backend_handler: Backend, ldap_base_dn: String, ldap_user_dn: UserId) -> Self { pub fn new(backend_handler: Backend, ldap_base_dn: String, ldap_user_dn: UserId) -> Self {
Self { Self {
dn: UserId::new("unauthenticated"), dn: LdapDn("unauthenticated".to_string()),
user_id: UserId::new("unauthenticated"),
backend_handler, backend_handler,
base_dn: parse_distinguished_name(&ldap_base_dn).unwrap_or_else(|_| { base_dn: parse_distinguished_name(&ldap_base_dn).unwrap_or_else(|_| {
panic!( panic!(
@ -283,7 +304,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
ldap_base_dn ldap_base_dn
) )
}), }),
ldap_user_dn: UserId::new(&format!("cn={},ou=people,{}", ldap_user_dn, &ldap_base_dn)), ldap_user_dn: LdapDn(format!("cn={},ou=people,{}", ldap_user_dn, &ldap_base_dn)),
base_dn_str: ldap_base_dn, base_dn_str: ldap_base_dn,
} }
} }
@ -302,13 +323,14 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
match self match self
.backend_handler .backend_handler
.bind(BindRequest { .bind(BindRequest {
name: user_id, name: user_id.clone(),
password: password.clone(), password: password.clone(),
}) })
.await .await
{ {
Ok(()) => { Ok(()) => {
self.dn = UserId::new(&request.dn); self.dn = LdapDn(request.dn.clone());
self.user_id = user_id;
(LdapResultCode::Success, "".to_string()) (LdapResultCode::Success, "".to_string())
} }
Err(_) => (LdapResultCode::InvalidCredentials, "".to_string()), Err(_) => (LdapResultCode::InvalidCredentials, "".to_string()),
@ -382,15 +404,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
pub async fn do_search(&mut self, request: &LdapSearchRequest) -> Vec<LdapOp> { pub async fn do_search(&mut self, request: &LdapSearchRequest) -> Vec<LdapOp> {
if self.dn != self.ldap_user_dn { let admin = self.dn == self.ldap_user_dn;
return vec![make_search_error(
LdapResultCode::InsufficentAccessRights,
format!(
r#"Current user `{}` is not allowed to query LDAP, expected {}"#,
&self.dn, &self.ldap_user_dn
),
)];
}
if request.base.is_empty() if request.base.is_empty()
&& request.scope == LdapSearchScope::Base && request.scope == LdapSearchScope::Base
&& request.filter == LdapFilter::Present("objectClass".to_string()) && request.filter == LdapFilter::Present("objectClass".to_string())
@ -418,19 +432,20 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
let mut results = Vec::new(); let mut results = Vec::new();
let mut got_match = false; let mut got_match = false;
let user_filter = if admin { None } else { Some(&self.user_id) };
if dn_parts.len() == self.base_dn.len() if dn_parts.len() == self.base_dn.len()
|| (dn_parts.len() == self.base_dn.len() + 1 || (dn_parts.len() == self.base_dn.len() + 1
&& dn_parts[0] == ("ou".to_string(), "people".to_string())) && dn_parts[0] == ("ou".to_string(), "people".to_string()))
{ {
got_match = true; got_match = true;
results.extend(self.get_user_list(request).await); results.extend(self.get_user_list(request, &user_filter).await);
} }
if dn_parts.len() == self.base_dn.len() if dn_parts.len() == self.base_dn.len()
|| (dn_parts.len() == self.base_dn.len() + 1 || (dn_parts.len() == self.base_dn.len() + 1
&& dn_parts[0] == ("ou".to_string(), "groups".to_string())) && dn_parts[0] == ("ou".to_string(), "groups".to_string()))
{ {
got_match = true; got_match = true;
results.extend(self.get_groups_list(request).await); results.extend(self.get_groups_list(request, &user_filter).await);
} }
if !got_match { if !got_match {
warn!( warn!(
@ -445,9 +460,13 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
results results
} }
async fn get_user_list(&self, request: &LdapSearchRequest) -> Vec<LdapOp> { async fn get_user_list(
&self,
request: &LdapSearchRequest,
user_filter: &Option<&UserId>,
) -> Vec<LdapOp> {
let filters = match self.convert_user_filter(&request.filter) { let filters = match self.convert_user_filter(&request.filter) {
Ok(f) => Some(f), Ok(f) => f,
Err(e) => { Err(e) => {
return vec![make_search_error( return vec![make_search_error(
LdapResultCode::UnwillingToPerform, LdapResultCode::UnwillingToPerform,
@ -455,7 +474,13 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
)] )]
} }
}; };
let users = match self.backend_handler.list_users(filters).await { let filters = match user_filter {
None => filters,
Some(u) => {
UserRequestFilter::And(vec![filters, UserRequestFilter::UserId((*u).clone())])
}
};
let users = match self.backend_handler.list_users(Some(filters)).await {
Ok(users) => users, Ok(users) => users,
Err(e) => { Err(e) => {
return vec![make_search_error( return vec![make_search_error(
@ -478,7 +503,11 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
}) })
} }
async fn get_groups_list(&self, request: &LdapSearchRequest) -> Vec<LdapOp> { async fn get_groups_list(
&self,
request: &LdapSearchRequest,
user_filter: &Option<&UserId>,
) -> Vec<LdapOp> {
let filter = match self.convert_group_filter(&request.filter) { let filter = match self.convert_group_filter(&request.filter) {
Ok(f) => f, Ok(f) => f,
Err(e) => { Err(e) => {
@ -488,6 +517,12 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
)] )]
} }
}; };
let filter = match user_filter {
None => filter,
Some(u) => {
GroupRequestFilter::And(vec![filter, GroupRequestFilter::Member((*u).clone())])
}
};
let groups = match self.backend_handler.list_groups(Some(filter)).await { let groups = match self.backend_handler.list_groups(Some(filter)).await {
Ok(groups) => groups, Ok(groups) => groups,
@ -501,7 +536,14 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
groups groups
.into_iter() .into_iter()
.map(|u| make_ldap_search_group_result_entry(u, &self.base_dn_str, &request.attrs)) .map(|u| {
make_ldap_search_group_result_entry(
u,
&self.base_dn_str,
&request.attrs,
user_filter,
)
})
.map(|entry| Ok(LdapOp::SearchResultEntry(entry?))) .map(|entry| Ok(LdapOp::SearchResultEntry(entry?)))
.collect::<Result<Vec<_>>>() .collect::<Result<Vec<_>>>()
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
@ -528,7 +570,8 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
LdapOp::SearchRequest(request) => self.do_search(&request).await, LdapOp::SearchRequest(request) => self.do_search(&request).await,
LdapOp::UnbindRequest => { LdapOp::UnbindRequest => {
self.dn = UserId::new("unauthenticated"); self.dn = LdapDn("unauthenticated".to_string());
self.user_id = UserId::new("unauthenticated");
// No need to notify on unbind (per rfc4511) // No need to notify on unbind (per rfc4511)
return None; return None;
} }
@ -801,7 +844,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn test_bind_invalid_credentials() { async fn test_search_non_admin_user() {
let mut mock = MockTestBackendHandler::new(); let mut mock = MockTestBackendHandler::new();
mock.expect_bind() mock.expect_bind()
.with(eq(crate::domain::handler::BindRequest { .with(eq(crate::domain::handler::BindRequest {
@ -810,6 +853,18 @@ mod tests {
})) }))
.times(1) .times(1)
.return_once(|_| Ok(())); .return_once(|_| Ok(()));
mock.expect_list_users()
.with(eq(Some(UserRequestFilter::And(vec![
UserRequestFilter::And(vec![]),
UserRequestFilter::UserId(UserId::new("test")),
]))))
.times(1)
.return_once(|_| {
Ok(vec![User {
user_id: UserId::new("test"),
..Default::default()
}])
});
let mut ldap_handler = let mut ldap_handler =
LdapHandler::new(mock, "dc=example,dc=com".to_string(), UserId::new("admin")); LdapHandler::new(mock, "dc=example,dc=com".to_string(), UserId::new("admin"));
@ -822,13 +877,17 @@ mod tests {
LdapResultCode::Success LdapResultCode::Success
); );
let request = make_user_search_request::<String>(LdapFilter::And(vec![]), vec![]); let request =
make_user_search_request::<String>(LdapFilter::And(vec![]), vec!["1.1".to_string()]);
assert_eq!( assert_eq!(
ldap_handler.do_search(&request).await, ldap_handler.do_search(&request).await,
vec![make_search_error( vec![
LdapResultCode::InsufficentAccessRights, LdapOp::SearchResultEntry(LdapSearchResultEntry {
r#"Current user `cn=test,ou=people,dc=example,dc=com` is not allowed to query LDAP, expected cn=admin,ou=people,dc=example,dc=com"#.to_string() dn: "cn=test,ou=people,dc=example,dc=com".to_string(),
)] attributes: vec![],
}),
make_search_success()
],
); );
} }
@ -1144,17 +1203,14 @@ mod tests {
LdapFilter::Equality("objectclass".to_string(), "groupOfUniqueNames".to_string()), LdapFilter::Equality("objectclass".to_string(), "groupOfUniqueNames".to_string()),
LdapFilter::Equality("objectclass".to_string(), "groupOfNames".to_string()), LdapFilter::Equality("objectclass".to_string(), "groupOfNames".to_string()),
]), ]),
vec!["cn"], vec!["1.1"],
); );
assert_eq!( assert_eq!(
ldap_handler.do_search(&request).await, ldap_handler.do_search(&request).await,
vec![ vec![
LdapOp::SearchResultEntry(LdapSearchResultEntry { LdapOp::SearchResultEntry(LdapSearchResultEntry {
dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(),
attributes: vec![LdapPartialAttribute { attributes: vec![],
atype: "cn".to_string(),
vals: vec!["group_1".to_string()]
},],
}), }),
make_search_success(), make_search_success(),
] ]