Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LDAP user search/custom mapping + TLS support #545

Merged
merged 9 commits into from
Jul 21, 2023
Prev Previous commit
Next Next commit
Set search source outside of callback
  • Loading branch information
cecilialau6776 committed Jun 7, 2023
commit c2483fae8bf271cb25f6dae45fa258c188f36a76
63 changes: 58 additions & 5 deletions coldfront/plugins/ldap_user_search/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,22 @@ search.py code in the FreeIPA plugin.
## Design

ColdFront provides an API to define additional user search classes for
extending the default search functionality. This app implements a
LDAPUserSearch class in `utils.py` which performs the LDAP search. This class is
extending the default search functionality. This app implements an
`LDAPUserSearch` class in `utils.py` which performs the LDAP search. This class is
then registered with ColdFront by setting `ADDITIONAL_USER_SEARCH_CLASSES`
in `config/plugins/ldap_user_search.py`
in `config/plugins/ldap_user_search.py`. This class also allows customization
through Django settings of the attributes requested and how they're mapped to
ColdFront users.

## Requirements

- pip install python-ldap ldap3

## Usage

To enable this plugin set the following environment variables:
To enable this plugin set the following applicable environment variables:

```
```env
PLUGIN_LDAP_USER_SEARCH=True
LDAP_USER_SEEACH_SERVER_URI=ldap://example.com
LDAP_USER_SEARCH_BASE="dc=example,dc=com"
Expand All @@ -35,3 +37,54 @@ LDAP_USER_SEARCH_CACERT_FILE=/path/to/cacert
LDAP_USER_SEARCH_CERT_FILE=/path/to/cert
LDAP_USER_SEARCH_PRIV_KEY_FILE=/path/to/key
```

For custom attributes, set the Django variable `LDAP_USER_SEARCH_ATTRIBUTE_MAP` in ColdFront's [local settings](https://coldfront.readthedocs.io/en/latest/config/#configuration-files). This dictionary maps from ColdFront User attributes to LDAP attributes:
```py
# default
LDAP_USER_SEARCH_ATTRIBUTE_MAP = {
"username": "uid",
"last_name": "sn",
"first_name": "givenName",
"email": "mail",
}
```

To set a custom mapping, define an `LDAP_USER_SEARCH_MAPPING_CALLBACK` function with parameters `attr_map` and `entry_dict` that returns a dictionary mapping ColdFront User attributes to their values. `attr_map` is just `LDAP_USER_SEARCH_ATTRIBUTE_MAP`, and `entry_dict` is further explained below.

For example, if your LDAP schema provides a full name and no first and last name attributes, you can define `LDAP_USER_SEARCH_ATTRIBUTE_MAP` and `LDAP_USER_SEARCH_MAPPING_CALLBACK` as follows:

```py
LDAP_USER_SEARCH_ATTRIBUTE_MAP = {
"username": "uid",
"email": "mail",
"full_name": "cn",
}

def LDAP_USER_SEARCH_MAPPING_CALLBACK(attr_map, entry_dict):
user_dict = {
"username": entry_dict.get(attr_map["username"])[0],
"email": entry_dict.get(attr_map["email"])[0],
"first_name": entry_dict.get(attr_map["full_name"])[0].split(" ")[0],
"last_name": entry_dict.get(attr_map["full_name"])[0].split(" ")[-1],
}
return user_dict
```

`entry_dict` is provided as a dictionary mapping from the LDAP attribute to a list of values.
```py
entry_dict = {
'mail': ['[email protected]'],
'cn': ['Jane E Doe'],
'uid': ['janedoe1234']
}
```

If this was the input to the above callback, `user_dict` would look like this:
```py
user_dict = {
"username": "janedoe1234",
"email": "[email protected]",
"first_name": "Jane",
"last_name": "Doe",
}
```
12 changes: 5 additions & 7 deletions coldfront/plugins/ldap_user_search/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,8 @@ def __init__(self, user_search_string, search_by):
self.server = Server(self.LDAP_SERVER_URI, use_ssl=self.LDAP_USE_SSL, connect_timeout=self.LDAP_CONNECT_TIMEOUT, tls=tls)
self.conn = Connection(self.server, self.LDAP_BIND_DN, self.LDAP_BIND_PASSWORD, auto_bind=True)

def parse_ldap_entry(search_source, attribute_map, entry):
entry_dict = json.loads(entry.entry_to_json()).get('attributes')

user_dict = {'source': search_source}
def parse_ldap_entry(attribute_map, entry_dict):
user_dict = {}
for user_attr, ldap_attr in attribute_map:
user_dict[user_attr] = entry_dict.get(ldap_attr)[0] if entry_dict.get(ldap_attr) else ''

Expand All @@ -73,14 +71,14 @@ def search_a_user(self, user_search_string=None, search_by='all_fields'):
'search_filter': filter,
'attributes': ldap_attrs,
'size_limit': size_limit}
logger.debug(f"search params:{searchParameters}")
logger.debug(f"search params: {searchParameters}")
self.conn.search(**searchParameters)
users = []
logger.debug(self.conn.result)
for idx, entry in enumerate(self.conn.entries, 1):
entry_dict = json.loads(entry.entry_to_json()).get('attributes')
logger.debug(f"Entry dict: {entry_dict}")
user_dict = self.MAPPING_CALLBACK(self.search_source, self.ATTRIBUTE_MAP, entry_dict)
user_dict = self.MAPPING_CALLBACK(self.ATTRIBUTE_MAP, entry_dict)
user_dict["source"] = self.search_source
users.append(user_dict)
logger.info("LDAP user search for %s found %s results", user_search_string, len(users))
return users