Development environment without docker. Plus client samples. Fixes #18

This commit is contained in:
Éloi Rivard 2020-11-10 17:52:52 +01:00
parent 87d9937436
commit 7762e58d67
19 changed files with 480 additions and 37 deletions

View file

@ -16,13 +16,12 @@ We use `black` to format our code. Please apply `black` on your patches before s
Development environment
-----------------------
To try a development environment, you can run the docker image and then open https://127.0.0.1:5000
To try a development environment, you can run the docker image and then open https://127.0.0.1:5000 to access the canaille server.
Two dummy clients are available at https://127.0.0.1:5001 and https://127.0.0.1:5002
You can then connect with user *admin* and password *admin* to access an admin account, or user *user* and password *user* for a regular one.
.. code-block:: console
cp canaille/conf/config.sample.toml canaille/conf/config.toml
cp canaille/conf/oauth-authorization-server.sample.json canaille/conf/oauth-authorization-server.json
cp canaille/conf/openid-configuration.sample.json canaille/conf/openid-configuration.json
cd dev
docker-compose up
cd demo
./run.sh
# or 'docker-compose up' if you prefer docker

2
demo/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
env
*.pem

4
demo/Procfile Normal file
View file

@ -0,0 +1,4 @@
slapd: ./slapd.sh
canaille: env AUTHLIB_INSECURE_TRANSPORT=1 FLASK_ENV=development CONFIG=conf/canaille.toml FLASK_APP=../../canaille env/bin/flask run --extra-files conf/canaille.toml
client1: env FLASK_ENV=development CONFIG=../conf/client1.cfg FLASK_APP=client env/bin/flask run --port=5001
client2: env FLASK_ENV=development CONFIG=../conf/client2.cfg FLASK_APP=client env/bin/flask run --port=5002

18
demo/README.md Normal file
View file

@ -0,0 +1,18 @@
# Demo and development
To check out how canaille looks like, or to start contributions, just run it!
- If you have *OpenLDAP* installed on your system, run: `./run.sh`.
- If you prefer using docker, you can also run `docker-compose up`.
Then you have access to:
- A canaille server at http://127.0.0.1:5000
- A dummy client at http://127.0.0.1:5001
- Another dummy client at http://127.0.0.1:5002
The canaille server has some default users:
- A regular user which login and password are *user*;
- A moderator user which login and password are *moderator*;
- An admin user which admin and password are *admin*.

51
demo/client/__init__.py Normal file
View file

@ -0,0 +1,51 @@
from authlib.integrations.flask_client import OAuth
from authlib.oidc.discovery import get_well_known_url
from flask import Flask, render_template, redirect, url_for, flash, session
def create_app():
app = Flask(__name__)
app.config.from_envvar("CONFIG")
app.static_folder = "../../canaille/static"
oauth = OAuth()
oauth.init_app(app)
oauth.register(
name="yaal",
client_id=app.config["OAUTH_CLIENT_ID"],
client_secret=app.config["OAUTH_CLIENT_SECRET"],
server_metadata_url=get_well_known_url(
app.config["OAUTH_AUTH_SERVER"], external=True
),
client_kwargs={"scope": "openid profile email"},
)
@app.route("/")
def index():
return render_template(
"index.html", user=session.get("user"), name=app.config["NAME"]
)
@app.route("/login")
def login():
return oauth.yaal.authorize_redirect(url_for("authorize", _external=True))
@app.route("/authorize")
def authorize():
token = oauth.yaal.authorize_access_token()
userinfo = oauth.yaal.parse_id_token(token)
session["user"] = userinfo
flash("You have been successfully logged in.", "success")
return redirect(url_for("index"))
@app.route("/logout")
def logout():
try:
del session["user"]
except KeyError:
pass
flash("You have been successfully logged out.", "success")
return redirect(url_for("index"))
return app

View file

@ -0,0 +1,69 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Canaille test client</title>
<link href="/static/fomanticui/semantic.min.css" rel="stylesheet">
<link href="/static/css/base.css" rel="stylesheet">
{% if logo_url %}<link rel="icon" href="{{ favicon_url }}">{% endif %}
{% block style %}{% endblock %}
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<script>window.html5 || document.write('<script src="/static/js/html5shiv.min.js"><\/script>')</script>
<script src="/static/js/respond.min.js"></script>
<![endif]-->
</head>
<body>
<nav class="ui stackable labeled icon menu container">
<div class="header item">
<a href="/" class="logo">
<img class="ui image" src="/static/img/canaille-head.png" alt="Canaille client" />
</a>
</div>
{% if user %}
<a class="item" href="{{ url_for('logout') }}">
<i class="sign out alternate icon"></i>
Log out
</a>
{% else %}
<a class="item" href="{{ url_for('login') }}">
<i class="sign in alternate icon"></i>
Log in
</a>
{% endif %}
</nav>
<div class="ui container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="ui attached message {{ category }}">
{{ message }}
</div>
{% endfor %}
{% endwith %}
<div class="ui segment">
<h2 class="ui header">{{ name }}</h2>
<div>
{% if user %}
Welcome {{ user.name }}
{% else %}
Welcome, please <a href="{{ url_for('login') }}">log-in</a>.
{% endif %}
</div>
</div>
</div>
<script src="/static/jquery/jquery.min.js"></script>
<script src="/static/fomanticui/semantic.min.js"></script>
<script src="/static/js/base.js"></script>
{% block script %}{% endblock %}
</body>
</html>

