diff --git a/Dockerfile b/Dockerfile index 4405f43..4cb8364 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,11 +8,13 @@ ENV LDAP_SEARCH_BASE="" ENV LDAP_SEARCH_FILTER="" ENV LDAP_REQUIRED_GROUPS="" ENV LDAP_REQUIRED_GROUPS_CONDITIONAL="and" +ENV LDAP_REQUIRED_GROUPS_CASE_SENSITIVE="enabled" +ENV LDAP_HTTPS_SUPPORT="disabled" ENV PYTHONUNBUFFERED=0 -RUN apk --no-cache add build-base openldap-dev -RUN pip install --no-cache-dir flask Flask-HTTPAuth python-ldap +RUN apk --no-cache add build-base openldap-dev libffi-dev +RUN pip install --no-cache-dir flask Flask-HTTPAuth python-ldap pyopenssl COPY files/* /opt/ EXPOSE 9000 diff --git a/README.md b/README.md index 12df51c..f66a244 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,20 @@ **Another LDAP Authentication** is an implementation of the `ldap-auth-daemon` services described in the official blog from Nginx in the [following article](https://www.nginx.com/blog/nginx-plus-authenticate-users/). -**Another LDAP Authentication** it's prepared to run inside a Docker container, also you can run the Python script without the Docker container. Supports `ldap` and `ldaps` and provide a simple cache. +**Another LDAP Authentication** it's prepared to run inside a Docker container, also you can run the Python script without the Docker container. [![Docker Hub](https://img.shields.io/badge/Docker-Hub-blue.svg)](https://hub.docker.com/r/dignajar/another-ldap-auth) -[![Kubernetes](https://img.shields.io/badge/Kubernetes-Deployment-blue.svg)](https://github.com/dignajar/another-ldap-auth#deploy-in-kubernetes-with-nginx-ingress-controller) +[![Kubernetes YAML manifests](https://img.shields.io/badge/Kubernetes-Deployment-blue.svg)](https://github.com/dignajar/another-ldap-auth/tree/master/kubernetes) + +## Features +- Supports `ldap` and `ldaps`. +- Provide a cache for users, you can limit the time of the cache. +- Supports validation groups. +- Supports validation groups with conditionals and regex. +- Supports configuration via headers or via environment variables. +- Supports HTTP response headers such as username and matched groups. +- Log format in Plain-Text or JSON. ## Diagram ![Another LDAP Authentication](https://i.ibb.co/Fn1ncbP/another-ldap-authentication.jpg) @@ -33,8 +42,9 @@ All values type are `string`. | LDAP_REQUIRED_GROUPS_CONDITIONAL | `and` | `and`, `or` | Conditional to match all the groups in the list or just one of them. | `or` | | LDAP_REQUIRED_GROUPS_CASE_SENSITIVE | `enabled` | `enabled`, `disabled` | Enabled or disabled case sensitive groups matches. | `disabled` | | CACHE_EXPIRATION | `5` | | Cache expiration time in minutes. | `10` | -| LOG_LEVEL | `INFO` | `DEBUG`, `INFO`, `WARN`, `ERROR` | Logger level. | `DEBUG` | +| LOG_LEVEL | `INFO` | `INFO`, `WARNING`, `ERROR` | Logger level. | `DEBUG` | | LOG_FORMAT | `TEXT` | `TEXT`, `JSON` | Output format of the logger. | `JSON` | +| LDAP_HTTPS_SUPPORT | `disabled`| `enabled`, `disabled` | Enabled or disabled HTTPS support with self signed certificate. | | ### HTTP request headers The variables send via HTTP headers take precedence over environment variables. diff --git a/files/aldap.py b/files/aldap.py index 89eb690..ceb4991 100644 --- a/files/aldap.py +++ b/files/aldap.py @@ -78,7 +78,7 @@ def findMatch(self, group:str, ADGroup:str): ADGroup = re.match('CN=((\w*\s?_?]*)*)', ADGroup).group(1) if not self.groupCaseSensitive: - group = group.lower() + ADGroup = ADGroup.lower() # return match against supplied group/pattern (None if there is no match) try: @@ -110,21 +110,21 @@ def validateGroups(self, groups): matchesByGroup.append((group,matches)) matchedGroups.extend(matches) - self.logs.info({'message':'Validating groups.', 'matchedGroups': ','.join(matchedGroups), 'groups': ','.join(groups), 'conditional': self.groupConditional}) + self.logs.info({'message':'Validating groups.', 'username': self.username, 'matchedGroups': ','.join(matchedGroups), 'groups': ','.join(groups), 'conditional': self.groupConditional}) # Conditiona OR, true if just 1 group match if self.groupConditional == 'or': if matchedGroups: - self.logs.info({'message':'At least one group is valid for the user.', 'matchedGroups': ','.join(matchedGroups), 'groups': ','.join(groups), 'conditional': self.groupConditional}) + self.logs.info({'message':'At least one group is valid for the user.', 'username': self.username, 'matchedGroups': ','.join(matchedGroups), 'groups': ','.join(groups), 'conditional': self.groupConditional}) return True,matchedGroups # Conditiona AND, true if all the groups match elif self.groupConditional == 'and': if len(groups) == len(matchesByGroup): - self.logs.info({'message':'All groups are valid for the user.', 'matchedGroups': ','.join(matchedGroups), 'groups': ','.join(groups), 'conditional': self.groupConditional}) + self.logs.info({'message':'All groups are valid for the user.', 'username': self.username, 'matchedGroups': ','.join(matchedGroups), 'groups': ','.join(groups), 'conditional': self.groupConditional}) return True,matchedGroups else: - self.logs.warning({'message':'Invalid group conditional.', 'conditional': self.groupConditional}) + self.logs.error({'message':'Invalid group conditional.', 'username': self.username, 'conditional': self.groupConditional}) return False,[] + self.logs.error({'message':'Invalid groups for the user.', 'username': self.username, 'matchedGroups': ','.join(matchedGroups), 'groups': ','.join(groups), 'conditional': self.groupConditional}) return False,[] - diff --git a/files/main.py b/files/main.py index 7aede64..f5fb9fd 100644 --- a/files/main.py +++ b/files/main.py @@ -70,17 +70,23 @@ def login(username, password): LDAP_REQUIRED_GROUPS_CONDITIONAL = environ["LDAP_REQUIRED_GROUPS_CONDITIONAL"] # The default is "enabled", another option is "disabled" - LDAP_REQUIRED_GROUPS_CASE_SENSITIVE = "enabled" + LDAP_REQUIRED_GROUPS_CASE_SENSITIVE = True if "Ldap-Required-Groups-Case-Sensitive" in request.headers: - LDAP_REQUIRED_GROUPS_CASE_SENSITIVE = request.headers["Ldap-Required-Groups-Case-Sensitive"] =='enabled' + LDAP_REQUIRED_GROUPS_CASE_SENSITIVE = (request.headers["Ldap-Required-Groups-Case-Sensitive"] == "enabled") elif "LDAP_REQUIRED_GROUPS_CASE_SENSITIVE" in environ: - LDAP_REQUIRED_GROUPS_CASE_SENSITIVE = environ["LDAP_REQUIRED_GROUPS_CASE_SENSITIVE"] =='enabled' + LDAP_REQUIRED_GROUPS_CASE_SENSITIVE = (environ["LDAP_REQUIRED_GROUPS_CASE_SENSITIVE"] == "enabled") LDAP_SERVER_DOMAIN = "" if "Ldap-Server-Domain" in request.headers: LDAP_SERVER_DOMAIN = request.headers["Ldap-Server-Domain"] elif "LDAP_SERVER_DOMAIN" in environ: LDAP_SERVER_DOMAIN = environ["LDAP_SERVER_DOMAIN"] + + LDAP_HTTPS_SUPPORT = False + if "Ldap-Http-Support" in request.headers: + LDAP_HTTPS_SUPPORT = (request.headers["Ldap-Http-Support"] == "disabled") + elif "LDAP_HTTPS_SUPPORT" in environ: + LDAP_HTTPS_SUPPORT = (environ["LDAP_HTTPS_SUPPORT"] == "disabled") except KeyError as e: logs.error({'message': 'Invalid parameter'}) return False @@ -115,7 +121,8 @@ def login(username, password): groups = LDAP_REQUIRED_GROUPS.split(",") # Split the groups by comma and trim groups = [x.strip() for x in groups] # Remove spaces if not LDAP_REQUIRED_GROUPS_CASE_SENSITIVE: - groups = groups.lower() + groups = [x.lower() for x in groups] # Convert to lowercase + print(groups) validGroups, matchesGroups = aldap.validateGroups(groups) if not validGroups: return False @@ -137,4 +144,11 @@ def index(path): # Main if __name__ == '__main__': - app.run(host='0.0.0.0', port=9000, debug=False) + LDAP_HTTPS_SUPPORT = False + if "LDAP_HTTPS_SUPPORT" in environ: + LDAP_HTTPS_SUPPORT = (environ["LDAP_HTTPS_SUPPORT"] == "enabled") + + if LDAP_HTTPS_SUPPORT: + app.run(host='0.0.0.0', port=9000, debug=False, ssl_context='adhoc') + else: + app.run(host='0.0.0.0', port=9000, debug=False)