mirror of
https://github.com/nitnelave/lldap.git
synced 2023-04-12 14:25:13 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
060a082368
5
.github/workflows/rust.yml
vendored
5
.github/workflows/rust.yml
vendored
@ -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
|
||||
|
@ -100,7 +100,7 @@ impl Component for App {
|
||||
html! {
|
||||
<div class="container shadow-sm py-3">
|
||||
{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">
|
||||
<Router<AppRoute>
|
||||
render = Router::render(move |s| Self::dispatch_route(s, &link, is_admin))
|
||||
|
90
example_configs/dolibarr.md
Normal file
90
example_configs/dolibarr.md
Normal 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';
|
||||
```
|
||||
|
||||
|
@ -13,6 +13,9 @@ use ldap3_server::proto::{
|
||||
};
|
||||
use log::{debug, warn};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
struct LdapDn(String);
|
||||
|
||||
fn make_dn_pair<I>(mut iter: I) -> Result<(String, String)>
|
||||
where
|
||||
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>> {
|
||||
match attribute.to_lowercase().as_str() {
|
||||
"objectclass" => Ok(vec![
|
||||
fn get_user_attribute(user: &User, attribute: &str, dn: &str) -> Result<Option<Vec<String>>> {
|
||||
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::<Result<Vec<LdapPartialAttribute>>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_group_attribute(group: &Group, base_dn_str: &str, attribute: &str) -> Result<Vec<String>> {
|
||||
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<Option<Vec<String>>> {
|
||||
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<LdapSearchResultEntry> {
|
||||
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::<Result<Vec<LdapPartialAttribute>>>()?,
|
||||
})
|
||||
@ -265,17 +284,19 @@ fn root_dse_response(base_dn: &str) -> LdapOp {
|
||||
}
|
||||
|
||||
pub struct LdapHandler<Backend: BackendHandler + LoginHandler + OpaqueHandler> {
|
||||
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<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend> {
|
||||
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<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
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,
|
||||
}
|
||||
}
|
||||
@ -302,13 +323,14 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
match self
|
||||
.backend_handler
|
||||
.bind(BindRequest {
|
||||
name: user_id,
|
||||
name: user_id.clone(),
|
||||
password: password.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
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<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
}
|
||||
|
||||
pub async fn do_search(&mut self, request: &LdapSearchRequest) -> Vec<LdapOp> {
|
||||
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<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
}
|
||||
let mut results = Vec::new();
|
||||
let mut got_match = false;
|
||||
let user_filter = if admin { None } else { Some(&self.user_id) };
|
||||
if dn_parts.len() == self.base_dn.len()
|
||||
|| (dn_parts.len() == self.base_dn.len() + 1
|
||||
&& dn_parts[0] == ("ou".to_string(), "people".to_string()))
|
||||
{
|
||||
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()
|
||||
|| (dn_parts.len() == self.base_dn.len() + 1
|
||||
&& dn_parts[0] == ("ou".to_string(), "groups".to_string()))
|
||||
{
|
||||
got_match = true;
|
||||
results.extend(self.get_groups_list(request).await);
|
||||
results.extend(self.get_groups_list(request, &user_filter).await);
|
||||
}
|
||||
if !got_match {
|
||||
warn!(
|
||||
@ -445,9 +460,13 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
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) {
|
||||
Ok(f) => Some(f),
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
return vec![make_search_error(
|
||||
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,
|
||||
Err(e) => {
|
||||
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) {
|
||||
Ok(f) => f,
|
||||
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 {
|
||||
Ok(groups) => groups,
|
||||
@ -501,7 +536,14 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
|
||||
groups
|
||||
.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?)))
|
||||
.collect::<Result<Vec<_>>>()
|
||||
.unwrap_or_else(|e| {
|
||||
@ -528,7 +570,8 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
}
|
||||
LdapOp::SearchRequest(request) => 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::<String>(LdapFilter::And(vec![]), vec![]);
|
||||
let request =
|
||||
make_user_search_request::<String>(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(),
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user