diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a41eb285..1527bc29 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -19,13 +19,13 @@ Development environment .. code-block:: console cd demo - ./run.sh + ./run.sh # or `docker-compose up` to run it with docker 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 +- A canaille server at http://localhost:5000 +- A dummy client at http://localhost:5001 +- Another dummy client at http://localhost:5002 The canaille server has some default users: diff --git a/README.md b/README.md index 6b039c1f..7cfa6ac8 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ It aims to be very light, simple to install and simple to maintain. Its main fea ```bash cd demo -./run.sh +./run.sh # or `docker-compose up` to run it with docker ``` # Documentation diff --git a/demo/Dockerfile-canaille b/demo/Dockerfile-canaille new file mode 100644 index 00000000..05d419aa --- /dev/null +++ b/demo/Dockerfile-canaille @@ -0,0 +1,16 @@ +FROM python:slim + +RUN \ + apt update && \ + apt -y upgrade && \ + apt install -y \ + gcc \ + libsasl2-dev \ + libldap2-dev \ + libssl-dev + +COPY setup.cfg setup.py /opt/canaille/ +RUN pip install --editable /opt/canaille + +WORKDIR /opt/canaille +ENTRYPOINT ["flask", "run", "--host=0.0.0.0"] diff --git a/demo/Dockerfile-client b/demo/Dockerfile-client new file mode 100644 index 00000000..d340d649 --- /dev/null +++ b/demo/Dockerfile-client @@ -0,0 +1,13 @@ +FROM python:slim + +RUN \ + apt update && \ + apt -y upgrade + +RUN pip install \ + flask \ + "authlib<1.0.0" \ + requests + +WORKDIR /opt/client +ENTRYPOINT ["flask", "run", "--host=0.0.0.0"] diff --git a/demo/README.md b/demo/README.md index 61c09ea8..58b5bd5e 100644 --- a/demo/README.md +++ b/demo/README.md @@ -1,8 +1,10 @@ # Demo and development -To check out how canaille looks like, or to start contributions, just run it with `./run.sh`! +To check out how canaille looks like, or to start contributions, just run the demo: +- with `docker-compose up` to install and run it in preconfigured docker containers +- or with `./run.sh` to install it natively in a virtual environment and run it locally! -# Prerequisites +# Prerequisites for native demo installation You need to have `OpenLDAP` somewhere in your system. @@ -10,7 +12,7 @@ You can either: - install it with your distro packages *(for instance `sudo apt install slapd ldap-utils` with Ubuntu)*. it is not required to launch the system ldap service. - have `docker` plus `docker-compose` installed on your system, the `./run.sh` script will download and - run a OpenLDAP image. + run an OpenLDAP image. canaille depends on [python-ldap](https://github.com/python-ldap/python-ldap), and this package needs some headers to be installed on your system to be built. For instance on Ubuntu you can install this: @@ -32,15 +34,15 @@ sudo aa-complain /usr/sbin/slapd 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 +- A canaille server at http://localhost:5000 +- A dummy client at http://localhost:5001 +- Another dummy client at http://localhost: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**. -- A new user which admin and password are **new**. This user has no password yet, - and his first attempt to log-in will result in sending a password initialization - email. +- An admin user which login and password are **admin**. +- A new user which login is **james**. This user has no password yet, + and his first attempt to log-in would result in sending a password initialization + email (if a smtp server is configurated). diff --git a/demo/conf-docker/canaille.toml b/demo/conf-docker/canaille.toml new file mode 100644 index 00000000..a2cd4820 --- /dev/null +++ b/demo/conf-docker/canaille.toml @@ -0,0 +1,180 @@ +# All the Flask configuration values can be used: +# https://flask.palletsprojects.com/en/1.1.x/config/#builtin-configuration-values + +# 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" + +# The name of a theme in the 'theme' directory, or an absolute path +# to a theme. Defaults to 'default'. Theming is done with +# https://github.com/tktech/flask-themer +# THEME = "default" + +# 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" + +# If HIDE_INVALID_LOGINS is set to true, when a user tries to sign in with +# an invalid login, a message is shown saying that the login does not +# exist. If HIDE_INVALID_LOGINS is set to false (the default) a message is +# shown saying that the password is wrong, but does not give a clue +# wether the login exists or not. +# HIDE_INVALID_LOGINS = false + +# The validity duration of registration invitations, in seconds. +# Defaults to 2 days +# INVITATION_EXPIRATION = 172800 + +[LOGGING] +# LEVEL can be one value among: +# DEBUG, INFO, WARNING, ERROR, CRITICAL +# Defaults to WARNING +# LEVEL = "WARNING" +LEVEL = "DEBUG" + +# The path of the log file. If not set (the default) logs are +# written in the standard error output. +# PATH = "" + +[LDAP] +URI = "ldap://ldap:389" +ROOT_DN = "dc=mydomain,dc=tld" +BIND_DN = "cn=admin,dc=mydomain,dc=tld" +BIND_PW = "admin" +TIMEOUT = 10 + +# Where to search for users? +USER_BASE = "ou=users,dc=mydomain,dc=tld" + +# The object class to use for creating new users +# USER_CLASS = "inetOrgPerson" + +# The attribute to identify an object in the User dn. +USER_ID_ATTRIBUTE = "uid" + +# Filter to match users on sign in. Supports a variable +# {login} that can be used to compare against several fields: +# USER_FILTER = "(|(uid={login})(mail={login}))" + +# Where to search for groups? +GROUP_BASE = "ou=groups,dc=mydomain,dc=tld" + +# The object class to use for creating new groups +# GROUP_CLASS = "groupOfNames" + +# The attribute to identify an object in the User dn. +# GROUP_ID_ATTRIBUTE = "cn" + +# The attribute to use to identify a group +# GROUP_NAME_ATTRIBUTE = "cn" + +# A filter to check if a user belongs to a group +# A 'user' variable is available. +# GROUP_USER_FILTER = "member={user.dn}" + +# You can define access controls that define what users can do on canaille +# An access control consists in a FILTER to match users, a list of PERMISSIONS +# matched users will be able to perform, and fields users will be able +# to READ and WRITE. Users matching several filters will cumulate permissions. +# +# A 'FILTER' parameter that is a LDAP filter used to determine if a user +# belongs to an access control. If absent, all the users will match this +# access control. If your LDAP server has the 'memberof' overlay, you can +# filter against group membership. +# Here are some examples +# FILTER = 'uid=admin' +# FILTER = 'memberof=cn=admins,ou=groups,dc=mydomain,dc=tld' +# +# The 'PERMISSIONS' parameter that is an list of items the users in the access +# control will be able to manage. 'PERMISSIONS' is optionnal. Values can be: +# - "use_oidc" to allow OpenID Connect authentication +# - "manage_oidc" to allow OpenID Connect client managements +# - "manage_users" to allow other users management +# - "manage_groups" to allow group edition and creation +# - "delete_account" allows a user to delete his own account. If used with +# manage_users, the user can delete any account +# - "impersonate_users" to allow a user to take the identity of another user +# +# The 'READ' and 'WRITE' attributes are the LDAP attributes of the user +# object that users will be able to read and/or write. +[ACL.DEFAULT] +PERMISSIONS = ["use_oidc"] +READ = ["uid", "groups"] +WRITE = ["jpegPhoto", "givenName", "sn", "userPassword", "telephoneNumber", "mail", "labeledURI"] + +[ACL.ADMIN] +FILTER = "memberof=cn=admins,ou=groups,dc=mydomain,dc=tld" +PERMISSIONS = [ + "manage_users", + "manage_groups", + "manage_oidc", + "delete_account", + "impersonate_users", +] +WRITE = ["groups"] + +[ACL.HALF_ADMIN] +FILTER = "memberof=cn=moderators,ou=groups,dc=mydomain,dc=tld" +PERMISSIONS = ["manage_users", "manage_groups", "delete_account"] +WRITE = ["groups"] + +# The jwt configuration. You can generate a RSA keypair with: +# openssl genrsa -out private.pem 4096 +# openssl rsa -in private.pem -pubout -outform PEM -out public.pem +[JWT] +# The path to the private key. +PRIVATE_KEY = "conf/private.pem" +# The path to the public key. +PUBLIC_KEY = "conf/public.pem" +# The key type parameter +# KTY = "RSA" +# The key algorithm +# ALG = "RS256" +# The time the JWT will be valid, in seconds +# EXP = 3600 + +[JWT.MAPPING] +# Mapping between JWT fields and LDAP attributes from your +# User objectClass. +# {attribute} will be replaced by the user ldap attribute value. +# Default values fits inetOrgPerson. +SUB = "{{ user.uid[0] }}" +NAME = "{{ user.cn[0] }}" +PHONE_NUMBER = "{{ user.telephoneNumber[0] }}" +EMAIL = "{{ user.mail[0] }}" +GIVEN_NAME = "{{ user.givenName[0] }}" +FAMILY_NAME = "{{ user.sn[0] }}" +PREFERRED_USERNAME = "{{ user.displayName[0] }}" +LOCALE = "{{ user.preferredLanguage[0] }}" +ADDRESS = "{{ user.postalAddress[0] }}" +PICTURE = "{% if user.jpegPhoto %}{{ url_for('account.photo', uid=user.uid[0], field='jpegPhoto', _external=True) }}{% endif %}" +WEBSITE = "{{ user.labeledURI[0] }}" + +# The SMTP server options. If not set, mail related features such as +# user invitations, and password reset emails, will be disabled. +[SMTP] +# HOST = "localhost" +# PORT = 25 +# TLS = false +# LOGIN = "" +# PASSWORD = "" +FROM_ADDR = "admin@mydomain.tld" diff --git a/demo/conf-docker/client1.cfg b/demo/conf-docker/client1.cfg new file mode 100644 index 00000000..dd54c5fb --- /dev/null +++ b/demo/conf-docker/client1.cfg @@ -0,0 +1,7 @@ +SECRET_KEY="46bf9fb5-88d5-489b-9312-899588377ff0" +NAME = "Client 1" +SESSION_COOKIE_NAME="client1-session" + +OAUTH_CLIENT_ID="1JGkkzCbeHpGtlqgI5EENByf" +OAUTH_CLIENT_SECRET="2xYPSReTQRmGG1yppMVZQ0ASXwFejPyirvuPbKhNa6TmKC5x" +OAUTH_AUTH_SERVER="http://canaille:5000" diff --git a/demo/conf-docker/client2.cfg b/demo/conf-docker/client2.cfg new file mode 100644 index 00000000..dd700245 --- /dev/null +++ b/demo/conf-docker/client2.cfg @@ -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://canaille:5000" diff --git a/demo/conf-docker/oauth-authorization-server.json b/demo/conf-docker/oauth-authorization-server.json new file mode 100644 index 00000000..0a812209 --- /dev/null +++ b/demo/conf-docker/oauth-authorization-server.json @@ -0,0 +1,31 @@ +{ + "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", + "introspection_endpoint": + "https://mydomain.tld/oauth/introspect", + "scopes_supported": + ["openid", "profile", "email", "address", + "phone", "groups"], + "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"] +} diff --git a/demo/conf-docker/openid-configuration.json b/demo/conf-docker/openid-configuration.json new file mode 100644 index 00000000..5b1d73e6 --- /dev/null +++ b/demo/conf-docker/openid-configuration.json @@ -0,0 +1,66 @@ +{ + "issuer": + "http://localhost:5000", + "authorization_endpoint": + "http://localhost:5000/oauth/authorize", + "token_endpoint": + "http://canaille: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://canaille:5000/oauth/userinfo", + "check_session_iframe": + "http://canaille:5000/oauth/check_session", + "end_session_endpoint": + "http://canaille:5000/oauth/end_session", + "jwks_uri": + "http://canaille:5000/oauth/jwks.json", + "registration_endpoint": + "http://canaille:5000/oauth/register", + "introspection_endpoint": + "https://canaille:5000/oauth/introspect", + "scopes_supported": + ["openid", "profile", "email", "address", + "phone", "groups"], + "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", + "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"] +} diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index 322f998c..2cb962c2 100644 --- a/demo/docker-compose.yml +++ b/demo/docker-compose.yml @@ -7,9 +7,66 @@ services: environment: - LDAP_DOMAIN=mydomain.tld volumes: - - ./ldif/bootstrap.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/50-boostrap.ldif:ro + - ./ldif/memberof.ldif:/container/service/slapd/assets/config/bootstrap/ldif/03-memberOf.ldif:ro + # memberof overlay is already present in openldap docker image but only for groupOfUniqueNames. We need to overwrite it (until canaille can handle groupOfUniqueNames). + # https://github.com/osixia/docker-openldap/blob/master/image/service/slapd/assets/config/bootstrap/ldif/03-memberOf.ldif - ../canaille/ldap_backend/schemas/oauth2-openldap.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/40-oauth2.ldif:ro + - ./ldif/bootstrap-tree.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/50-boostrap-tree.ldif:ro + - ./ldif/bootstrap-data.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/60-boostrap-data.ldif:ro command: --copy-service --loglevel debug ports: - 5389:389 - 5636:636 + + canaille: + depends_on: + - ldap + build: + context: .. + dockerfile: demo/Dockerfile-canaille + environment: + - AUTHLIB_INSECURE_TRANSPORT=1 + - FLASK_ENV=development + - CONFIG=/opt/canaille/conf/canaille.toml + - FLASK_APP=canaille + volumes: + - ../canaille:/opt/canaille/canaille + - ./conf-docker:/opt/canaille/conf + ports: + - 5000:5000 + + client1: + depends_on: + - canaille + build: + context: . + dockerfile: Dockerfile-client + environment: + - FLASK_ENV=development + - CONFIG=/opt/client/conf/client1.cfg + - FLASK_APP=client + volumes: + - ./client:/opt/client/client + - ./conf-docker:/opt/client/conf + - ../canaille/static:/opt/canaille/static + command: --port=5001 + ports: + - 5001:5001 + + client2: + depends_on: + - canaille + build: + context: . + dockerfile: Dockerfile-client + environment: + - FLASK_ENV=development + - CONFIG=/opt/client/conf/client2.cfg + - FLASK_APP=client + volumes: + - ./client:/opt/client/client + - ./conf-docker:/opt/client/conf + - ../canaille/static:/opt/canaille/static + command: --port=5002 + ports: + - 5002:5002 diff --git a/demo/slapd.sh b/demo/slapd.sh index 2a5e8f60..b9cfeccf 100755 --- a/demo/slapd.sh +++ b/demo/slapd.sh @@ -4,7 +4,7 @@ if [ "$SLAPD_BINARY" == "NATIVE" ] || ([ "$SLAPD_BINARY" == "" ] && type slapd > env BIN=$BIN:/usr/bin:/usr/sbin env/bin/python ldap-server.py elif [ "$SLAPD_BINARY" == "DOCKER" ] || ([ "$SLAPD_BINARY" == "" ] && type docker-compose > /dev/null 2>&1); then - docker-compose up + docker-compose run --service-ports --rm ldap else echo "Cannot start the LDAP server. Please install openldap or docker on your system."