Skip to content

Commit

Permalink
auth_ldap deep refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
sebo-b committed Mar 15, 2023
1 parent a45d28f commit a9683ef
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 190 deletions.
208 changes: 152 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ The story of this project begins when, due to COVID-19, we have converted our re
I've quickly evaluated a couple of existing solutions, but they were either too big and complicated and/or too expensive. As I assumed that other people would have the same challenge I had, I decided to spend my after-hours time making an open-source tailored system for the need. Yes - it is free as speech, not as beer.

## What WARP can do

- It allows people to book / change / unbook desks (or even parking stalls) in the office.
- It allows people to check who else will be in the office.
- It works on mobile.
- All is done in an easy, visual way.
- Generate a report of past bookings and export it to Excel file

## More advanced features

- Seats can be limited to certain people, so other people cannot book them (it is called assigned seats).
- Seats can be disabled, so people don't see them at all.
- Multiple zones (maps) can be created, for example, floors or parking.
Expand All @@ -23,10 +25,12 @@ I've quickly evaluated a couple of existing solutions, but they were either too
- Translations - currently, English and Polish are supported.

## What I'm not even planning to do

- Approvals - the main goal of the system was to make it autonomous and management-free. So I don't intend to implement approval flows.
- Timezone support - the selected time is always in the same timezone as a zone. It works well and is simple. But in case someone would like to have a couple of zones in different timezones and keep the `one person one seat at a given time` rule across these timezones, this will fail.
- Timezone support - the selected time is always in the same timezone as a zone. It works well and is simple. But in case someone would like to have a couple of zones in different timezones and keep the`one person one seat at a given time` rule across these timezones, this will fail.

## What browsers are supported

To be honest, I was not paying much attention to browser compatibility, nor was I extensively testing it on other browsers than Chrome and Firefox. Nevertheless, all modern browsers should be supported (definitely not IE).

## Is there any demo?
Expand All @@ -48,6 +52,7 @@ The preferred way to deploy is to run it via Docker. You need a working docker,
### docker compose

From the command line:

```
# clone the repository
$ git clone https://github.com/sebo-b/warp.git
Expand All @@ -61,6 +66,7 @@ After that, open http://127.0.0.1:8080 in your browser and log in as `admin` wit
### without docker compose (but why?)

From the command line:

```
# clone the repository
$ git clone https://github.com/sebo-b/warp.git
Expand Down Expand Up @@ -132,21 +138,28 @@ After that, open http://127.0.0.1:5000 in your browser and log in as `admin` wit
For the production envirnoment, I recommend running Nginx and PostgreSQL on separate VMs. Then (even multiple) WARP image can be simply started via Docker and rev-proxed from Nginx.

Each configuration parameter (check config.py) can be passed via the envirnoment as `WARP_varname`.
As envirnoment variables as passed as strings, they need to be parsed into Python types and data structures.
To do that values are first converted to lower case and then `jsom.loads` is used. If that fails variable is treaten as string.
This makes possible to pass integers, floats, booleans as well as dicts, arrays and None value (as JSON null).

### SECRET_KEY

For the production environment, **make sure** that you have generated `SECRET_KEY` used for signing cookies. It is defined in `config.py.`

Flask documentation mentions this method to generate it:

```
$ python -c 'import os; print(os.urandom(16))'
```

Alternatively, you can use OpenSSL and Sed:

```
$ openssl rand -hex 16 | sed 's/\(..\)/\\x\1/g;s/^/b"/;s/$/"/'
```

or wrap it into Python:

```
$ python -c 'from subprocess import run; print(run(["openssl","rand","16"],capture_output=True).stdout)'
```
Expand All @@ -155,76 +168,158 @@ $ python -c 'from subprocess import run; print(run(["openssl","rand","16"],captu

Change `LANGUAGE_FILE` variable in `config.py` or set `WARP_LANGUAGE_FILE` environment variable. Currently, language is global for the instance.

### Active Directory (or any other LDAP) authentication

WARP supports authentication against an LDAP server. It is the simplest way to allow your LDAP directory users to log in on your WARP installation.
# Advanced configuration

To enable LDAP auth, you need to configure `AUTH_LDAP`, `LDAP_SEARCH_BASE`, `LDAP_AUTH_SERVER`, and `LDAP_GROUP_MAP`. When enabled, WARP will check the user login and password via LDAP bind action and the list of authorized groups (see `LDAP_GROUP_MAP` configuration setting). Logging is allowed if the Bind action succeeds and the user belongs to authorized groups.
## LDAP authentication (including Active Directory)

Authorization is managed by adding a user, on the first login, to WARP auth database and assigning the WARP group defined by `LDAP_GROUP_MAP`.
WARP supports authentication against an LDAP server. In this way your LDAP directory users to log in on your WARP installation.