87
demo/conf/canaille.toml Normal file
View file

@ -0,0 +1,87 @@
# The flask secret key for cookies. You MUST change this.
SECRET_KEY = "change me before you go in production"
# Your organization name.
NAME = "Canaille"
# The interface on which canaille will be served
# SERVER_NAME = "auth.mydomain.tld"
# PREFERRED_URL_SCHEME = "https"
# You can display a logo to be recognized on login screens
LOGO = "/static/img/canaille-head.png"
# Your favicon. If unset the LOGO will be used.
FAVICON = "/static/img/canaille-c.png"
# If unset, language is detected
# LANGUAGE = "en"
# Path to the RFC8414 metadata file. You should update those files
# with your production URLs.
OAUTH2_METADATA_FILE = "conf/oauth-authorization-server.json"
OIDC_METADATA_FILE = "conf/openid-configuration.json"
# If you have a sentry instance, you can set its dsn here:
# SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
[LDAP]
URI = "ldap://127.0.0.1:5389"
ROOT_DN = "dc=mydomain,dc=tld"
BIND_DN = "cn=admin,dc=mydomain,dc=tld"
BIND_PW = "admin"
# Where to search for users?
USER_BASE = "ou=users,dc=mydomain,dc=tld"
# Filter to match users on sign in. Supports a variable
# {login}. For sigin against either uid or mail use:
# USER_FILTER = "(|(uid={login})(mail={login}))"
USER_FILTER = "(|(uid={login})(cn={login}))"
# A class to use for creating new users
USER_CLASS = "inetOrgPerson"
# Filter to match super admin users. Super admins can manage
# OAuth clients, tokens and authorizations. If your LDAP server has
# the 'memberof' overlay, you can filter against group membership.
# ADMIN_FILTER = "uid=admin"
ADMIN_FILTER = "memberof=cn=admins,ou=groups,dc=mydomain,dc=tld"
# Filter to match super admin users. User admins can edit, create
# and delete user accounts. If your LDAP server has the 'memberof'
# overlay, you can filter against group membership.
# USER_ADMIN_FILTER = "uid=moderator"
USER_ADMIN_FILTER = "memberof=cn=moderators,ou=groups,dc=mydomain,dc=tld"
# The jwt configuration. You can generate a RSA keypair with:
# ssh-keygen -t rsa -b 4096 -m PEM -f private.pem
# openssl rsa -in private.pem -pubout -outform PEM -out public.pem
[JWT]
PUBLIC_KEY = "conf/public.pem"
PRIVATE_KEY = "conf/private.pem"
KTY = "RSA"
ALG = "RS256"
EXP = 3600
[JWT.MAPPING]
# Mapping between JWT fields and LDAP attributes from your
# User objectClass. Default values fits inetOrgPerson.
SUB = "uid"
NAME = "cn"
PHONE_NUMBER = "telephoneNumber"
EMAIL = "mail"
GIVEN_NAME = "givenName"
FAMILY_NAME = "sn"
PREFERRED_USERNAME = "displayName"
LOCALE = "preferredLanguage"
PICTURE = "photo"
ADDRESS = "postalAddress"
[SMTP]
HOST = "localhost"
PORT = 25
TLS = false
LOGIN = "smtp_user"
PASSWORD = "smtp_password"
FROM_ADDR = "admin@mydomain.tld"

7
demo/conf/client1.cfg Normal file
View file

@ -0,0 +1,7 @@
SECRET_KEY="46bf9fb5-88d5-489b-9312-899588377ff0"
NAME = "Client 2"
SESSION_COOKIE_NAME="client1-session"
OAUTH_CLIENT_ID="1JGkkzCbeHpGtlqgI5EENByf"
OAUTH_CLIENT_SECRET="2xYPSReTQRmGG1yppMVZQ0ASXwFejPyirvuPbKhNa6TmKC5x"
OAUTH_AUTH_SERVER="http://localhost:5000"

7
demo/conf/client2.cfg Normal file
View file

