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
|
*.sqlite
|
||||||
*.pyc
|
*.pyc
|
||||||
venv/*
|
venv/*
|
||||||
|
env
|
||||||
.*@neomake*
|
.*@neomake*
|
||||||
.ash_history
|
.ash_history
|
||||||
.python_history
|
.python_history
|
||||||
config.toml
|
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.
|
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.
|
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"
|
LANGUAGE = "en"
|
||||||
|
|
||||||
[LDAP]
|
[LDAP]
|
||||||
URI = "ldaps://ldap.mydomain.tld"
|
URI = "ldap://ldap"
|
||||||
BIND_USER = "cn=admin,dc=mydomain,dc=tld"
|
ROOT_DN = "dc=mydomain,dc=tld"
|
||||||
|
BIND_DN = "cn=admin,dc=mydomain,dc=tld"
|
||||||
BIND_PW = "admin"
|
BIND_PW = "admin"
|
||||||
|
|
|
@ -8,8 +8,11 @@ services:
|
||||||
- LDAP_DOMAIN=mydomain.tld
|
- LDAP_DOMAIN=mydomain.tld
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker/bootstrap.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/50-boostrap.ldif:ro
|
- ./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
|
command: --copy-service
|
||||||
|
ports:
|
||||||
|
- 5389:389
|
||||||
|
- 5636:636
|
||||||
|
|
||||||
oauth:
|
oauth:
|
||||||
build:
|
build:
|
||||||
|
|
|
@ -3,4 +3,7 @@ flask
|
||||||
flask-babel
|
flask-babel
|
||||||
flask-wtf
|
flask-wtf
|
||||||
python-ldap
|
python-ldap
|
||||||
|
pytest
|
||||||
|
pytest-flask
|
||||||
toml
|
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 flask_babel import Babel
|
||||||
|
|
||||||
from .oauth2utils import config_oauth
|
from .oauth2utils import config_oauth
|
||||||
|
from .ldaputils import LDAPObjectHelper
|
||||||
|
|
||||||
|
|
||||||
def create_app(config=None):
|
def create_app(config=None):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
app.config.from_mapping(
|
app.config.from_mapping({"OAUTH2_REFRESH_TOKEN_GENERATOR": True})
|
||||||
{"OAUTH2_REFRESH_TOKEN_GENERATOR": True,}
|
if config:
|
||||||
)
|
app.config.from_mapping(config)
|
||||||
app.config.from_mapping(toml.load(os.environ.get("CONFIG", "config.toml")))
|
elif "CONFIG" in os.environ:
|
||||||
|
app.config.from_mapping(toml.load(os.environ.get("CONFIG")))
|
||||||
app.url_map.strict_slashes = False
|
elif os.path.exists("config.toml"):
|
||||||
|
app.config.from_mapping(toml.load("config.toml"))
|
||||||
|
|
||||||
setup_app(app)
|
setup_app(app)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def setup_app(app):
|
def setup_app(app):
|
||||||
@app.before_request
|
app.url_map.strict_slashes = False
|
||||||
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
|
|
||||||
|
|
||||||
config_oauth(app)
|
config_oauth(app)
|
||||||
app.register_blueprint(routes.bp)
|
app.register_blueprint(routes.bp)
|
||||||
|
@ -44,6 +35,20 @@ def setup_app(app):
|
||||||
|
|
||||||
babel = Babel(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
|
@app.context_processor
|
||||||
def global_processor():
|
def global_processor():
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -8,6 +8,7 @@ class LDAPObjectHelper:
|
||||||
may = None
|
may = None
|
||||||
must = None
|
must = None
|
||||||
base = None
|
base = None
|
||||||
|
root_dn = None
|
||||||
id = None
|
id = None
|
||||||
|
|
||||||
def __init__(self, dn=None, **kwargs):
|
def __init__(self, dn=None, **kwargs):
|
||||||
|
@ -29,6 +30,10 @@ class LDAPObjectHelper:
|
||||||
self.__class__.__name__, self.id, getattr(self, self.id)
|
self.__class__.__name__, self.id, getattr(self, self.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ldap(cls):
|
||||||
|
return g.ldap
|
||||||
|
|
||||||
def keys(self):
|
def keys(self):
|
||||||
return self.must + self.may
|
return self.must + self.may
|
||||||
|
|
||||||
|
@ -40,20 +45,37 @@ class LDAPObjectHelper:
|
||||||
self.__setattr__(k, v)
|
self.__setattr__(k, v)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
g.ldap.delete_s(self.dn)
|
self.ldap().delete_s(self.dn)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dn(self):
|
def dn(self):
|
||||||
if not self.id in self.attrs:
|
if not self.id in self.attrs:
|
||||||
return None
|
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
|
@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:
|
if cls._object_class_by_name:
|
||||||
return 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=*)", ["*", "+"]
|
"cn=subschema", ldap.SCOPE_BASE, "(objectclass=*)", ["*", "+"]
|
||||||
)
|
)
|
||||||
subschema_entry = res[0]
|
subschema_entry = res[0]
|
||||||
|
@ -69,11 +91,13 @@ class LDAPObjectHelper:
|
||||||
return cls._object_class_by_name
|
return cls._object_class_by_name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def attr_type_by_name(cls):
|
def attr_type_by_name(cls, conn=None):
|
||||||
if cls._attribute_type_by_name:
|
if cls._attribute_type_by_name:
|
||||||
return 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=*)", ["*", "+"]
|
"cn=subschema", ldap.SCOPE_BASE, "(objectclass=*)", ["*", "+"]
|
||||||
)
|
)
|
||||||
subschema_entry = res[0]
|
subschema_entry = res[0]
|
||||||
|
@ -88,9 +112,10 @@ class LDAPObjectHelper:
|
||||||
|
|
||||||
return cls._attribute_type_by_name
|
return cls._attribute_type_by_name
|
||||||
|
|
||||||
def save(self):
|
def save(self, conn=None):
|
||||||
|
conn = conn or self.ldap()
|
||||||
try:
|
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:
|
except ldap.NO_SUCH_OBJECT:
|
||||||
match = False
|
match = False
|
||||||
|
|
||||||
|
@ -99,19 +124,19 @@ class LDAPObjectHelper:
|
||||||
(ldap.MOD_REPLACE, k, [elt.encode("utf-8") for elt in v])
|
(ldap.MOD_REPLACE, k, [elt.encode("utf-8") for elt in v])
|
||||||
for k, v in self.attrs.items()
|
for k, v in self.attrs.items()
|
||||||
]
|
]
|
||||||
g.ldap.modify_s(self.dn, attributes)
|
conn.modify_s(self.dn, attributes)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
attributes = [
|
attributes = [
|
||||||
(k, [elt.encode("utf-8") for elt in v]) for k, v in self.attrs.items()
|
(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
|
@classmethod
|
||||||
def get(cls, dn):
|
def get(cls, dn):
|
||||||
if "=" not in dn:
|
if "=" not in dn:
|
||||||
dn = f"{cls.id}={dn},{cls.base}"
|
dn = f"{cls.id}={dn},{cls.base},{cls.root_dn}"
|
||||||
result = g.ldap.search_s(dn, ldap.SCOPE_SUBTREE)
|
result = cls.ldap().search_s(dn, ldap.SCOPE_SUBTREE)
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
return None
|
return None
|
||||||
|
@ -127,7 +152,8 @@ class LDAPObjectHelper:
|
||||||
class_filter = "".join([f"(objectClass={oc})" for oc in cls.objectClass])
|
class_filter = "".join([f"(objectClass={oc})" for oc in cls.objectClass])
|
||||||
arg_filter = "".join(f"({k}={v})" for k, v in kwargs.items())
|
arg_filter = "".join(f"({k}={v})" for k, v in kwargs.items())
|
||||||
ldapfilter = f"(&{class_filter}{arg_filter})"
|
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 [
|
return [
|
||||||
cls(**{k: [elt.decode("utf-8") for elt in v] for k, v in args.items()},)
|
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):
|
class User(LDAPObjectHelper):
|
||||||
objectClass = ["person"]
|
objectClass = ["person"]
|
||||||
base = "ou=users,dc=mydomain,dc=tld"
|
base = "ou=users"
|
||||||
id = "cn"
|
id = "cn"
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
|
@ -24,7 +24,7 @@ class User(LDAPObjectHelper):
|
||||||
|
|
||||||
class Client(LDAPObjectHelper, ClientMixin):
|
class Client(LDAPObjectHelper, ClientMixin):
|
||||||
objectClass = ["oauthClient"]
|
objectClass = ["oauthClient"]
|
||||||
base = "ou=clients,dc=mydomain,dc=tld"
|
base = "ou=clients"
|
||||||
id = "oauthClientID"
|
id = "oauthClientID"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -93,7 +93,7 @@ class Client(LDAPObjectHelper, ClientMixin):
|
||||||
|
|
||||||
class AuthorizationCode(LDAPObjectHelper, AuthorizationCodeMixin):
|
class AuthorizationCode(LDAPObjectHelper, AuthorizationCodeMixin):
|
||||||
objectClass = ["oauthAuthorizationCode"]
|
objectClass = ["oauthAuthorizationCode"]
|
||||||
base = "ou=authorizations,dc=mydomain,dc=tld"
|
base = "ou=authorizations"
|
||||||
id = "oauthCode"
|
id = "oauthCode"
|
||||||
|
|
||||||
def get_redirect_uri(self):
|
def get_redirect_uri(self):
|
||||||
|
@ -121,7 +121,7 @@ class AuthorizationCode(LDAPObjectHelper, AuthorizationCodeMixin):
|
||||||
|
|
||||||
class Token(LDAPObjectHelper, TokenMixin):
|
class Token(LDAPObjectHelper, TokenMixin):
|
||||||
objectClass = ["oauthToken"]
|
objectClass = ["oauthToken"]
|
||||||
base = "ou=tokens,dc=mydomain,dc=tld"
|
base = "ou=tokens"
|
||||||
id = "oauthAccessToken"
|
id = "oauthAccessToken"
|
||||||
|
|
||||||
def get_client_id(self):
|
def get_client_id(self):
|
||||||
|
|
Loading…
Reference in a new issue