From d9218d70df20de83a836f956a09aa5bfb81e14f7 Mon Sep 17 00:00:00 2001 From: dignajar Date: Tue, 19 May 2020 15:19:40 +0200 Subject: [PATCH] Return username and groups after valid authentication --- README.md | 56 +++++++++++++++++++++++++++----------------------- files/aldap.py | 23 +++++++++++++-------- files/main.py | 11 +++++++--- 3 files changed, 53 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index f4222f2..8a54cab 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,36 @@ ## Diagram ![Another LDAP Authentication](https://i.ibb.co/Fn1ncbP/another-ldap-authentication.jpg) +## Available configurations parameters +The parameters can be sent via environment variables or via HTTP headers, also you can combine them. + +The parameter `LDAP_SEARCH_FILTER` support variable expansion with the username, you can do something like this `(sAMAccountName={username})` and `{username}` is going to be replaced by the username typed in the login form. + +### Environment variables +- `LDAP_ENDPOINT` LDAP URL with the port number. Ex: `ldaps://testmyldap.com:636` +- `LDAP_MANAGER_DN_USERNAME` Username to bind and search in the LDAP tree. Ex: `CN=john-service-user,OU=Administrators,DC=TESTMYLDAP,DC=COM` +- `LDAP_MANAGER_PASSWORD` Password for the bind user. +- `LDAP_SEARCH_BASE` Ex: `DC=TESTMYLDAP,DC=COM` +- `LDAP_SEARCH_FILTER` Filter to search, for Microsoft Active Directory usually you can use `sAMAccountName`. Ex: `(sAMAccountName={username})` +- `LDAP_SERVER_DOMAIN` **(Optional)**, for Microsoft Active Directory usually need the domain name for authenticate the user. Ex: `TESTMYLDAP.COM` +- `LDAP_REQUIRED_GROUPS` **(Optional)**, required groups are case insensitive (`DevOps` is the same as `DEVOPS`), you can send a list separated by commas, try first without required groups. Ex: `'DevOps', 'DevOps_QA'` +- `LDAP_REQUIRED_GROUPS_CONDITIONAL` **(Optional, default="and")**, you can set the conditional to match all the groups on the list or just one of them. To match all of them use `and` and for match just one use `or`. Ex: `and` +- `CACHE_EXPIRATION` **(Optional, default=5)** Expiration time in minutes for the cache. Ex: `10` + +### HTTP request headers +- `Ldap-Endpoint` +- `Ldap-Manager-Dn-Username` +- `Ldap-Manager-Password` +- `Ldap-Search-Base` +- `Ldap-Search-Filter` +- `Ldap-Server-Domain` **(Optional)** +- `Ldap-Required-Groups` **(Optional)** +- `Ldap-Required-Groups-Conditional` **(Optional)** + +### HTTP response headers +- `x-username` Contains the authenticated username +- `x-groups` Contains the username matches groups + ## Installation and configuration The easy way to use **Another LDAP Authentication** is running as a Docker container and set the parameters via environment variables. @@ -152,31 +182,5 @@ spec: servicePort: 80 ``` -## Available configurations parameters -The parameters can be sent via environment variables or via HTTP headers, also you can combine them. - -The parameter `LDAP_SEARCH_FILTER` support variable expansion with the username, you can do something like this `(sAMAccountName={username})` and `{username}` is going to be replaced by the username typed in the login form. - -### Environment variables -- `LDAP_ENDPOINT` LDAP URL with the port number. Ex: `ldaps://testmyldap.com:636` -- `LDAP_MANAGER_DN_USERNAME` Username to bind and search in the LDAP tree. Ex: `CN=john-service-user,OU=Administrators,DC=TESTMYLDAP,DC=COM` -- `LDAP_MANAGER_PASSWORD` Password for the bind user. -- `LDAP_SEARCH_BASE` Ex: `DC=TESTMYLDAP,DC=COM` -- `LDAP_SEARCH_FILTER` Filter to search, for Microsoft Active Directory usually you can use `sAMAccountName`. Ex: `(sAMAccountName={username})` -- `LDAP_SERVER_DOMAIN` **(Optional)**, for Microsoft Active Directory usually need the domain name for authenticate the user. Ex: `TESTMYLDAP.COM` -- `LDAP_REQUIRED_GROUPS` **(Optional)**, required groups are case insensitive (`DevOps` is the same as `DEVOPS`), you can send a list separated by commas, try first without required groups. Ex: `'DevOps', 'DevOps_QA'` -- `LDAP_REQUIRED_GROUPS_CONDITIONAL` **(Optional, default=and)**, you can set the conditional to match all the groups on the list or just one of them. To match all of them use `and` and for match just one use `or`. Ex: `and` -- `CACHE_EXPIRATION` **(Optional, default=5)** Expiration time in minutes for the cache. Ex: `10` - -### HTTP headers -- `Ldap-Endpoint` -- `Ldap-Manager-Dn-Username` -- `Ldap-Manager-Password` -- `Ldap-Search-Base` -- `Ldap-Search-Filter` -- `Ldap-Server-Domain` **(Optional)** -- `Ldap-Required-Groups` **(Optional)** -- `Ldap-Required-Groups-Conditional` **(Optional)** - ## Known limitations - Parameters via headers need to be escaped, for example, you can not send parameters such as `$1` or `$test` because Nginx is applying variable expansion. diff --git a/files/aldap.py b/files/aldap.py index 209314f..d125547 100644 --- a/files/aldap.py +++ b/files/aldap.py @@ -53,19 +53,26 @@ def validateGroups(self, groups, conditional): if group.lower() in str(listAD).lower(): # The user has this group include the group in the matchesGroup matchesGroups.append(group) - if conditional == 'or': - print("[INFO][GROUPS] Matched group:",group) - return True print("[INFO][GROUPS] Matched groups:",matchesGroups) - # If the group is the same as matchesGroups means the user has all the groups - if set(groups) == set(matchesGroups): - print("[INFO][GROUPS] All groups are valid for the user.") - return True + # Conditiona OR, true if just 1 group match + if conditional == 'or': + for group in groups: + if group in matchesGroups: + print("[INFO][GROUPS] One of the groups is valid for the user.") + return True,matchesGroups + # Conditiona AND, true if all the groups match + elif conditional == 'and': + if set(groups) == set(matchesGroups): + print("[INFO][GROUPS] All groups are valid for the user.") + return True,matchesGroups + else: + print("[WARN][GROUPS] Invalid group conditional.") + return False,[] print("[WARN][GROUPS] Invalid groups.") - return False + return False,[] def authenticateUser(self): finalUsername = self.username diff --git a/files/main.py b/files/main.py index 9d77a35..d92ed22 100644 --- a/files/main.py +++ b/files/main.py @@ -1,5 +1,6 @@ from flask import Flask from flask import request +from flask import g from flask_httpauth import HTTPBasicAuth from aldap import Aldap from cache import Cache @@ -17,7 +18,6 @@ @auth.verify_password def login(username, password): - if not username or not password: print("[ERROR] Username or password empty.") return False @@ -86,10 +86,12 @@ def login(username, password): aldap.setUser(username, password) # Check groups only if they are defined + matchesGroups = [] if LDAP_REQUIRED_GROUPS: groups = LDAP_REQUIRED_GROUPS.split(",") # Split the groups by comma and trim groups = [x.strip() for x in groups] # Remove spaces - if not aldap.validateGroups(groups, LDAP_REQUIRED_GROUPS_CONDITIONAL): + validGroups, matchesGroups = aldap.validateGroups(groups, LDAP_REQUIRED_GROUPS_CONDITIONAL) + if not validGroups: return False # Check if the username and password are valid @@ -102,6 +104,8 @@ def login(username, password): cache.add(username, password) # Success + g.username = username # Set the username to send in the headers response + g.matchesGroups = ','.join(matchesGroups) # Set the matches groups to send in the headers response return True # Catch-All URL @@ -111,7 +115,8 @@ def login(username, password): def index(path): code = 200 msg = "Another LDAP Auth" - return msg, code + headers = [('x-username', g.username),('x-groups', g.matchesGroups)] + return msg, code, headers # Main if __name__ == '__main__':