It is possible to exclude some users from using LDAP auth by adding them to `LDAP_EXCLUDED_USERS` list. Credentials for users on this list will be checked via WARP auth database ignoring LDAP AUTH configuration.
To enable LDAP auth, you need to set`AUTH_LDAP` to `True` and at least configure `LDAP_SERVER_URL`, `LDAP_USER_DN_TEMPLATE`. Probably you will need to tweak more parameters to make it working with your LDAP setup, so keep reading.

Supported configurations to contact the LDAP server are:
- LDAP protocol: `LDAP` or `LDAPS`. It is not recommended to use LDAP plain connections in production environments.
- Authentication mechanisms: `SIMPLE` or `NTLM v2*`
This plugin supports:
- LDAP over plain text, SSL or StartTLS
- SIMPLE or NTLM LDAP authentication
- automatic Warp user creation on the first login
- replicating user name and user groups from LDAP
- limiting access only to users within a specific LDAP group(s)
- exclude users (e.g. admins) from LDAP login

#### Configuration variables
### Configuration variables

Please note that every variable can be set either in the config file or via the environment (in that case, it needs to be prefixed by `WARP_` string).

|variable|default value|description|
|--|--|--|
`AUTH_LDAP`|`None`| If set to `True` enables LDAP authentication|
`LDAP_SEARCH_BASE`|`None`| Location in the directory of user and groups|
`LDAP_AUTH_SERVER`|`None`| LDAP server address|
`LDAP_GROUP_MAP`|`[]`| Array of mapping between LDAP group and default asigned group on WARP. Order is important as only one group is assigned. First match is used.|
`LDAP_EXCLUDED_USERS`|`[]`| Array of logins that are excluded form LDAP auth (to check login of this users WARP uses password stored in warp DB).|
`LDAP_USER_CLASS`|`user`| LDAP user objectclass.|
`LDAP_USER_ID_ATTRIBUTE`|`uid`| LDAP directory attribute to be compare with login.|
`LDAP_USER_NAME_ATTRIBUTE`|`cn`| LDAP directory attribute used to obtain name for the automatically created user on warp DB.|
`LDAP_USER_GROUPS_ATTRIBUTE`|`memberOf`| LDAP directory attribute contaning group list.|
`LDAP_AUTH_SERVER_PORT`|`389`| LDAP/LDAPS server port|
`LDAP_AUTH_USE_LDAPS`|`False`| True form LDAPS connection not defined or False for LDAP plain connection|
`LDAP_AUTH_TLS_VERSION`| 1.2 | 1.2 for TLS1.2, any other value for TLS1|
`LDAP_AUTH_CIPHER`|`ECDHE-RSA-AES256-SHA384`| Set to a valid cipher for LDAPS server.|
`LDAP_AUTH_VALIDATE_CERT`|`False`| Certificate validation.|
`LDAP_AUTH_TYPE`|`SIMPLE`| `NTLM` or `SIMPLE`|
`LDAP_AUTH_NTLM_DOMAIN`|`None`| NTLM domain name is the prefix used for the login name when NTLM AUTH is enabled `DomainName\loginname`|
`LDAP_MATCHING_RULE_IN_CHAIN`|`False`| Set to `True` to include nested groups on ldap group search. For Active Directory set to `True`. On other LDAP implementation check if your ldap implementation supports [LDAP_MATCHING_RULE_IN_CHAIN](https://ldapwiki.com/wiki/LDAP_MATCHING_RULE_IN_CHAIN).|

#### Sample values:

```
AUTH_LDAP: 'True'
LDAP_MATCHING_RULE_IN_CHAIN: 'True'
LDAP_AUTH_TYPE: 'NTLM'
LDAP_AUTH_NTLM_DOMAIN: "Domain1"
LDAP_EXCLUDED_USERS: '["admin"]'
LDAP_GROUP_MAP: '[{"ldapgroup": "CN=LDAP-GROUP-NAME,OU=XXXX,DC=yourDomain,dc=com", "warpgroup": "AsignedGroupOnWarpApp"}]'
LDAP_USER_CLASS: "user"
LDAP_USER_ID_ATTRIBUTE: "sAMAccountName"
LDAP_USER_NAME_ATTRIBUTE: "name"
LDAP_USER_GROUPS_ATTRIBUTE: "memberOf"
LDAP_SEARCH_BASE: 'DC=yourDomain,DC=com'
LDAP_AUTH_SERVER: 'ldapServerName'
# LDAP
LDAP_AUTH_SERVER_PORT: 389
# LDAPS
LDAP_AUTH_SERVER_PORT: 636
LDAP_AUTH_USE_LDAPS: 'True'
LDAP_AUTH_TLS_VERSION: 1.2
LDAP_AUTH_CIPHER: 'ECDHE-RSA-AES256-SHA384'
LDAP_AUTH_VALIDATE_CERT: 'True'
| variable | default value | type | description |
| --- | --- | --- | --- |
| `AUTH_LDAP` | `False` | `boolean` | If set to `True` enables LDAP authentication |
| `LDAP_SERVER_URL` | `None` | `string` | Server url, either `ldap://address[:port]` or `ldaps://address[:port]`<br/>It must be `ldap://` for StartTLS |
| `LDAP_AUTH_TYPE` | `SIMPLE` | `string`: `SIMPLE` or `NTLM` | LDAP authentication type.<br/>For `NTLM` authentication `LDAP_AUTH_NTLM_DOMAIN` must be also set |
| `LDAP_AUTH_NTLM_DOMAIN` | `None` | `string` | NTLM domain used for `NTLM` authentication |
| `LDAP_STARTTLS` | `True` | `boolean` | If StartTLS should be invoked before bind. |
| `LDAP_VALIDATE_CERT` | `False` | `boolean` | If server certificate should be validated for `SSL` or `StartTLS` |
| `LDAP_TLS_VERSION` | `None` | `string`: `TLSv1`, `TLSv1.1` or `TLSv1.2` | TLS version to be user.<br/>If not set, default Python SSL module is used. |
| `LDAP_TLS_CIPHERS` | `None` | `string` | Limit TLS only to specified ciphers. |
| `LDAP_USER_DN_TEMPLATE` | `None` | `string` | Template used for user distinguished name, it must contain `{login}` placeholder.<br/>Example value is: `uid={login},ou=users,dc=example,dc=org` |
| `LDAP_USER_NAME_ATTRIBUTE` | `cn` | `string` | Full user name LDAP atribute. |
| `LDAP_GROUP_SEARCH_BASE` | `None` | `string` | Base for searching for user groups.<br/>Example value is: `ou=groups,dc=example,dc=org`<br>Check the next sections for more advanced examples. |
| `LDAP_GROUP_SEARCH_FILTER_TEMPLATE` | `(&(memberUid={login})(cn={group}))` | `string` | Search filter for user's group lookup.<br>It must contain `{login}` and `{group}` placeholders.<br>Check the next sections for more advanced examples. |
| `LDAP_GROUP_MAP` | `[ [null,null] ]` | `array` of `tuples` | See the next section |
| `LDAP_GROUP_STRICT_MAPPING` | `False` | `boolean` | Should user be removed from Warp groups if such mapping is not present in LDAP.<br>See next section for more details |
| `LDAP_EXCLUDED_USERS` | `[]` | `array` of `strings` | List of logins to be excluded from LDAP authentication. <br/> This can be usable for admins |

### LDAP group mapping

With a proper `LDAP_GROUP_MAP` and `LDAP_GROUP_STRICT_MAPPING` you can achieve the following scenarios:
- allow only limited LDAP group to login to Warp
- add users to Warp groups based on LDAP groups
- remove users from Warp groups based on LDAP groups
- add users to specified default Warp groups

`LDAP_GROUP_MAP` must be an array of array of two strings. The first string is LDAP group, the second string is Warp group.

You can interpret that in the following way:
- what LDAP groups are allowing user to log in to Warp
- to what WARP groups user should be added to, based on LDAP groups

The following configurations of an entry are possible:
1.
```
[
['LDAP group 1',null],
['LDAP group 2',null]
]
```
User must be in one of the `LDAP group 1` or `LDAP group 2` to be allowed to log in to Warp.

2.
```
[
['LDAP group 1','WARP group A'],
['LDAP group 2','WARP group B']
]
```
As in the previous example user must be in one of the `LDAP group 1` or `LDAP group 2` to be allowed to log in to Warp. In addition, during logging in user will be also accordingly added to `WARP group A` and/or `WARP group B` (based on LDAP group membership).

3.
```
[
['LDAP group 1',null],
[null,'WARP group A']
[null,'WARP group B']
]
```
User must be in the `LDAP group 1` to be allowed to log in to Warp (the first entry). During logging in user will be always added to `WARP group A` and `WARP group B`.

4.
```
[
[null,null],
['LDAP group 1','WARP group A'],
['LDAP group 2','WARP group B']
]
```
The first entry (`[null,null]`) changes the standard behaviour and every LDAP user will be allowed to log in to Warp. In addition if user is in `LDAP group 1` and/or `LDAP group 2` will be accordingly added to `WARP group A` and/or `WARP group B`.

Of course you can build a more complicated scenarios with multiple mappings, multiple default Warp, and multiple LDAP groups without a mapping.

Only users from LDAP groups specified in this array are allowed to login to Warp, unless there is a special `[null,null]` entry in this array.

Warp group are not automatically created by LDAP plugin, users are only added (and possibly removed) to an existing Warp groups.

If `LDAP_GROUP_STRICT_MAPPING` is set to `False` users are not removed from Warp groups based on LDAP group mapping mechanism.
If `LDAP_GROUP_STRICT_MAPPING` is set to `True` users are removed from all Warp groups not matched by the mapping.

### `memberOf` LDAP attribute and `LDAP_MATCHING_RULE_IN_CHAIN`

In case you use `memberOf` (or similar) LDAP attribute to assign users to groups, the follwing setup should do the trick:
```
LDAP_GROUP_SEARCH_BASE = "dc=example,dc=org"
LDAP_GROUP_SEARCH_FILTER_TEMPLATE = "(&(objectclass=user)(sAMAccountName={login})(memberOf={group}))"
```

In addition, if your server supports `LDAP_MATCHING_RULE_IN_CHAIN` you can specify it as follow:
```
LDAP_GROUP_SEARCH_BASE = "dc=example,dc=org"
LDAP_GROUP_SEARCH_FILTER_TEMPLATE = "(&(objectclass=user)(sAMAccountName={login})(memberOf:1.2.840.113556.1.4.1941:={group}))"
```

### Example configuration

#### For OpenLDAP
```
WARP_AUTH_LDAP = "True"
WARP_LDAP_SERVER_URL = "ldap://ldap.example.org:1389"
WARP_LDAP_USER_DN_TEMPLATE = "uid={login},ou=users,dc=example,dc=org"
WARP_LDAP_GROUP_SEARCH_BASE = "ou=groups,dc=example,dc=org"
WARP_LDAP_GROUP_MAP = "[ ['WARP_allowed',null], [null,'Everyone'] ]"
WARP_LDAP_EXCLUDED_USERS = "['admin']"
# the following values are default, keeping here just for clarity
WARP_LDAP_STARTTLS = "True"
WARP_LDAP_VALIDATE_CERT = "False"
WARP_LDAP_USER_NAME_ATTRIBUTE = "cn"
WARP_LDAP_GROUP_SEARCH_FILTER_TEMPLATE = "(&(memberUid={login})(cn={group}))"
```

#### For Active Directory
```
WARP_AUTH_LDAP = "True"
WARP_LDAP_SERVER_URL = "ldaps://ldap.example.org:636"
WARP_LDAP_VALIDATE_CERT = "True"
WARP_LDAP_AUTH_TYPE = "NTLM"
WARP_LDAP_AUTH_NTLM_DOMAIN = "Example1"
WARP_LDAP_USER_DN_TEMPLATE = "sAMAccountName={login},ou=users,dc=example,dc=org"
WARP_LDAP_GROUP_SEARCH_BASE = "dc=example,dc=org"
WARP_LDAP_GROUP_SEARCH_FILTER_TEMPLATE = "(&(objectclass=user)(sAMAccountName={login})(memberOf:1.2.840.113556.1.4.1941:={group}))"
WARP_LDAP_EXCLUDED_USERS = "['admin']"
WARP_LDAP_GROUP_MAP = "[ ['WARP_allowed',null], [null,'Everyone'] ]"
```

### How to import users

You can add them manually one by one via the users' management tab or import them directly to the database. Basically, insert users to `user` table, look at the table definition in `warp/sql/schema.sql.`

The role is one of:

```
10 - admin
20 - regular user
Expand All @@ -234,6 +329,7 @@ The role is one of:
Password is a hash used by `werkzeug.security.check_password_hash` (more documentation can be [found here](https://werkzeug.palletsprojects.com/en/2.0.x/utils/#werkzeug.security.generate_password_hash)), by default (in my configuration) it is pbkdf2:sha256 with 16 bytes salt and 260,000 iterations.

You can generate it with Python (just make sure you have activated the environment where Flask is installed):

```
python -c 'from getpass import getpass; from werkzeug.security import generate_password_hash; print(generate_password_hash(getpass()))'
Expand Down
1 change: 0 additions & 1 deletion warp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ def create_app():
and app.config['AUTH_MELLON']:
app.register_blueprint(auth_mellon.bp)
elif 'AUTH_LDAP' in app.config \
and 'LDAP_AUTH_SERVER' in app.config \
and app.config['AUTH_LDAP']:
app.register_blueprint(auth_ldap.bp)
else:
Expand Down
Loading

0 comments on commit a9683ef

Please sign in to comment.