@ -0,0 +1,7 @@
SECRET_KEY="8e953ecc-13be-497b-806f-c65faa1e328f"
NAME = "Client 2"
SESSION_COOKIE_NAME="client2-session"
OAUTH_CLIENT_ID="gn4yFN7GDykL7QP8v8gS9YfV"
OAUTH_CLIENT_SECRET="ouFJE5WpICt6hxTyf8icXPeeklMektMY4gV0Rmf3aY60VElA"
OAUTH_AUTH_SERVER="http://localhost:5000"

View file

@ -0,0 +1,29 @@
{
"issuer":
"http://localhost:5000",
"authorization_endpoint":
"http://localhost:5000/oauth/authorize",
"token_endpoint":
"http://localhost:5000/oauth/token",
"token_endpoint_auth_methods_supported":
["client_secret_basic", "private_key_jwt",
"client_secret_post", "none"],
"token_endpoint_auth_signing_alg_values_supported":
["RS256", "ES256"],
"userinfo_endpoint":
"http://localhost:5000/oauth/userinfo",
"jwks_uri":
"http://localhost:5000/oauth/jwks.json",
"registration_endpoint":
"http://localhost:5000/oauth/register",
"scopes_supported":
["openid", "profile", "email", "address",
"phone"],
"response_types_supported":
["code", "token", "id_token", "code token",
"code id_token", "token id_token"],
"service_documentation":
"http://localhost:5000/documentation.html",
"ui_locales_supported":
["en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"]
}

View file

@ -0,0 +1,64 @@
{
"issuer":
"http://localhost:5000",
"authorization_endpoint":
"http://localhost:5000/oauth/authorize",
"token_endpoint":
"http://localhost:5000/oauth/token",
"token_endpoint_auth_methods_supported":
["client_secret_basic", "private_key_jwt",
"client_secret_post", "none"],
"token_endpoint_auth_signing_alg_values_supported":
["RS256"],
"userinfo_endpoint":
"http://localhost:5000/oauth/userinfo",
"check_session_iframe":
"http://localhost:5000/oauth/check_session",
"end_session_endpoint":
"http://localhost:5000/oauth/end_session",
"jwks_uri":
"http://localhost:5000/oauth/jwks.json",
"registration_endpoint":
"http://localhost:5000/oauth/register",
"scopes_supported":
["openid", "profile", "email", "address",
"phone"],
"response_types_supported":
["code", "token", "id_token", "code token",
"code id_token", "token id_token"],
"acr_values_supported":
["urn:mace:incommon:iap:silver",
"urn:mace:incommon:iap:bronze"],
"subject_types_supported":
["public", "pairwise"],
"userinfo_signing_alg_values_supported":
["RS256", "ES256", "HS256"],
"userinfo_encryption_alg_values_supported":
["RSA1_5", "A128KW"],
"userinfo_encryption_enc_values_supported":
["A128CBC-HS256", "A128GCM"],
"id_token_signing_alg_values_supported":
["RS256", "ES256", "HS256"],
"id_token_encryption_alg_values_supported":
["RSA1_5", "A128KW"],
"id_token_encryption_enc_values_supported":
["A128CBC-HS256", "A128GCM"],
"request_object_signing_alg_values_supported":
["none", "RS256", "ES256"],
"display_values_supported":
["page", "popup"],
"claim_types_supported":
["normal", "distributed"],
"claims_supported":
["sub", "iss", "auth_time", "acr",
"name", "given_name", "family_name", "nickname",
"profile", "picture", "website",
"email", "email_verified", "locale", "zoneinfo",
"http://localhost:5000/claims/groups"],
"claims_parameter_supported":
true,
"service_documentation":
"http://localhost:5000/oauth/service_documentation.html",
"ui_locales_supported":
["en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"]
}

View file

@ -7,18 +7,9 @@ services:
environment:
- LDAP_DOMAIN=mydomain.tld
volumes:
- ./bootstrap.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/50-boostrap.ldif:ro
- ./ldif/bootstrap.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/50-boostrap.ldif:ro
- ../schemas/oauth2-openldap.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/40-oauth2.ldif:ro
command: --copy-service --loglevel debug
ports:
- 5389:389
- 5636:636
oauth:
build:
context: ..
dockerfile: dev/Dockerfile
ports:
- 5000:5000
volumes:
- ../:/app

51
demo/ldap-server.py Normal file
View file

