Skip to content

Commit 73e2020

Browse files
David SimpsonDavid Simpson
authored andcommitted
Adds project_openldap plugin
Signed-off-by: David Simpson <>
1 parent a23e951 commit 73e2020

File tree

16 files changed

+2310
-0
lines changed

16 files changed

+2310
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# SPDX-FileCopyrightText: (C) ColdFront Authors
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
from coldfront.config.base import INSTALLED_APPS
6+
from coldfront.config.env import ENV
7+
8+
INSTALLED_APPS += [
9+
"coldfront.plugins.project_openldap",
10+
]
11+
12+
# Connection URI and bind user
13+
PROJECT_OPENLDAP_SERVER_URI = ENV.str("PROJECT_OPENLDAP_SERVER_URI", default="")
14+
PROJECT_OPENLDAP_BIND_USER = ENV.str("PROJECT_OPENLDAP_BIND_USER", default="")
15+
PROJECT_OPENLDAP_BIND_PASSWORD = ENV.str("PROJECT_OPENLDAP_BIND_PASSWORD", default="")
16+
# Timeout and SSL settings
17+
PROJECT_OPENLDAP_CONNECT_TIMEOUT = ENV.float("PROJECT_OPENLDAP_CONNECT_TIMEOUT", default=2.5)
18+
PROJECT_OPENLDAP_USE_SSL = ENV.bool("PROJECT_OPENLDAP_USE_SSL", default=True)
19+
PROJECT_OPENLDAP_USE_TLS = ENV.bool("PROJECT_OPENLDAP_USE_TLS", default=False)
20+
PROJECT_OPENLDAP_PRIV_KEY_FILE = ENV.str("PROJECT_OPENLDAP_PRIV_KEY_FILE", default=None)
21+
PROJECT_OPENLDAP_CERT_FILE = ENV.str("PROJECT_OPENLDAP_CERT_FILE", default=None)
22+
PROJECT_OPENLDAP_CACERT_FILE = ENV.str("PROJECT_OPENLDAP_CACERT_FILE", default=None)
23+
# OU, GID, Arhive and sync excludes
24+
PROJECT_OPENLDAP_OU = ENV.str("PROJECT_OPENLDAP_OU", default="") # where projects will be stored
25+
PROJECT_OPENLDAP_GID_START = ENV.int(
26+
"PROJECT_OPENLDAP_GID_START"
27+
) # where project gid numbering will start, no default value provided here on purpose, site should define sensible value
28+
PROJECT_OPENLDAP_REMOVE_PROJECT = ENV.bool(
29+
"PROJECT_OPENLDAP_REMOVE_PROJECT", default=True
30+
) # remove projects on archive
31+
PROJECT_OPENLDAP_ARCHIVE_OU = ENV.str(
32+
"PROJECT_OPENLDAP_ARCHIVE_OU", default=""
33+
) # where projects will be stored for archive e.g. ou=archive_projects...
34+
PROJECT_OPENLDAP_EXCLUDE_USERS = ENV.tuple(
35+
"PROJECT_OPENLDAP_EXCLUDE_USERS", default=("coldfront",)
36+
) # never try to add these users to OpenLDAP - used by syncer script
37+
# OpenLDAP description field for project
38+
PROJECT_OPENLDAP_DESCRIPTION_TITLE_LENGTH = ENV.int(
39+
"PROJECT_OPENLDAP_DESCRIPTION_TITLE_LENGTH", default=100
40+
) # control the length of project title component from CF which is inserted into the OpenLDAP description field for each OpenLDAP project and is potentially truncated down

coldfront/config/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"PLUGIN_LDAP_USER_SEARCH": "plugins/ldap_user_search.py",
3030
"PLUGIN_API": "plugins/api.py",
3131
"PLUGIN_AUTO_COMPUTE_ALLOCATION": "plugins/auto_compute_allocation.py",
32+
"PLUGIN_PROJECT_OPENLDAP": "plugins/project_openldap.py",
3233
}
3334

