From 66de3195efe4e4c48ca5144d4c79f0d81080135e Mon Sep 17 00:00:00 2001 From: Sergey Sveshnikov Date: Wed, 26 Apr 2023 00:20:32 +0300 Subject: [PATCH] Added authorization via LDAP --- .pylintrc | 2 +- README.md | 38 ++++++++---- proxy/plugin/__init__.py | 2 + proxy/plugin/auth_ldap.py | 121 ++++++++++++++++++++++++++++++++++++++ requirements-release.txt | 1 + requirements-testing.txt | 1 + setup.cfg | 1 + 7 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 proxy/plugin/auth_ldap.py diff --git a/.pylintrc b/.pylintrc index 25434cea23..0b8f907bdb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -9,7 +9,7 @@ extension-pkg-allow-list= # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. (This is an alternative name to extension-pkg-allow-list # for backward compatibility.) -extension-pkg-whitelist= +extension-pkg-whitelist=_ldap # Return non-zero exit code if any of these messages/categories are detected, # even if score is above --fail-under value. Syntax same as enable. Messages diff --git a/README.md b/README.md index 90141417e6..2c72ba0a72 100644 --- a/README.md +++ b/README.md @@ -2355,8 +2355,12 @@ usage: -m [-h] [--tunnel-hostname TUNNEL_HOSTNAME] [--tunnel-port TUNNEL_PORT] [--ca-signing-key-file CA_SIGNING_KEY_FILE] [--auth-plugin AUTH_PLUGIN] [--cache-requests] [--cache-by-content-type] [--cache-dir CACHE_DIR] - [--proxy-pool PROXY_POOL] [--enable-web-server] - [--enable-static-server] [--static-server-dir STATIC_SERVER_DIR] + [--ldap-server LDAP_SERVER] [--ldap-root-dn LDAP_ROOT_DN] + [--ldap-root-pw LDAP_ROOT_PW] [--ldap-base-dn LDAP_BASE_DN] + [--ldap-user-search LDAP_USER_SEARCH] + [--ldap-auth-timeout LDAP_AUTH_TIMEOUT] [--proxy-pool PROXY_POOL] + [--enable-web-server] [--enable-static-server] + [--static-server-dir STATIC_SERVER_DIR] [--min-compression-length MIN_COMPRESSION_LENGTH] [--enable-reverse-proxy] [--pac-file PAC_FILE] [--pac-file-url-path PAC_FILE_URL_PATH] @@ -2366,7 +2370,7 @@ usage: -m [-h] [--tunnel-hostname TUNNEL_HOSTNAME] [--tunnel-port TUNNEL_PORT] [--filtered-client-ips FILTERED_CLIENT_IPS] [--filtered-url-regex-config FILTERED_URL_REGEX_CONFIG] -proxy.py v2.4.4rc4.dev6+g4ee982a.d20221022 +proxy.py v0.1.dev886+g9ba2ea7.d20230425 options: -h, --help show this help message and exit @@ -2489,10 +2493,10 @@ options: Default: None. Signing certificate to use for signing dynamically generated HTTPS certificates. If used, must also pass --ca-key-file and --ca-signing-key-file - --ca-file CA_FILE Default: /Users/abhinavsingh/Dev/proxy.py/.venv/lib/py - thon3.10/site-packages/certifi/cacert.pem. Provide - path to custom CA bundle for peer certificate - verification + --ca-file CA_FILE Default: + /home/sv/MyProjects/proxy.py/venv/lib/python3.10/site- + packages/certifi/cacert.pem. Provide path to custom CA + bundle for peer certificate verification --ca-signing-key-file CA_SIGNING_KEY_FILE Default: None. CA signing key to use for dynamic generation of HTTPS certificates. If used, must also @@ -2507,9 +2511,23 @@ options: from responses. Extracted content type is written to the cache directory e.g. video.mp4. --cache-dir CACHE_DIR - Default: /Users/abhinavsingh/.proxy/cache. Flag only - applicable when cache plugin is used with on-disk - storage. + Default: /home/sv/.proxy/cache. Flag only applicable + when cache plugin is used with on-disk storage. + --ldap-server LDAP_SERVER + Default: ldap://ldap.example.org. LDAP server address. + --ldap-root-dn LDAP_ROOT_DN + Default: uid=Manager,ou=People,dc=example,dc=com. LDAP + root dn. + --ldap-root-pw LDAP_ROOT_PW + Default: SecretPassword. LDAP root password. + --ldap-base-dn LDAP_BASE_DN + Default: ou=People,dc=example,dc=com. LDAP users base + DN. + --ldap-user-search LDAP_USER_SEARCH + Default: (&(uid={user})(accountStatus=active)). LDAP + user search filter. + --ldap-auth-timeout LDAP_AUTH_TIMEOUT + Default: 3600. LDAP user auth timeout. --proxy-pool PROXY_POOL List of upstream proxies to use in the pool --enable-web-server Default: False. Whether to enable diff --git a/proxy/plugin/__init__.py b/proxy/plugin/__init__.py index c3ad91945b..032c705818 100644 --- a/proxy/plugin/__init__.py +++ b/proxy/plugin/__init__.py @@ -18,6 +18,7 @@ Lua """ from .cache import CacheResponsesPlugin, BaseCacheResponsesPlugin +from .auth_ldap import LDAPAuthPlugin from .shortlink import ShortLinkPlugin from .proxy_pool import ProxyPoolPlugin from .program_name import ProgramNamePlugin @@ -36,6 +37,7 @@ __all__ = [ + 'LDAPAuthPlugin', 'CacheResponsesPlugin', 'BaseCacheResponsesPlugin', 'FilterByUpstreamHostPlugin', diff --git a/proxy/plugin/auth_ldap.py b/proxy/plugin/auth_ldap.py new file mode 100644 index 0000000000..391b15b2e5 --- /dev/null +++ b/proxy/plugin/auth_ldap.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. + + .. spelling:: + + auth + http + ldap +""" +import base64 +from time import time +from typing import Dict, Optional + +import ldap + +from ..http import httpHeaders +from ..http.proxy import HttpProxyBasePlugin +from ..common.flag import flags +from ..http.parser import HttpParser +from ..http.exception import ProxyAuthenticationFailed + + +DEFAULT_LDAP_SERVER = 'ldap://ldap.example.org' +DEFAULT_LDAP_ROOT_DN = 'uid=Manager,ou=People,dc=example,dc=com' +DEFAULT_LDAP_ROOT_PW = 'SecretPassword' +DEFAULT_LDAP_BASE_DN = 'ou=People,dc=example,dc=com' +DEFAULT_LDAP_USER_SEARCH = '(&(uid={user})(accountStatus=active))' +DEFAULT_LDAP_AUTH_TIMEOUT = 3600 + +flags.add_argument( + '--ldap-server', + type=str, + default=DEFAULT_LDAP_SERVER, + help='Default: ' + DEFAULT_LDAP_SERVER + + '. LDAP server address.', +) + +flags.add_argument( + '--ldap-root-dn', + type=str, + default=DEFAULT_LDAP_ROOT_DN, + help='Default: ' + DEFAULT_LDAP_ROOT_DN + + '. LDAP root dn.', +) + +flags.add_argument( + '--ldap-root-pw', + type=str, + default=DEFAULT_LDAP_ROOT_PW, + help='Default: ' + DEFAULT_LDAP_ROOT_PW + + '. LDAP root password.', +) + +flags.add_argument( + '--ldap-base-dn', + type=str, + default=DEFAULT_LDAP_BASE_DN, + help='Default: ' + DEFAULT_LDAP_BASE_DN + + '. LDAP users base DN.', +) + +flags.add_argument( + '--ldap-user-search', + type=str, + default=DEFAULT_LDAP_USER_SEARCH, + help='Default: ' + DEFAULT_LDAP_USER_SEARCH + + '. LDAP user search filter.', +) + +flags.add_argument( + '--ldap-auth-timeout', + type=int, + default=DEFAULT_LDAP_AUTH_TIMEOUT, + help='Default: ' + str(DEFAULT_LDAP_AUTH_TIMEOUT) + + '. LDAP user auth timeout.', +) + + +class LDAPAuthPlugin(HttpProxyBasePlugin): + """Performs proxy authentication through LDAP.""" + + __auth_pass__: Dict[bytes, float] = {} + + def auth_user(self, user: str, password: str) -> bool: + ldap_connection = ldap.initialize(self.flags.ldap_server) + ldap_connection.bind_s(self.flags.ldap_root_dn, self.flags.ldap_root_pw) + search_filter = self.flags.ldap_user_search.format(user=user) + search_result = ldap_connection.search_s(self.flags.ldap_base_dn, ldap.SCOPE_SUBTREE, search_filter, ['uid']) + if len(search_result) != 1 and len(search_result[0]) != 2: + return False + try: + ldap_connection.bind_s(search_result[0][0], password) + except ldap.LDAPError: + return False + return True + + def before_upstream_connection( + self, request: HttpParser, + ) -> Optional[HttpParser]: + if not request.headers or httpHeaders.PROXY_AUTHORIZATION not in request.headers: + raise ProxyAuthenticationFailed() + parts = request.headers[httpHeaders.PROXY_AUTHORIZATION][1].split() + if len(parts) != 2 or parts[0].lower() != b'basic': + raise ProxyAuthenticationFailed() + elif self.__auth_pass__.get(parts[1], 0) > time(): + return request + elif self.__auth_pass__.get(parts[1], 0) < time(): + userpass = base64.b64decode(parts[1]).decode().split(':') + user = userpass[0] + password = userpass[-1] + if self.auth_user(user, password): + self.__auth_pass__[parts[1]] = time() + self.flags.ldap_auth_timeout + return request + raise ProxyAuthenticationFailed() diff --git a/requirements-release.txt b/requirements-release.txt index 6e7b54213a..d7db7fe983 100644 --- a/requirements-release.txt +++ b/requirements-release.txt @@ -1,2 +1,3 @@ setuptools-scm == 6.3.2 twine==3.8.0 +python-ldap==3.4.3 diff --git a/requirements-testing.txt b/requirements-testing.txt index 36f6fdb5c7..1057d1c045 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -14,6 +14,7 @@ tox==3.25.1 mccabe==0.6.1 pylint==2.13.7 rope==1.1.1 +python-ldap==3.4.3 # Required by test_http2.py httpx==0.22.0 h2==4.1.0 diff --git a/setup.cfg b/setup.cfg index 124681dd94..9f0780bd04 100644 --- a/setup.cfg +++ b/setup.cfg @@ -106,6 +106,7 @@ zip_safe = False # These are required in actual runtime: install_requires = + python-ldap==3.4.3 [options.entry_points] console_scripts =