@ -0,0 +1,51 @@
import logging
import slapdtest
from slapdtest._slapdtest import combined_logger
class DevSlapdObject(slapdtest.SlapdObject):
openldap_schema_files = (
"core.ldif",
"cosine.ldif",
"nis.ldif",
"inetorgperson.ldif",
"../schemas/oauth2-openldap.ldif",
"ldif/memberof.ldif"
)
suffix = "dc=mydomain,dc=tld"
root_cn = "admin"
root_pw = "admin"
_log = combined_logger("slapd-demo-server", logging.INFO)
def _avail_tcp_port(self):
return 5389
slapd = DevSlapdObject()
slapd.start()
try:
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"
)
with open("ldif/bootstrap.ldif") as fd:
slapd.ldapadd(fd.read())
slapd.wait()
finally:
slapd.stop()

View file

@ -58,3 +58,45 @@ mail: user@mydomain.tld
telephoneNumber: 555-000-001
userPassword: {SSHA}Yr1ZxSljRsKyaTB30suY2iZ1KRTStF1X
memberof: cn=users,ou=groups,dc=mydomain,dc=tld
dn: ou=oauth,dc=mydomain,dc=tld
objectclass: organizationalUnit
ou: oauth
dn: ou=clients,ou=oauth,dc=mydomain,dc=tld
objectclass: organizationalUnit
ou: clients
dn: oauthClientID=1JGkkzCbeHpGtlqgI5EENByf,ou=clients,ou=oauth,dc=mydomain,dc=tld
objectclass: oauthClient
oauthClientID: 1JGkkzCbeHpGtlqgI5EENByf
oauthClientSecret: 2xYPSReTQRmGG1yppMVZQ0ASXwFejPyirvuPbKhNa6TmKC5x
oauthClientName: Client1
oauthClientContact: admin@mydomain.tld
oauthClientURI: http://localhost:5001
oauthRedirectURIs: http://localhost:5001/authorize
oauthGrantType: authorization_code
oauthGrantType: refresh_token
oauthScope: openid
oauthScope: profile
oauthScope: email
oauthResponseType: code
oauthResponseType: id_token
oauthTokenEndpointAuthMethod: client_secret_basic
dn: oauthClientID=gn4yFN7GDykL7QP8v8gS9YfV,ou=clients,ou=oauth,dc=mydomain,dc=tld
objectclass: oauthClient
oauthClientID: gn4yFN7GDykL7QP8v8gS9YfV
oauthClientSecret: ouFJE5WpICt6hxTyf8icXPeeklMektMY4gV0Rmf3aY60VElA
oauthClientName: Client2
oauthClientContact: admin@mydomain.tld
oauthClientURI: http://localhost:5002
oauthRedirectURIs: http://localhost:5002/authorize
oauthGrantType: authorization_code
oauthGrantType: refresh_token
oauthScope: openid
oauthScope: profile
oauthScope: email
oauthResponseType: code
oauthResponseType: id_token
oauthTokenEndpointAuthMethod: client_secret_basic

16
demo/ldif/memberof.ldif Normal file
View file

@ -0,0 +1,16 @@
dn: cn=module,cn=config
cn: module
objectClass: olcModuleList
olcModuleLoad: memberof
dn: olcOverlay=memberof,olcDatabase={1}mdb,cn=config
objectClass: olcConfig
objectClass: olcMemberOf
objectClass: olcOverlayConfig
objectClass: top
olcOverlay: memberof
olcMemberOfDangling: ignore
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfNames
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf

15
demo/run.sh Executable file
View file

@ -0,0 +1,15 @@
#!/bin/bash
if ! type python > /dev/null 2>&1; then
echo "Cannot start the LDAP server. Please install python on your system."
return -1
fi
if ! test -d env; then
virtualenv env
env/bin/pip install --editable ..
env/bin/pip install honcho requests
env/bin/pip install --upgrade git+https://github.com/python-ldap/python-ldap.git
fi
env/bin/honcho start

12
demo/slapd.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
if type slapd > /dev/null 2>&1; then
env/bin/python ldap-server.py
elif type docker-compose > /dev/null 2>&1; then
docker-compose up
else
echo "Cannot start the LDAP server. Please install openldap or docker on your system."
return -1
fi

View file

@ -1,16 +0,0 @@
FROM python:3-alpine
RUN adduser -D -h /app oauthserver
COPY --chown=oauthserver:oauthserver . /app/
RUN apk add curl libldap libffi su-exec
RUN apk add --virtual .dev-dependencies gcc musl-dev openldap-dev libffi-dev
RUN pip install /app/
WORKDIR /app
USER oauthserver
ENV FLASK_APP=canaille
ENV FLASK_ENV=development
ENV AUTHLIB_INSECURE_TRANSPORT=1
ENTRYPOINT [ "flask", "run", "--host", "0.0.0.0", "--extra-files", "canaille/conf/config.toml" ]

View file

@ -1,5 +0,0 @@
# Devevolpment
Here are tools for developping.
Run `docker-compose up`.