3435
# This allows plugins to be enabled via environment variables. Can alternatively
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
# project_openldap - A plugin to push project information to an OpenLDAP instance
2+
3+
Coldfront django plugin providing capability to push project information to OpenLDAP.
4+
5+
OpenLDAP is a commonly deployed Identity Provider (IdP) - see https://www.openldap.org/
6+
7+
This plugin makes use of django signals within Coldfront to push project information to OpenLDAP. The main motivation for this is so projects and their members can be represented inside a _PosixGroup_, with usage of _memberUid_ (for members). Having this information in OpenLDAP facilitates the usage of filesystem quotas and other activities on a project basis.
8+
9+
**The motivation for using this plugin, is that you want Coldfront and it's WebUI to be the source of truth**. This might be in contrast to another operating modality where information is [generally] imported from another system or IdP into Coldfront.
10+
- You will still need some means of creating/registering users in your OpenLDAP, e.g. a user registration portal - that is not covered/provided here. If you are just testing, you might add these to the DIT with ldifs.
11+
12+
- **NOTE: The plugin doesn't write a Coldfront project's allocation(s) into OpenLDAP. It is expected a separate plugin will do this.**
13+
14+
15+
## Design
16+
17+
The plugin results in each Coldfront project being created in OpenLDAP with an associated OU and a posixgroup with synchronized membership.
18+
19+
- **NOTE:** Each project gets a **<ins>per project group _OU_</ins>** AND **<ins>a per project group _posixgroup_</ins>** written.
20+
21+
To do this django signals are used, which fire upon actions in the Coldfront WebUI. E.g. project creation.
22+
23+
The plugin uses a bind user (which the site has to setup with appropriate write access) and _python ldap3_ to write changes to OpenLDAP.
24+
25+
The **bind user** will **write to a project project OU** and **potentially an archive OU** (if defined by the site admin).
26+
27+
The OpenLDAP server (slapd) must be operational and ready to accept changes via the configured bind user.
28+
29+
Variables to use to configure the plugin are detailed further down in this ``README.md``
30+
31+
### Design - signals and actions
32+
33+
#### signals
34+
35+
The following five Coldfront django signals are used by this plugin, upon Coldfront WebUI actions:
36+
37+
- project new
38+
- project archive
39+
- project update (e.g. update title)
40+
- add project member(s)
41+
- remove project member(s)
42+
43+
To see the exact receiver definitions look at the ``signals.py`` within the plugin directory.
44+
45+
#### actions
46+
47+
The aforementioned signals trigger functions in tasks.py, these in turn use functions in ``utils.py`` to accomplish the action required. Examples are:
48+
49+
- create a per project OU and per project posixgroup within a Projects OU - **new project**
50+
- move a per project OU (and per project posixgroup) to an Archive OU or simply delete it - **archive project** - which action depends on environment variable setup
51+
- add members to the per project posixgroup - **project add members**
52+
- remove members from the per project posixgroup - **project remove members**
53+
- update title - **project update**
54+
55+
The next section shows the representation in OpenLDAP.
56+
57+
### DIT Diagram
58+
59+
DIT Mermaid diagram.
60+
61+
- Projects get created in the Projects OU. A per project OU and a per project posixgroup is created.
62+
- Per project OU's get moved to Archive OU or deleted on Coldfront WebUI archive action.
63+
- **If no Archive OU** is setup by the site administrator, then **projects** (per project OUs) are **deleted on archival**.
64+
65+
66+
```mermaid
67+
%%{init: {'theme': 'forest', 'themeVariables': { 'fontSize': '20px', 'fontFamily': 'Inter'}}}%%
68+
classDiagram
69+
OPENLDAP_DIT <-- projects_OU
70+
OPENLDAP_DIT <-- archive_OU
71+
projects_OU <-- per_project_OU
72+
per_project_OU <-- per_project_posixgroup
73+
class OPENLDAP_DIT {
74+
}
75+
class projects_OU {
76+
objectClass: organizationalUnit
77+
objectClass: top
78+
}
79+
class archive_OU {
80+
objectClass: organizationalUnit
81+
objectClass: top
82+
}
83+
class per_project_OU {
84+
ou: project_code
85+
objectClass: organizationalUnit
86+
objectClass: top
87+
description: OU for project: project_code
88+
}
89+
class per_project_posixgroup {
90+
cn: project_code
91+
gidNumber: GID_START+PK
92+
objectClass: posixgroup
93+
description: institution, pi, title
94+
--------------------------------------------------
95+
memberUid: ldapuser1
96+
memberUid: ldapuser2
97+
}
98+
```
99+
100+
101+
102+
### Syncronization and management command usage
103+
104+
Should Coldfront continue creating or modifying projects whilst the OpenLDAP server is unavailable, corrective action will likely be needed. A management command is provided to perform checks and synchronize changes required. This should only be used when Coldfront and OpenLDAP are (or are suspected) to be out of sync.
105+
106+
From within the Coldfront user's venv environment, the management command help can be seen:
107+
108+
``coldfront project_openldap_sync --help``
109+
110+
Further detail on the management command can be seen in the directory ``README.md``
111+
112+
Also included within that directory is a Mermaid diagram (``Mermaid.md``) of the main function of the sync management command.
113+
114+
### Optional setup checker
115+
116+
Another management command is present to aid setup checks for this plugin, before entering production. See command's help and/or ``README.md`` in management command directory.
117+
118+
``coldfront project_openldap_check_setup --help``
119+
120+
121+
## Requirements - general
122+
123+
The plugin makes extensive use of the _python ldap3_ library - https://ldap3.readthedocs.io/
124+
125+
### Requirements - OpenLDAP
126+
127+
- A working OpenLDAP master instance is required.
128+
- This plugin requires a bind user to make changes. It's recommended that a dedicated bind user is setup for this purpose and this purpose only.
129+
- You will also need to supply an OU to store projects in and optionally an OU to store (move) archived projects to. You should create this in the normal way e.g. ldif.
130+
131+
#### GID:
132+
133+
- **NOTE: A starting GID number to increment posixgroup GIDs is required. Consider this carefully before going into project to avoid clashes or problems.**
134+
135+
#### Security:
136+
137+
- **NOTE: OpenLDAP security and general site security is the responsibility of site administrators.**
138+
139+
140+
## Usage
141+
142+
The plugin requires that various environment variables are defined in the Coldfront django settings - e.g. coldfront.env
143+
144+
Example pre-requisites - we require `project_code` to be enabled, here is an example
145+
146+
**Example Required project_code:**
147+
```
148+
PROJECT_CODE="CDF"
149+
```
150+
Example Optional project_code padding:
151+
```
152+
PROJECT_CODE_PADDING=4
153+
```
154+
155+
### Usage - Example setup
156+
157+
An example setup might look something like this.
158+
- The institution feature usage is not required or mandated.
159+
- Here we are setup to use the Archive OU and not delete per project OUs upon Coldfront archive action in WebUI
160+
161+
**NOTE:** Security (e.g. SSL + TLS) configuration is the responsibility of the site administrator - these aren't production settings below, only a starting point.
162+
163+
```
164+
PROJECT_CODE="CDF"
165+
PROJECT_CODE_PADDING=4
166+
167+
PLUGIN_PROJECT_OPENLDAP=True
168+
PROJECT_OPENLDAP_GID_START=8000
169+
PROJECT_OPENLDAP_SERVER_URI='ldaps://openldap.coldfront.ac.uk'
170+
PROJECT_OPENLDAP_OU='ou=projects,dc=coldfront,dc=ac,dc=uk'
171+
PROJECT_OPENLDAP_BIND_USER='my_bind_user_dn'
172+
PROJECT_OPENLDAP_BIND_PASSWORD='some_secure_password'
173+
PROJECT_OPENLDAP_CACERT_FILE='/etc/path/to/CAcert'
174+
PROJECT_OPENLDAP_USE_TLS=False
175+
176+
# archive settings
177+
PROJECT_OPENLDAP_REMOVE_PROJECT=True #default is true
178+
PROJECT_OPENLDAP_ARCHIVE_OU='ou=archive_projects,dc=coldfront,dc=ac,dc=uk'
179+
180+
#institution
181+
PROJECT_INSTITUTION_EMAIL_MAP=coldfront.ac.uk=ColdfrontUniversity
182+
183+
```
184+
185+
186+
### Usage - Variable descriptions
187+
188+
**Required Plugin load:**
189+
190+
| Option | Type | Default | Description |
191+
|--- | --- | --- | --- |
192+
| `PLUGIN_PROJECT_OPENLDAP` | Bool | True | Enable the plugin, required to be set as True (bool). |
193+
194+
**Required:**
195+
| Option | Type | Default | Description |
196+
|--- | --- | --- | --- |
197+
| `PROJECT_OPENLDAP_GID_START` | int | N/A | Starting value for project gidNumbers, requires an integer. |
198+
| `PROJECT_OPENLDAP_SERVER_URI` | str | empty str | The URI of the OpenLDAP instance, requires a string URI. |
199+
| `PROJECT_OPENLDAP_OU` | str | empty str |The OU where projects will be written, requires a string DN of OU. |
200+
| `PROJECT_OPENLDAP_BIND_USER` | str | empty str |The bind user, that can write to the `PROJECT_OPENLDAP_OU`, requires a string DN of bind user. |
201+
| `PROJECT_OPENLDAP_BIND_PASSWORD` | str | empty str |The password for the bind user, requires a string. |
202+
| `PROJECT_OPENLDAP_REMOVE_PROJECT` | Bool |True | Required to take action upon archive (action) of a project. Default True (bool). |
203+
204+
**OpenLDAP specific:**
205+
| Option | Type | Default | Description |
206+
|--- | --- | --- | --- |
207+
| `PROJECT_OPENLDAP_CONNECT_TIMEOUT` | int |2.5 | Connection timeout. |
208+
| `PROJECT_OPENLDAP_USE_SSL` | Bool | True | Use SSL. |
209+
| `PROJECT_OPENLDAP_USE_TLS` | Bool | False | Enable Tls. |
210+
| `PROJECT_OPENLDAP_PRIV_KEY_FILE` | str | None | Tls Private key. |
211+
| `PROJECT_OPENLDAP_CERT_FILE` | str | None | Tls Certificate file. |
212+
| `PROJECT_OPENLDAP_CACERT_FILE` | str | None | Tls CA certificate file. |
213+
214+
**Optional:**
215+
216+
| Option | Type | Default | Description |
217+
|--- | --- | --- | --- |
218+
| `PROJECT_OPENLDAP_ARCHIVE_OU` | str | empty str | Optional, if this string is defined, then move upon archival action move the project object in OpenLDAP here. Supply a string DN of the desired OU. |
219+
| `PROJECT_OPENLDAP_DESCRIPTION_TITLE_LENGTH` | int | 100 | Optional, this integer will constrain the length of the Coldfront project title, within the OpenLDAP project description field. |
220+
221+
222+
<br>
223+
224+
**Optional, management [sync] command related:**
225+
226+
| Option | Type | Default | Description |
227+
|--- | --- | --- | --- |
228+
| `PROJECT_OPENLDAP_EXCLUDE_USERS` | tuple | ('coldfront',) | Tuple which by default is a single element tuple that will exclude the "coldfront" username from operations in the Syncer script - e.g. to avoid attempted add to OpenLDAP. |
229+
230+
231+
232+
## Example per project prosixgroup and OU container
233+
234+
An example of a project posixgroup and OU (see further down for OU).
235+
236+
- Using 8000 as the start GID, this is project 11 (pk=11), so 8000+11 is the resultant gidNumber.
237+
- Within the OpenLDAP description _INSTITUTE_ gets populated if possible, the plugin doesn't require the institution feature is enabled though. _INSTITUTE: NotDefined_ will be seen if not enabled.
238+
<br>
239+
240+
```
241+
dn: cn=CDF0011,ou=CDF0011,ou=projects,dc=coldfront,dc=ac,dc=uk
242+
description: INSTITUTE: ColdfrontUniversity | PI: ldapuser1 | TITLE: cf project
243+
11
244+
gidNumber: 8011
245+
objectClass: posixGroup
246+
cn: CDF0011
247+
memberUid: ldapuser1
248+
memberUid: ldapuser2
249+
memberUid: ldapuser3
250+
```
251+
252+
The per project OU containing the posixgroup looks like this:
253+
254+
```
255+
dn: ou=CDF0011,ou=projects,dc=coldfront,dc=ac,dc=uk
256+
ou: CDF0011
257+
description: OU for project CDF0011
258+
objectClass: top
259+
objectClass: organizationalUnit
260+
```
261+
262+
263+
264+
265+
266+
## Future work
267+
268+
Future work could include:
269+
270+
Plugin and integration - ``allocation_openldap``:
271+
- writing allocations (not just projects) to OpenLDAP, these could potentially be identified using the _project_code_ and a combination of other parameters to ensure unique. An ``allocation_openldap`` plugin is likely to be worked on soon.
272+
- currently the project removal (deletion) deletes the project group's _posixgroup_ then the project group's _OU_. This may need changed in future, such that multiple subordinates under the project group's _OU_ are deleted - this will depend on how the ``allocation_openldap`` feature is developed. For now though, the approach is simple and safe.
273+
- the sync management command may also see changes to push a couple of methods in ``utils.py`` - e.g. fetch membership.
274+
275+
SASL:
276+
- SASL is not currently supported, all operations currently take place via the bind user and URI. SASL is currently considered low-priority.
277+
278+
### Future considerations
279+
280+
Some sites may want per-institution project OUs (which then contain the relevant project group OUs). Currently this is out-of-scope. The single projects OU is currently provided/favoured for simplicity.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# SPDX-FileCopyrightText: (C) ColdFront Authors
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
default_app_config = "coldfront.plugins.project_openldap.apps.ProjectOpenldapConfig"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# SPDX-FileCopyrightText: (C) ColdFront Authors
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
"""Coldfront project_openldap plugin apps.py"""
6+
7+
import importlib
8+
9+
from django.apps import AppConfig
10+
11+
12+
class ProjectOpenldapConfig(AppConfig):
13+
name = "coldfront.plugins.project_openldap"
14+
15+
def ready(self):
16+
importlib.import_module("coldfront.plugins.project_openldap.signals")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# SPDX-FileCopyrightText: (C) ColdFront Authors
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# project_openldap_sync.py
2+
3+
Operation of main function sync_check_projects in **project_openldap_sync.py**
4+
5+
```mermaid
6+
%%{init: {'theme': 'forest', 'themeVariables': { 'fontSize': '20px', 'fontFamily': 'Inter'}}}%%
7+
graph LR;
8+
A[sync_check_projects - processing is per project] --> B[1 Setup and checks. Get project by code, DNs, check ldapsearch for project]
9+
B --> C[2 Archive project status_id]
10+
B --> D[2 New,Active project status_id]
11+
B --> K[2 Unknown project status_id EXIT]
12+
C -- move to archive --> E[handle_project_in_openldap_but_not_archive]
13+
C -- add to archive --> F[handle_missing_project_in_openldap_archive]
14+
C -- delete from archive --> G[handle_project_removal_if_needed]
15+
C -- NOTIFY --> H[NOTIFY found project DN - ARCHIVE_OU]
16+
D --add missing project--> I[handle_missing_project_in_openldap_new_active]
17+
D -- NOTIFY --> J[NOTIFY found project DN - PROJECT_OU]
18+
J --> L[3 Fetch CF+Openldap members, check for DNs]
19+
H --> L[3 Fetch CF+Openldap members, check for DNs]
20+
L --> M[4 IF openldap_members is None,
21+
then NOTIFY and EXIT]
22+
L --> N[4 RUN sync_members]
23+
N --> Y[5 OpenLDAP description update, if requested]
24+
Y --> Z[END processing for project]
25+
```
26+
27+
28+
29+

0 commit comments

Comments
 (0)