From 531c34a689187b3274f4f67237164d5f70b4432f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Tue, 18 Aug 2020 17:39:34 +0200 Subject: [PATCH] tests workflow --- .gitignore | 2 + .gitlab-ci.yml | 29 ++ README.md | 12 + config.sample.toml | 5 +- docker-compose.yml | 5 +- requirements.txt | 3 + .../oauth2-openldap.ldif | 0 schemas/oauth2-openldap.schema | 334 ++++++++++++++++++ setup.cfg | 11 + tests/__init__.py | 0 tests/conftest.py | 117 ++++++ tests/test_password_flow.py | 2 + web/__init__.py | 41 ++- web/ldaputils.py | 52 ++- web/models.py | 8 +- 15 files changed, 583 insertions(+), 38 deletions(-) create mode 100644 .gitlab-ci.yml rename oauth2-openldap.ldif => schemas/oauth2-openldap.ldif (100%) create mode 100644 schemas/oauth2-openldap.schema create mode 100644 setup.cfg create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_password_flow.py diff --git a/.gitignore b/.gitignore index 6ee42de7..4ef59b57 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ *.sqlite *.pyc venv/* +env .*@neomake* .ash_history .python_history config.toml +.tox diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..cdf1d927 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,29 @@ +--- +image: python + +stages: + - test + - build + - release + +before_script: + - apt update + - env DEBIAN_FRONTEND=noninteractive apt install --yes slapd python3-dev libldap2-dev libsasl2-dev libssl-dev ldap-utils + - curl -O https://bootstrap.pypa.io/get-pip.py + - python get-pip.py + - pip install tox poetry coveralls pyyaml + +python36: + image: python:3.6 + stage: test + script: tox -e py36 + +python37: + image: python:3.7 + stage: test + script: tox -e py37 + +python38: + image: python:3.8 + stage: test + script: tox -e py38 diff --git a/README.md b/README.md index ed09a5a1..f51a1120 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,15 @@ oidc-ldap-bridge is a simple OpenID Connect provider based upon OpenLDAP. It authenticates your LDAP users, and do not need any additional database to work. Everything is stored in your OpenLDAP server. + +## Contribute + +Contributions are welcome! +To run the tests, you just need to run `tox`. + +To try a development environment, you can run the docker image and then open https://127.0.0.1:5000 + +```bash +cp config.sample.toml config.toml +docker-compose up +``` diff --git a/config.sample.toml b/config.sample.toml index 1a5b2e4e..47ee4fae 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -6,6 +6,7 @@ NAME = "MyDomain" LANGUAGE = "en" [LDAP] -URI = "ldaps://ldap.mydomain.tld" -BIND_USER = "cn=admin,dc=mydomain,dc=tld" +URI = "ldap://ldap" +ROOT_DN = "dc=mydomain,dc=tld" +BIND_DN = "cn=admin,dc=mydomain,dc=tld" BIND_PW = "admin" diff --git a/docker-compose.yml b/docker-compose.yml index 3f3c0122..6b024972 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,8 +8,11 @@ services: - LDAP_DOMAIN=mydomain.tld volumes: - ./docker/bootstrap.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/50-boostrap.ldif:ro - - ./oauth2-openldap.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/40-oauth2.ldif:ro + - ./schemas/oauth2-openldap.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/40-oauth2.ldif:ro command: --copy-service + ports: + - 5389:389 + - 5636:636 oauth: build: diff --git a/requirements.txt b/requirements.txt index 9d33de9f..137e3119 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,7 @@ flask flask-babel flask-wtf python-ldap +pytest +pytest-flask toml +pdbpp diff --git a/oauth2-openldap.ldif b/schemas/oauth2-openldap.ldif similarity index 100% rename from oauth2-openldap.ldif rename to schemas/oauth2-openldap.ldif diff --git a/schemas/oauth2-openldap.schema b/schemas/oauth2-openldap.schema new file mode 100644 index 00000000..506e34dc --- /dev/null +++ b/schemas/oauth2-openldap.schema @@ -0,0 +1,334 @@ +attributetype ( 1.3.6.1.4.1.56207.1.1.1 NAME 'oauthCode' + DESC 'OAuth 2.0 Authorization Code' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.2 NAME 'oauthClientID' + DESC 'Authorized client' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.3 NAME 'oauthRedirectURI' + DESC 'Authorization Code Redirection URI' + EQUALITY caseIgnoreMatch + ORDERING caseIgnoreOrderingMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.4 NAME 'oauthResponseType' + DESC 'OAuth 2.0 response type' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.5 NAME 'oauthScope' + DESC 'OAuth 2.0 scope value' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.6 NAME 'oauthNonce' + DESC 'OAuth 2.0 nonce' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.7 NAME 'oauthAuthorizationDate' + DESC 'Access token issue date' + EQUALITY generalizedTimeMatch + ORDERING generalizedTimeOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.8 NAME 'oauthCodeChallenge' + DESC 'OAuth 2.0 nonce' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.9 NAME 'oauthCodeChallengeMethod' + DESC 'OAuth 2.0 nonce' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.10 NAME 'oauthClientSecret' + DESC 'Client secret' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.11 NAME 'oauthClientSecretExpDate' + DESC 'Client secret expiration date/time' + EQUALITY generalizedTimeMatch + ORDERING generalizedTimeOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.12 NAME 'oauthIssueDate' + DESC 'Client identifier issue date/time' + EQUALITY generalizedTimeMatch + ORDERING generalizedTimeOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.13 NAME 'oauthGrantType' + DESC 'OAuth 2.0 grant type' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.14 NAME 'oauthTokenLifetime' + DESC 'OAuth 2.0 refresh token lifetime, in seconds' + EQUALITY integerMatch + ORDERING integerOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.15 NAME 'oauthClientName' + DESC 'Client name' + EQUALITY caseIgnoreMatch + ORDERING caseIgnoreOrderingMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.16 NAME 'oauthClientContact' + DESC 'Client name' + EQUALITY caseIgnoreMatch + ORDERING caseIgnoreOrderingMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.17 NAME 'oauthClientURI' + DESC 'Client URI' + EQUALITY caseIgnoreMatch + ORDERING caseIgnoreOrderingMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.18 NAME 'oauthLogoURI' + DESC 'Logo URI' + EQUALITY caseIgnoreMatch + ORDERING caseIgnoreOrderingMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.19 NAME 'oauthTermsOfServiceURI' + DESC 'Terms of service URI' + EQUALITY caseIgnoreMatch + ORDERING caseIgnoreOrderingMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.20 NAME 'oauthPolicyURI' + DESC 'Policy URI' + EQUALITY caseIgnoreMatch + ORDERING caseIgnoreOrderingMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.21 NAME 'oauthJWKURI' + DESC 'JWK set URI' + EQUALITY caseIgnoreMatch + ORDERING caseIgnoreOrderingMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.22 NAME 'oauthJWK' + DESC 'JWK set JSON' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.23 NAME 'oauthSoftwareID' + DESC 'Software identifier' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.24 NAME 'oauthSoftwareVersion' + DESC 'Software version' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.25 NAME 'oauthToken' + DESC 'OAuth 2.0 Token' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.26 NAME 'oauthTokenType' + DESC 'OAuth 2.0 Token' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.27 NAME 'oauthAccessToken' + DESC 'OAuth 2.0 access token' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.28 NAME 'oauthRefreshToken' + DESC 'OAuth 2.0 refresh token' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.29 NAME 'oauthTokenEndpointAuthMethod' + DESC 'OAuth 2.0 Token endpoint authentication method' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0 Dynamic Client Registration Protocol' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.30 NAME 'oauthSubject' + DESC 'OAuth 2.0 Token subject' + EQUALITY caseExactMatch + ORDERING caseExactOrderingMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0 Dynamic Client Registration Protocol' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.31 NAME 'oauthRedirectURIs' + DESC 'Authorization Code Redirection URI' + EQUALITY caseIgnoreMatch + ORDERING caseIgnoreOrderingMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +attributetype ( 1.3.6.1.4.1.56207.1.1.32 NAME 'oauthAuthorizationLifetime' + DESC 'OAuth 2.0 authorization code lifetime, in seconds' + EQUALITY integerMatch + ORDERING integerOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE + USAGE userApplications + X-ORIGIN 'OAuth 2.0' ) +objectclass ( 1.3.6.1.4.1.56207.1.2.1 NAME 'oauthClient' + DESC 'OAuth 2.0 Authorization Code' + SUP top + STRUCTURAL + MUST oauthClientID + MAY ( description $ + oauthClientName $ + oauthClientContact $ + oauthClientURI $ + oauthRedirectURIs $ + oauthLogoURI $ + oauthIssueDate $ + oauthClientSecret $ + oauthClientSecretExpDate $ + oauthGrantType $ + oauthResponseType $ + oauthScope $ + oauthTermsOfServiceURI $ + oauthPolicyURI $ + oauthJWKURI $ + oauthJWK $ + oauthTokenEndpointAuthMethod $ + oauthSoftwareID $ + oauthSoftwareVersion ) + ) + X-ORIGIN 'OAuth 2.0' ) +objectclass ( 1.3.6.1.4.1.56207.1.2.2 NAME 'oauthAuthorizationCode' + DESC 'OAuth 2.0 Authorization Code' + SUP top + STRUCTURAL + MUST oauthCode + MAY ( description $ + oauthClientID $ + oauthSubject $ + oauthRedirectURI $ + oauthResponseType $ + oauthScope $ + oauthNonce $ + oauthAuthorizationDate $ + oauthAuthorizationLifetime $ + oauthCodeChallenge $ + oauthCodeChallengeMethod ) + X-ORIGIN 'OAuth 2.0' ) +objectclass ( 1.3.6.1.4.1.56207.1.2.3 NAME 'oauthToken' + DESC 'OAuth 2.0 Token' + SUP top + STRUCTURAL + MUST oauthAccessToken + MAY ( description $ + oauthClientID $ + oauthSubject $ + oauthTokenType $ + oauthRefreshToken $ + oauthScope $ + oauthIssueDate $ + oauthTokenLifetime ) + X-ORIGIN 'OAuth 2.0' ) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..359ceb35 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[tox:tox] +envlist = + py36 + py37 + py38 +skipsdist=True + +[testenv] +install_command = pip install {packages} +commands = {envbindir}/pytest --showlocals {posargs} +deps = --requirement requirements.txt diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..77549b4c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,117 @@ +import datetime +import ldap.ldapobject +import os +import pytest +import slapdtest +from werkzeug.security import gen_salt +from web import create_app +from web.models import User, Client, Token, AuthorizationCode +from web.ldaputils import LDAPObjectHelper + + +class CustomSlapdObject(slapdtest.SlapdObject): + custom_schema_files = ("oauth2-openldap.schema",) + + def _ln_schema_files(self, *args, **kwargs): + dir_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "schemas" + ) + super()._ln_schema_files(*args, **kwargs) + super()._ln_schema_files(self.custom_schema_files, dir_path) + + def gen_config(self): + previous = self.openldap_schema_files + self.openldap_schema_files += self.custom_schema_files + config = super().gen_config() + self.openldap_schema_files = previous + return config + + +@pytest.fixture(scope="session") +def slapd_server(): + slapd = CustomSlapdObject() + try: + slapd.start() + suffix_dc = slapd.suffix.split(",")[0][3:] + slapd.ldapadd( + "\n".join( + [ + "dn: " + slapd.suffix, + "objectClass: dcObject", + "objectClass: organization", + "dc: " + suffix_dc, + "o: " + suffix_dc, + "", + "dn: " + slapd.root_dn, + "objectClass: applicationProcess", + "cn: " + slapd.root_cn, + ] + ) + + "\n" + ) + + yield slapd + finally: + slapd.stop() + + +@pytest.fixture +def slapd_connection(slapd_server): + conn = ldap.ldapobject.SimpleLDAPObject(slapd_server.ldap_uri) + conn.protocol_version = 3 + conn.simple_bind_s(slapd_server.root_dn, slapd_server.root_pw) + yield conn + conn.unbind_s() + + +@pytest.fixture +def app(slapd_server, slapd_connection): + LDAPObjectHelper.root_dn = slapd_server.suffix + Client.initialize(slapd_connection) + User.initialize(slapd_connection) + Token.initialize(slapd_connection) + AuthorizationCode.initialize(slapd_connection) + + app = create_app( + { + "LDAP": { + "URI": slapd_server.ldap_uri, + "BIND_DN": slapd_server.root_dn, + "BIND_PW": slapd_server.root_pw, + } + } + ) + return app + + +@pytest.fixture +def client(app, slapd_connection): + c = Client( + oauthClientID=gen_salt(24), + oauthClientName="Some client", + oauthClientContact="contact@mydomain.tld", + oauthClientURI="https://mydomain.tld", + oauthRedirectURIs=[ + "https://mydomain.tld/redirect1", + "https://mydomain.tld/redirect2", + ], + oauthLogoURI="https://mydomain.tld/logo.png", + oauthIssueDate=datetime.datetime.now().strftime("%Y%m%d%H%S%MZ"), + oauthClientSecret=gen_salt(48), + oauthGrantType=["password", "authorization_code"], + oauthResponseType=["code"], + oauthScope=["openid", "profile"], + oauthTermsOfServiceURI="https://mydomain.tld/tos", + oauthPolicyURI="https://mydomain.tld/policy", + oauthJWKURI="https://mydomain.tld/jwk", + oauthTokenEndpointAuthMethod="client_secret_basic", + ) + c.save(slapd_connection) + return c + + +@pytest.fixture +def user(app, slapd_connection): + u = User(cn="John Doe", sn="Doe",) + u.save(slapd_connection) + return u diff --git a/tests/test_password_flow.py b/tests/test_password_flow.py new file mode 100644 index 00000000..917a77b3 --- /dev/null +++ b/tests/test_password_flow.py @@ -0,0 +1,2 @@ +def test_foobar(slapd_connection, user, client): + assert True diff --git a/web/__init__.py b/web/__init__.py index 5686732c..6061038a 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -7,35 +7,26 @@ from flask import Flask, g, request from flask_babel import Babel from .oauth2utils import config_oauth +from .ldaputils import LDAPObjectHelper def create_app(config=None): app = Flask(__name__) - app.config.from_mapping( - {"OAUTH2_REFRESH_TOKEN_GENERATOR": True,} - ) - app.config.from_mapping(toml.load(os.environ.get("CONFIG", "config.toml"))) - - app.url_map.strict_slashes = False + app.config.from_mapping({"OAUTH2_REFRESH_TOKEN_GENERATOR": True}) + if config: + app.config.from_mapping(config) + elif "CONFIG" in os.environ: + app.config.from_mapping(toml.load(os.environ.get("CONFIG"))) + elif os.path.exists("config.toml"): + app.config.from_mapping(toml.load("config.toml")) setup_app(app) return app def setup_app(app): - @app.before_request - def before_request(): - g.ldap = ldap.initialize(app.config["LDAP"]["URI"]) - g.ldap.simple_bind_s( - app.config["LDAP"]["BIND_USER"], app.config["LDAP"]["BIND_PW"] - ) - - @app.after_request - def after_request(response): - if "ldap" in g: - g.ldap.unbind_s() - return response + app.url_map.strict_slashes = False config_oauth(app) app.register_blueprint(routes.bp) @@ -44,6 +35,20 @@ def setup_app(app): babel = Babel(app) + @app.before_request + def before_request(): + LDAPObjectHelper.root_dn = app.config["LDAP"]["ROOT_DN"] + g.ldap = ldap.initialize(app.config["LDAP"]["URI"]) + g.ldap.simple_bind_s( + app.config["LDAP"]["BIND_DN"], app.config["LDAP"]["BIND_PW"] + ) + + @app.after_request + def after_request(response): + if "ldap" in g: + g.ldap.unbind_s() + return response + @app.context_processor def global_processor(): return { diff --git a/web/ldaputils.py b/web/ldaputils.py index 4e26c71d..0cdcaecc 100644 --- a/web/ldaputils.py +++ b/web/ldaputils.py @@ -8,6 +8,7 @@ class LDAPObjectHelper: may = None must = None base = None + root_dn = None id = None def __init__(self, dn=None, **kwargs): @@ -29,6 +30,10 @@ class LDAPObjectHelper: self.__class__.__name__, self.id, getattr(self, self.id) ) + @classmethod + def ldap(cls): + return g.ldap + def keys(self): return self.must + self.may @@ -40,20 +45,37 @@ class LDAPObjectHelper: self.__setattr__(k, v) def delete(self): - g.ldap.delete_s(self.dn) + self.ldap().delete_s(self.dn) @property def dn(self): if not self.id in self.attrs: return None - return f"{self.id}={self.attrs[self.id][0]},{self.base}" + return f"{self.id}={self.attrs[self.id][0]},{self.base},{self.root_dn}" @classmethod - def ocs_by_name(cls): + def initialize(cls, conn=None): + conn = conn or cls.ldap() + cls.ocs_by_name(conn) + cls.attr_type_by_name(conn) + + dn = f"{cls.base},{cls.root_dn}" + conn.add_s( + dn, + [ + ("objectClass", [b"organizationalUnit"]), + ("ou", [cls.base.encode("utf-8")]), + ], + ) + + @classmethod + def ocs_by_name(cls, conn=None): if cls._object_class_by_name: return cls._object_class_by_name - res = g.ldap.search_s( + conn = conn or cls.ldap() + + res = conn.search_s( "cn=subschema", ldap.SCOPE_BASE, "(objectclass=*)", ["*", "+"] ) subschema_entry = res[0] @@ -69,11 +91,13 @@ class LDAPObjectHelper: return cls._object_class_by_name @classmethod - def attr_type_by_name(cls): + def attr_type_by_name(cls, conn=None): if cls._attribute_type_by_name: return cls._attribute_type_by_name - res = g.ldap.search_s( + conn = conn or cls.ldap() + + res = conn.search_s( "cn=subschema", ldap.SCOPE_BASE, "(objectclass=*)", ["*", "+"] ) subschema_entry = res[0] @@ -88,9 +112,10 @@ class LDAPObjectHelper: return cls._attribute_type_by_name - def save(self): + def save(self, conn=None): + conn = conn or self.ldap() try: - match = bool(g.ldap.search_s(self.dn, ldap.SCOPE_SUBTREE)) + match = bool(conn.search_s(self.dn, ldap.SCOPE_SUBTREE)) except ldap.NO_SUCH_OBJECT: match = False @@ -99,19 +124,19 @@ class LDAPObjectHelper: (ldap.MOD_REPLACE, k, [elt.encode("utf-8") for elt in v]) for k, v in self.attrs.items() ] - g.ldap.modify_s(self.dn, attributes) + conn.modify_s(self.dn, attributes) else: attributes = [ (k, [elt.encode("utf-8") for elt in v]) for k, v in self.attrs.items() ] - g.ldap.add_s(self.dn, attributes) + conn.add_s(self.dn, attributes) @classmethod def get(cls, dn): if "=" not in dn: - dn = f"{cls.id}={dn},{cls.base}" - result = g.ldap.search_s(dn, ldap.SCOPE_SUBTREE) + dn = f"{cls.id}={dn},{cls.base},{cls.root_dn}" + result = cls.ldap().search_s(dn, ldap.SCOPE_SUBTREE) if not result: return None @@ -127,7 +152,8 @@ class LDAPObjectHelper: class_filter = "".join([f"(objectClass={oc})" for oc in cls.objectClass]) arg_filter = "".join(f"({k}={v})" for k, v in kwargs.items()) ldapfilter = f"(&{class_filter}{arg_filter})" - result = g.ldap.search_s(base or cls.base, ldap.SCOPE_SUBTREE, ldapfilter) + base = base or f"{cls.base},{cls.root_dn}" + result = cls.ldap().search_s(base, ldap.SCOPE_SUBTREE, ldapfilter) return [ cls(**{k: [elt.decode("utf-8") for elt in v] for k, v in args.items()},) diff --git a/web/models.py b/web/models.py index ce0b81af..73efcd0f 100644 --- a/web/models.py +++ b/web/models.py @@ -11,7 +11,7 @@ from .ldaputils import LDAPObjectHelper class User(LDAPObjectHelper): objectClass = ["person"] - base = "ou=users,dc=mydomain,dc=tld" + base = "ou=users" id = "cn" def check_password(self, password): @@ -24,7 +24,7 @@ class User(LDAPObjectHelper): class Client(LDAPObjectHelper, ClientMixin): objectClass = ["oauthClient"] - base = "ou=clients,dc=mydomain,dc=tld" + base = "ou=clients" id = "oauthClientID" @property @@ -93,7 +93,7 @@ class Client(LDAPObjectHelper, ClientMixin): class AuthorizationCode(LDAPObjectHelper, AuthorizationCodeMixin): objectClass = ["oauthAuthorizationCode"] - base = "ou=authorizations,dc=mydomain,dc=tld" + base = "ou=authorizations" id = "oauthCode" def get_redirect_uri(self): @@ -121,7 +121,7 @@ class AuthorizationCode(LDAPObjectHelper, AuthorizationCodeMixin): class Token(LDAPObjectHelper, TokenMixin): objectClass = ["oauthToken"] - base = "ou=tokens,dc=mydomain,dc=tld" + base = "ou=tokens" id = "oauthAccessToken" def get_client_id(self):