From f52197e76f0cc0f035f9720768109d4110862e4f Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Mon, 25 Apr 2022 09:03:55 +0200 Subject: [PATCH 1/4] server: allow non-admin user to do limited searches --- server/src/infra/ldap_handler.rs | 184 ++++++++++++++++++++----------- 1 file changed, 120 insertions(+), 64 deletions(-) diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index fcfa127..6e1e5f9 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -13,6 +13,9 @@ use ldap3_server::proto::{ }; use log::{debug, warn}; +#[derive(Debug, PartialEq, Eq, Clone)] +struct LdapDn(String); + fn make_dn_pair(mut iter: I) -> Result<(String, String)> where I: Iterator, @@ -94,24 +97,24 @@ fn get_user_id_from_distinguished_name( } } -fn get_user_attribute(user: &User, attribute: &str, dn: &str) -> Result> { - match attribute.to_lowercase().as_str() { - "objectclass" => Ok(vec![ +fn get_user_attribute(user: &User, attribute: &str, dn: &str) -> Result>> { + Ok(Some(match attribute.to_lowercase().as_str() { + "objectclass" => vec![ "inetOrgPerson".to_string(), "posixAccount".to_string(), "mailAccount".to_string(), "person".to_string(), - ]), - "dn" => Ok(vec![dn.to_string()]), - "uid" => Ok(vec![user.user_id.to_string()]), - "mail" => Ok(vec![user.email.clone()]), - "givenname" => Ok(vec![user.first_name.clone()]), - "sn" => Ok(vec![user.last_name.clone()]), - "cn" | "displayname" => Ok(vec![user.display_name.clone()]), - "createtimestamp" | "modifytimestamp" => Ok(vec![user.creation_date.to_rfc3339()]), - "1.1" => Ok(vec![]), + ], + "dn" => vec![dn.to_string()], + "uid" => vec![user.user_id.to_string()], + "mail" => vec![user.email.clone()], + "givenname" => vec![user.first_name.clone()], + "sn" => vec![user.last_name.clone()], + "cn" | "displayname" => vec![user.display_name.clone()], + "createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339()], + "1.1" => return Ok(None), _ => bail!("Unsupported user attribute: {}", attribute), - } + })) } fn make_ldap_search_user_result_entry( @@ -124,47 +127,63 @@ fn make_ldap_search_user_result_entry( dn: dn.clone(), attributes: attributes .iter() - .map(|a| { - Ok(LdapPartialAttribute { + .filter_map(|a| { + 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(), - vals: get_user_attribute(&user, a, &dn)?, - }) + vals: values, + })) }) .collect::>>()?, }) } -fn get_group_attribute(group: &Group, base_dn_str: &str, attribute: &str) -> Result> { - match attribute.to_lowercase().as_str() { - "objectclass" => Ok(vec!["groupOfUniqueNames".to_string()]), - "dn" => Ok(vec![format!( +fn get_group_attribute( + group: &Group, + base_dn_str: &str, + attribute: &str, + user_filter: &Option<&UserId>, +) -> Result>> { + Ok(Some(match attribute.to_lowercase().as_str() { + "objectclass" => vec!["groupOfUniqueNames".to_string()], + "dn" => vec![format!( "cn={},ou=groups,{}", group.display_name, base_dn_str - )]), - "cn" | "uid" => Ok(vec![group.display_name.clone()]), - "member" | "uniquemember" => Ok(group + )], + "cn" | "uid" => vec![group.display_name.clone()], + "member" | "uniquemember" => group .users .iter() + .filter(|u| user_filter.map(|f| *u == f).unwrap_or(true)) .map(|u| format!("cn={},ou=people,{}", u, base_dn_str)) - .collect()), + .collect(), + "1.1" => return Ok(None), _ => bail!("Unsupported group attribute: {}", attribute), - } + })) } fn make_ldap_search_group_result_entry( group: Group, base_dn_str: &str, attributes: &[String], + user_filter: &Option<&UserId>, ) -> Result { Ok(LdapSearchResultEntry { dn: format!("cn={},ou=groups,{}", group.display_name, base_dn_str), attributes: attributes .iter() - .map(|a| { - Ok(LdapPartialAttribute { + .filter_map(|a| { + 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(), - vals: get_group_attribute(&group, base_dn_str, a)?, - }) + vals: values, + })) }) .collect::>>()?, }) @@ -265,17 +284,19 @@ fn root_dse_response(base_dn: &str) -> LdapOp { } pub struct LdapHandler { - dn: UserId, + dn: LdapDn, + user_id: UserId, backend_handler: Backend, pub base_dn: Vec<(String, String)>, base_dn_str: String, - ldap_user_dn: UserId, + ldap_user_dn: LdapDn, } impl LdapHandler { pub fn new(backend_handler: Backend, ldap_base_dn: String, ldap_user_dn: UserId) -> Self { Self { - dn: UserId::new("unauthenticated"), + dn: LdapDn("unauthenticated".to_string()), + user_id: UserId::new("unauthenticated"), backend_handler, base_dn: parse_distinguished_name(&ldap_base_dn).unwrap_or_else(|_| { panic!( @@ -283,7 +304,7 @@ impl LdapHandler LdapHandler { - self.dn = UserId::new(&request.dn); + self.dn = LdapDn(request.dn.clone()); + self.user_id = user_id; (LdapResultCode::Success, "".to_string()) } Err(_) => (LdapResultCode::InvalidCredentials, "".to_string()), @@ -382,15 +404,7 @@ impl LdapHandler Vec { - if 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 - ), - )]; - } + let admin = self.dn == self.ldap_user_dn; if request.base.is_empty() && request.scope == LdapSearchScope::Base && request.filter == LdapFilter::Present("objectClass".to_string()) @@ -418,19 +432,20 @@ impl LdapHandler LdapHandler Vec { + async fn get_user_list( + &self, + request: &LdapSearchRequest, + user_filter: &Option<&UserId>, + ) -> Vec { let filters = match self.convert_user_filter(&request.filter) { - Ok(f) => Some(f), + Ok(f) => f, Err(e) => { return vec![make_search_error( LdapResultCode::UnwillingToPerform, @@ -455,7 +474,13 @@ impl LdapHandler 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, Err(e) => { return vec![make_search_error( @@ -478,7 +503,11 @@ impl LdapHandler Vec { + async fn get_groups_list( + &self, + request: &LdapSearchRequest, + user_filter: &Option<&UserId>, + ) -> Vec { let filter = match self.convert_group_filter(&request.filter) { Ok(f) => f, Err(e) => { @@ -488,6 +517,12 @@ impl LdapHandler filter, + Some(u) => { + GroupRequestFilter::And(vec![filter, GroupRequestFilter::Member((*u).clone())]) + } + }; let groups = match self.backend_handler.list_groups(Some(filter)).await { Ok(groups) => groups, @@ -501,7 +536,14 @@ impl LdapHandler>>() .unwrap_or_else(|e| { @@ -528,7 +570,8 @@ impl LdapHandler self.do_search(&request).await, 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) return None; } @@ -801,7 +844,7 @@ mod tests { } #[tokio::test] - async fn test_bind_invalid_credentials() { + async fn test_search_non_admin_user() { let mut mock = MockTestBackendHandler::new(); mock.expect_bind() .with(eq(crate::domain::handler::BindRequest { @@ -810,6 +853,18 @@ mod tests { })) .times(1) .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 = LdapHandler::new(mock, "dc=example,dc=com".to_string(), UserId::new("admin")); @@ -822,13 +877,17 @@ mod tests { LdapResultCode::Success ); - let request = make_user_search_request::(LdapFilter::And(vec![]), vec![]); + let request = + make_user_search_request::(LdapFilter::And(vec![]), vec!["1.1".to_string()]); assert_eq!( ldap_handler.do_search(&request).await, - vec![make_search_error( - LdapResultCode::InsufficentAccessRights, - 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() - )] + vec![ + LdapOp::SearchResultEntry(LdapSearchResultEntry { + 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(), "groupOfNames".to_string()), ]), - vec!["cn"], + vec!["1.1"], ); assert_eq!( ldap_handler.do_search(&request).await, vec![ LdapOp::SearchResultEntry(LdapSearchResultEntry { dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(), - attributes: vec![LdapPartialAttribute { - atype: "cn".to_string(), - vals: vec!["group_1".to_string()] - },], + attributes: vec![], }), make_search_success(), ] From c7f45b12ac5d7e51fe8aefad16489c88d8fe39f4 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Mon, 25 Apr 2022 09:35:13 +0200 Subject: [PATCH 2/4] app: add bottom padding to avoid overlap with the footer --- app/src/components/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/app.rs b/app/src/components/app.rs index 7429b23..9be91d9 100644 --- a/app/src/components/app.rs +++ b/app/src/components/app.rs @@ -100,7 +100,7 @@ impl Component for App { html! {
{self.view_banner()} -
+
render = Router::render(move |s| Self::dispatch_route(s, &link, is_admin)) From cb84f7f38796538d54e3776008dd6cdbb9b8fe8f Mon Sep 17 00:00:00 2001 From: Cyrix126 <58007246+Cyrix126@users.noreply.github.com> Date: Sun, 24 Apr 2022 12:33:22 +0200 Subject: [PATCH 3/4] Add example configuration for dolibarr --- example_configs/dolibarr.md | 90 +++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 example_configs/dolibarr.md diff --git a/example_configs/dolibarr.md b/example_configs/dolibarr.md new file mode 100644 index 0000000..2f4047d --- /dev/null +++ b/example_configs/dolibarr.md @@ -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'; +``` + + From 5420dcf2b8101ad2568ceba410a245cae2020a81 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Mon, 25 Apr 2022 17:38:13 +0200 Subject: [PATCH 4/4] github: skip coverage for doc branches --- .github/workflows/rust.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 68605e3..c048790 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -20,10 +20,12 @@ jobs: - id: skip_check uses: fkirc/skip-duplicate-actions@master with: - concurrent_skipping: 'never' + concurrent_skipping: 'outdated_runs' skip_after_successful_duplicate: 'true' paths_ignore: '["**/*.md", "**/docs/**", "example_configs/**", "*.sh"]' do_not_skip: '["workflow_dispatch", "schedule"]' + cancel_others: true + test: name: cargo test needs: pre_job @@ -97,6 +99,7 @@ jobs: coverage: name: Code coverage needs: pre_job + if: ${{ needs.pre_job.outputs.should_skip != 'true' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} runs-on: ubuntu-latest steps: - name: Checkout sources