forked from Github-Mirrors/canaille
tests workflow
This commit is contained in:
parent
9e75432145
commit
531c34a689
15 changed files with 583 additions and 38 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,7 +1,9 @@
|
|||
*.sqlite
|
||||
*.pyc
|
||||
venv/*
|
||||
env
|
||||
.*@neomake*
|
||||
.ash_history
|
||||
.python_history
|
||||
config.toml
|
||||
.tox
|
||||
|
|
29
.gitlab-ci.yml
Normal file
29
.gitlab-ci.yml
Normal file
|
@ -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
|
12
README.md
12
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
|
||||
```
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -3,4 +3,7 @@ flask
|
|||
flask-babel
|
||||
flask-wtf
|
||||
python-ldap
|
||||
pytest
|
||||
pytest-flask
|
||||
toml
|
||||
pdbpp
|
||||
|
|
334
schemas/oauth2-openldap.schema
Normal file
334
schemas/oauth2-openldap.schema
Normal file
|
@ -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' )
|
11
setup.cfg
Normal file
11
setup.cfg
Normal file
|
@ -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
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
117
tests/conftest.py
Normal file
117
tests/conftest.py
Normal file
|
@ -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
|
2
tests/test_password_flow.py
Normal file
2
tests/test_password_flow.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
def test_foobar(slapd_connection, user, client):
|
||||
assert True
|
|
@ -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 {
|
||||
|
|
|
@ -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()},)
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue