Documentation improvements

This commit is contained in:
Éloi Rivard 2021-12-03 18:37:25 +01:00 committed by Éloi Rivard
parent 04ca88ddcd
commit 18e4b0c42c
13 changed files with 515 additions and 97 deletions

View file

@ -101,13 +101,13 @@ def setup_ldap_models(app):
if user_base.endswith(app.config["LDAP"]["ROOT_DN"]):
user_base = user_base[: -len(app.config["LDAP"]["ROOT_DN"]) - 1]
User.base = user_base
User.id = app.config["LDAP"].get("USER_ID_ATTRIBUTE", "cn")
User.id = app.config["LDAP"].get("USER_ID_ATTRIBUTE", User.DEFAULT_ID_ATTRIBUTE)
group_base = app.config["LDAP"].get("GROUP_BASE")
if group_base.endswith(app.config["LDAP"]["ROOT_DN"]):
group_base = group_base[: -len(app.config["LDAP"]["ROOT_DN"]) - 1]
Group.base = group_base
Group.id = app.config["LDAP"].get("GROUP_ID_ATTRIBTUE", "cn")
Group.id = app.config["LDAP"].get("GROUP_ID_ATTRIBTUE", Group.DEFAULT_ID_ATTRIBUTE)
def setup_ldap_connection(app):
@ -260,7 +260,7 @@ def create_app(config=None, validate=True):
"has_smtp": "SMTP" in app.config,
"logo_url": app.config.get("LOGO"),
"favicon_url": app.config.get("FAVICON", app.config.get("LOGO")),
"website_name": app.config.get("NAME"),
"website_name": app.config.get("NAME", "Canaille"),
"user": current_user(),
"menu": True,
}

View file

@ -154,7 +154,11 @@ def firstlogin(uid):
@bp.route("/users")
@permissions_needed("manage_users")
def users(user):
users = User.filter(objectClass=current_app.config["LDAP"]["USER_CLASS"])
users = User.filter(
objectClass=current_app.config["LDAP"].get(
"USER_CLASS", User.DEFAULT_OBJECT_CLASS
)
)
return render_template("users.html", users=users, menuitem="users")
@ -290,7 +294,11 @@ def registration(data, hash):
def profile_create(current_app, form):
user = User(objectClass=current_app.config["LDAP"]["USER_CLASS"])
user = User(
objectClass=current_app.config["LDAP"].get(
"USER_CLASS", User.DEFAULT_OBJECT_CLASS
)
)
for attribute in form:
if attribute.name in user.may + user.must:
if isinstance(attribute.data, FileStorage):

View file

@ -6,10 +6,15 @@ import logging
import mimetypes
import smtplib
import urllib.request
from canaille.models import User
from email.utils import make_msgid
from flask import current_app, request
from flask_babel import gettext as _
DEFAULT_SMTP_HOST = "localhost"
DEFAULT_SMTP_PORT = 25
DEFAULT_SMTP_TLS = False
def obj_to_b64(obj):
return base64.b64encode(json.dumps(obj).encode("utf-8")).decode("utf-8")
@ -27,7 +32,7 @@ def profile_hash(*args):
def login_placeholder():
user_filter = current_app.config["LDAP"]["USER_FILTER"]
user_filter = current_app.config["LDAP"].get("USER_FILTER", User.DEFAULT_FILTER)
placeholders = []
if "cn={login}" in user_filter:
@ -91,9 +96,10 @@ def send_email(subject, recipient, text, html, sender=None, attachements=None):
if sender:
msg["From"] = sender
elif current_app.config.get("NAME"):
elif current_app.config.get("NAME", "Canaille"):
msg["From"] = '"{}" <{}>'.format(
current_app.config.get("NAME"), current_app.config["SMTP"]["FROM_ADDR"]
current_app.config.get("NAME", "Canaille"),
current_app.config["SMTP"]["FROM_ADDR"],
)
else:
msg["From"] = current_app.config["SMTP"]["FROM_ADDR"]
@ -106,14 +112,14 @@ def send_email(subject, recipient, text, html, sender=None, attachements=None):
)
try:
with smtplib.SMTP(
host=current_app.config["SMTP"]["HOST"],
port=current_app.config["SMTP"]["PORT"],
host=current_app.config["SMTP"].get("HOST", DEFAULT_SMTP_HOST),
port=current_app.config["SMTP"].get("PORT", DEFAULT_SMTP_PORT),
) as smtp:
if current_app.config["SMTP"].get("TLS"):
if current_app.config["SMTP"].get("TLS", DEFAULT_SMTP_TLS):
smtp.starttls()
if current_app.config["SMTP"].get("LOGIN"):
smtp.login(
user=current_app.config["SMTP"]["LOGIN"],
user=current_app.config["SMTP"].get("LOGIN"),
password=current_app.config["SMTP"].get("PASSWORD"),
)
smtp.send_message(msg)

View file

@ -5,7 +5,7 @@
SECRET_KEY = "change me before you go in production"
# Your organization name.
NAME = "Canaille"
# NAME = "Canaille"
# The interface on which canaille will be served
# SERVER_NAME = "auth.mydomain.tld"
@ -33,16 +33,17 @@ OIDC_METADATA_FILE = "canaille/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 this option is set to true, when a user tries to sign in with
# an invalid login, a message is shown indicating that the login does not
# exist. If this option is set to false (the default) a message is
# shown indicating that the password is wrong, but does not give a clue
# 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
[LOGGING]
# LEVEL can be one value among:
# DEBUG, INFO, WARNING, ERROR, CRITICAL
# Defaults to WARNING
# LEVEL = "WARNING"
# The path of the log file. If not set (the default) logs are
@ -54,36 +55,36 @@ URI = "ldap://ldap"
ROOT_DN = "dc=mydomain,dc=tld"
BIND_DN = "cn=admin,dc=mydomain,dc=tld"
BIND_PW = "admin"
TIMEOUT =
# TIMEOUT =
# 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"
# USER_CLASS = "inetOrgPerson"
# The attribute to identify an object in the User dn.
USER_ID_ATTRIBUTE = "cn"
# USER_ID_ATTRIBUTE = "cn"
# 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}))"
# USER_FILTER = "(|(uid={login})(mail={login}))"
# Where to search for groups?
GROUP_BASE = "ou=groups"
GROUP_BASE = "ou=groups,dc=mydomain,dc=tld"
# The object class to use for creating new groups
GROUP_CLASS = "groupOfNames"
# GROUP_CLASS = "groupOfNames"
# The attribute to identify an object in the User dn.
GROUP_ID_ATTRIBUTE = "cn"
# GROUP_ID_ATTRIBUTE = "cn"
# The attribute to use to identify a group
GROUP_NAME_ATTRIBUTE = "cn"
# 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}"
# 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
@ -130,11 +131,16 @@ WRITE = ["groups"]
# 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 = "canaille/conf/private.pem"
# The path to the public key.
PUBLIC_KEY = "canaille/conf/public.pem"
KTY = "RSA"
ALG = "RS256"
EXP = 3600
# 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
@ -153,10 +159,10 @@ ADDRESS = "{postalAddress}"
# The SMTP server options. If not set, mail related features such as
# user invitations, and password reset emails, will be disabled.
# [SMTP]
[SMTP]
# HOST = "localhost"
# PORT = 25
# TLS = false
# LOGIN = ""
# PASSWORD = ""
# FROM_ADDR = "admin@mydomain.tld"
FROM_ADDR = "admin@mydomain.tld"

View file

@ -20,7 +20,11 @@ bp = Blueprint("groups", __name__)
@bp.route("/")
@permissions_needed("manage_groups")
def groups(user):
groups = Group.filter(objectClass=current_app.config["LDAP"]["GROUP_CLASS"])
groups = Group.filter(
objectClass=current_app.config["LDAP"].get(
"GROUP_CLASS", Group.DEFAULT_OBJECT_CLASS
)
)
return render_template("groups.html", groups=groups, menuitem="groups")
@ -40,7 +44,11 @@ def create_group(user):
if not form.validate():
flash(_("Group creation failed."), "error")
else:
group = Group(objectClass=current_app.config["LDAP"]["GROUP_CLASS"])
group = Group(
objectClass=current_app.config["LDAP"].get(
"GROUP_CLASS", Group.DEFAULT_OBJECT_CLASS
)
)
group.member = [user.dn]
group.cn = [form.name.data]
group.description = [form.description.data]

View file

@ -13,6 +13,10 @@ from .ldaputils import LDAPObject
class User(LDAPObject):
DEFAULT_OBJECT_CLASS = "inetOrgPerson"
DEFAULT_FILTER = "(|(uid={login})(mail={login}))"
DEFAULT_ID_ATTRIBUTE = "cn"
def __init__(self, *args, **kwargs):
self.read = set()
self.write = set()
@ -27,7 +31,7 @@ class User(LDAPObject):
if login:
filter = (
current_app.config["LDAP"]
.get("USER_FILTER")
.get("USER_FILTER", User.DEFAULT_FILTER)
.format(login=ldap.filter.escape_filter_chars(login))
)
@ -39,8 +43,10 @@ class User(LDAPObject):
def load_groups(self, conn=None):
try:
group_filter = current_app.config["LDAP"]["GROUP_USER_FILTER"].format(
user=self
group_filter = (
current_app.config["LDAP"]
.get("GROUP_USER_FILTER", Group.DEFAULT_USER_FILTER)
.format(user=self)
)
escaped_group_filter = ldap.filter.escape_filter_chars(group_filter)
self._groups = Group.filter(filter=escaped_group_filter, conn=conn)
@ -180,12 +186,21 @@ class User(LDAPObject):
class Group(LDAPObject):
DEFAULT_OBJECT_CLASS = "groupOfNames"
DEFAULT_ID_ATTRIBUTE = "cn"
DEFAULT_NAME_ATTRIBUTE = "cn"
DEFAULT_USER_FILTER = "member={user.dn}"
@classmethod
def available_groups(cls, conn=None):
conn = conn or cls.ldap()
try:
attribute = current_app.config["LDAP"]["GROUP_NAME_ATTRIBUTE"]
object_class = current_app.config["LDAP"]["GROUP_CLASS"]
attribute = current_app.config["LDAP"].get(
"GROUP_NAME_ATTRIBUTE", Group.DEFAULT_NAME_ATTRIBUTE
)
object_class = current_app.config["LDAP"].get(
"GROUP_CLASS", Group.DEFAULT_OBJECT_CLASS
)
except KeyError:
return []
@ -195,7 +210,9 @@ class Group(LDAPObject):
@property
def name(self):
attribute = current_app.config["LDAP"].get("GROUP_NAME_ATTRIBUTE")
attribute = current_app.config["LDAP"].get(
"GROUP_NAME_ATTRIBUTE", Group.DEFAULT_NAME_ATTRIBUTE
)
return self[attribute][0]
def get_members(self, conn=None):

View file

@ -21,6 +21,8 @@ from .oauth2utils import (
RevocationEndpoint,
generate_user_info,
require_oauth,
DEFAULT_JWT_ALG,
DEFAULT_JWT_KTY,
)
from .forms import FullLoginForm
from .flaskutils import current_user
@ -184,14 +186,14 @@ def jwks():
with open(current_app.config["JWT"]["PUBLIC_KEY"]) as fd:
pubkey = fd.read()
obj = jwk.dumps(pubkey, current_app.config["JWT"]["KTY"])
obj = jwk.dumps(pubkey, current_app.config["JWT"].get("KTY", DEFAULT_JWT_KTY))
return jsonify(
{
"keys": [
{
"kid": None,
"use": "sig",
"alg": current_app.config["JWT"]["ALG"],
"alg": current_app.config["JWT"].get("ALG", DEFAULT_JWT_ALG),
**obj,
}
]

View file

@ -19,7 +19,11 @@ from authlib.oidc.core.grants import (
from authlib.oidc.core import UserInfo
from collections import defaultdict
from flask import current_app
from .models import Client, AuthorizationCode, Token, User
from .models import Client, AuthorizationCode, Group, Token, User
DEFAULT_JWT_KTY = "RSA"
DEFAULT_JWT_ALG = "RS256"
DEFAULT_JWT_EXP = 3600
def exists_nonce(nonce, req):
@ -28,12 +32,13 @@ def exists_nonce(nonce, req):
def get_jwt_config(grant):
with open(current_app.config["JWT"]["PRIVATE_KEY"]) as pk:
return {
"key": pk.read(),
"alg": current_app.config["JWT"]["ALG"],
"alg": current_app.config["JWT"].get("ALG", DEFAULT_JWT_ALG),
"iss": authorization.metadata["issuer"],
"exp": current_app.config["JWT"]["EXP"],
"exp": current_app.config["JWT"].get("EXP", DEFAULT_JWT_EXP),
}
@ -88,7 +93,9 @@ def generate_user_claims(user, claims, jwt_mapping_config=None):
# it's better to not insert a null or empty string value
data[claim] = formatted_claim
if claim == "groups":
group_name_attr = current_app.config["LDAP"]["GROUP_NAME_ATTRIBUTE"]
group_name_attr = current_app.config["LDAP"].get(
"GROUP_NAME_ATTRIBUTE", Group.DEFAULT_NAME_ATTRIBUTE
)
data[claim] = [getattr(g, group_name_attr)[0] for g in user.groups]
return data

View file

@ -62,30 +62,30 @@ TIMEOUT = 10
USER_BASE = "ou=users,dc=mydomain,dc=tld"
# The object class to use for creating new users
USER_CLASS = "inetOrgPerson"
# 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}))"
# USER_FILTER = "(|(uid={login})(mail={login}))"
# Where to search for groups?
GROUP_BASE = "ou=groups"
GROUP_BASE = "ou=groups,dc=mydomain,dc=tld"
# The object class to use for creating new groups
GROUP_CLASS = "groupOfNames"
# GROUP_CLASS = "groupOfNames"
# The attribute to identify an object in the User dn.
GROUP_ID_ATTRIBUTE = "cn"
# GROUP_ID_ATTRIBUTE = "cn"
# The attribute to use to identify a group
GROUP_NAME_ATTRIBUTE = "cn"
# 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}"
# 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
@ -137,11 +137,16 @@ WRITE = ["groups"]
# 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"
KTY = "RSA"
ALG = "RS256"
EXP = 3600
# 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
@ -161,9 +166,9 @@ ADDRESS = "{postalAddress}"
# 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 = ""
# HOST = "localhost"
# PORT = 25
# TLS = false
# LOGIN = ""
# PASSWORD = ""
FROM_ADDR = "admin@mydomain.tld"

240
doc/configuration.rst Normal file
View file

@ -0,0 +1,240 @@
Configuration
#############
Here are the different options you can have in your configuration file.
.. contents::
:local:
Sections
========
Miscellaneous
-------------
Canaille is based on Flask, so any `flask configuration <https://flask.palletsprojects.com/en/1.1.x/config/#builtin-configuration-values>`_ option will be usable with canaille:
:SECRET_KEY:
**Required.** The Flask secret key. You should set a random string here.
:NAME:
*Optional.* The name of your organization. If not set `Canaille` will be used.
:LOGO:
*Optional.* The URL ot the logo of your organization. The default is the canaille logo.
:FAVICON:
*Optional.* An URL to a favicon. The default is the value of ``LOGO``.
:THEME:
*Optional.* The name or the path to a canaille theme.
If the value is just a name, the theme should be in a directory with that namein the *themes* directory.
:LANGUAGE:
*Optional.* The locale code of the language to use. If not set, the language of the browser will be used.
:OAUTH2_METADATA_FILE:
*Optional.* The path to the OAUTH2 metadata file.
If not set the file will be looked for in the same directory as the configuration file.
:OIDC_METADATA_FILE:
*Optional.* The path to the OpenID Connect metadata file.
If not set the file will be looked for in the same directory as the configuration file.
:SENTRY_DSN:
*Optional.* A DSN to a sentry instance.
This needs the ``sentry_sdk`` python package to be installed.
This is useful if you want to collect the canaille exceptions in a production environment.
:HIDE_INVALID_LOGINS:
*Optional.* Wether to tell the users if a username exists during failing login attempts.
Defaults to ``True``. This may be a security issue to disable this, as this give a way to malicious people to guess who has an account on this canaille instance.
LOGGING
-------
:LEVEL:
*Optional.* The logging level. Must be an either *DEBUG*, *INFO*, *WARNING*, *ERROR* or *CRITICAL*. Defults to *WARNING*.
:PATH:
*Optional.* The log file path. If not set, logs are written in the standard error output.
LDAP
----
:URI:
**Required.** The URI to the LDAP server.
e.g. ``ldaps://ldad.mydomain.tld``
:ROOT_DN:
**Required.** The root DN of your LDAP server.
e.g. ``dc=mydomain,dc=tld``
:BIND_DN:
**Required.** The LDAP DN to bind with.
e.g. ``cn=admin,dc=mydomain,dc=tld``
:BIND_PW:
**Required.** The LDAP user associated with ``BIND_DN``.
:TIMEOUT:
*Optional.* The time to wait for the LDAP server to respond before considering it is not functional.
:USER_BASE:
**Required.** The DN of the node in which users will be searched for, and created.
e.g. ``ou=users,dc=mydomain,dc=tld``
:USER_CLASS:
*Optional.* The LDAP object class to filter existing users, and create new users.
Defaults to ``inetOrgPerson``.
:USER_ID_ATTRIBUTE:
*Optional.* The attribute to identify an object in the User DN.
For example, if it has the value ``uid``, users DN will be in the form ``uid=foobar,ou=users,dc=mydomain,dc=tld``.
Defaults to ``cn``.
:USER_FILTER:
*Optional.* The filter to match users on sign in.
Supports a variable {login} that can be used to compare against several LDAP attributes.
Defaults to ``(|(uid={login})(mail={login}))``
:GROUP_BASE:
**Required.** The DN where of the node in which LDAP groups will be created and searched for.
e.g. ``ou=groups,dc=mydomain,dc=tld``
:GROUP_CLASS:
*Optional.* The LDAP object class to filter existing groups, and create new groups.
Defaults to ``groupOfNames``
:GROUP_ID_ATTRIBUTE:
*Optional.* The attribute to identify an object in a group DN.
For example, if it has the value ``cn``, groups DN will be in the form ``cn=foobar,ou=users,dc=mydomain,dc=tld``.
Defaults to ``cn``
:GROUP_NAME_ATTRIBUTE:
*Optional.* The attribute to identify a group in the web interface.
Defaults to ``cn``
:GROUP_USER_FILTER:
*Optional.* A filter to check if a user belongs to a group. A 'user' variable is available.
Defaults to ``member={user.dn}``
ACL
---
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`` that users will be able to perform, and fields users will be able
to ``READ`` and ``WRITE``. Users matching several filters will cumulate permissions.
The 'READ' and 'WRITE' attributes are the LDAP attributes of the user
object that users will be able to read and/or write.
:FILTER:
*Optional.* A filter to test on the users to test if they belong to this ACL.
If absent, all the users will have the permissions in this ACL.
e.g. ``uid=admin`` or ``memberof=cn=admin,ou=groups,dc=mydomain,dc=tld``
:PERMISSIONS:
*Optional.* A list of items the users in the access control will be able to manage. 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
:READ:
*Optional.* A list of attributes of ``USER_CLASS`` the user will be able to see, but not edit.
If the user has the ``manage_users`` permission, he will be able to see this fields on other users profile.
If the list containts the special ``groups`` field, the user will be able to see the groups he belongs to.
:WRITE:
*Optional.* A list of attributes of ``USER_CLASS`` the user will be able to edit.
If the user has the ``manage_users`` permission, he will be able to edit this fields on other users profile.
If the list containts the special ``groups`` field, the user will be able to edit the groups he belongs to.
JWT
---
Canaille needs a key pair to sign the JWT. The installation command will generate a key pair for you, but you can also do it manually.
:PRIVATE_KEY:
**Required.** The path to the private key.
e.g. ``/path/to/canaille/conf/private.pem``
:PUBLIC_KEY:
**Required.** The path to the public key.
e.g. ``/path/to/canaille/conf/private.pem``
:KTY:
*Optional.* The key type parameter.
Defaults to ``RSA``.
:ALG:
*Optional.* The key algorithm.
Defaults to ``RS256``.
:EXP:
*Optional.* The time the JWT will be valid, in seconds.
Defaults to ``3600``
JWT.MAPPINGS
------------
A mapping where keys are JWT claims, and values are LDAP user object attributes.
:SUB:
*Optional.* Defaults to ``{uid}``
:NAME:
*Optional.* Defaults to ``{cn}``
:PHONE_NUMBER:
*Optional.* Defaults to ``{telephoneNumber}``
:EMAIL:
*Optional.* Defaults to ``{mail}``
:GIVEN_NAME:
*Optional.* Defaults to ``{givenName}``
:FAMILY_NAME:
*Optional.* Defaults to ``{sn}``
:PREFERRED_USERNAME:
*Optional.* Defaults to ``{displayName}``
:LOCALE:
*Optional.* Defaults to ``{preferredLanguage}``
:ADDRESS:
*Optional.* Defaults to ``{postalAddress}``
SMTP
----
Canaille needs you to configure a SMTP server to send some mails, including the *I forgot my password* and the *invitation* mails.
Without this section Canaille will still be usable, but all the features related to mail will be disabled.
:HOST:
The SMTP server to connect to.
Defaults to ``localhost``
:PORT:
The port to use with the SMTP connection.
Defaults to ``25``
:TLS:
Whether the SMTP connection use TLS.
Default to ``False``
:LOGIN:
The SMTP server authentication login.
*Optional.*
:PASSWORD:
The SMTP server authentication password.
*Optional.*
:FROM_ADDR:
*Required.* The mail address to use as the sender for Canaille emails.

View file

@ -34,6 +34,7 @@ Table of contents
:maxdepth: 2
install
configuration
contributing
changelog

View file

@ -1,22 +1,68 @@
Installation
############
⚠ Canaille is under heavy development and may not fit a production environment yet. ⚠
.. warning ::
First you need to install the schemas into your LDAP server. There are several ways to achieve this:
Canaille is under heavy development and may not fit a production environment yet.
The installation of canaille consist in several steps, some of which you can do manually or with command line tool:
.. contents::
:local:
Get the code
============
As the moment there is no distributon package for canaille so it can be installed with the ``pip`` package manager.
Choose a path to store your configuration, for instance ``/etc/canaille`` and then copy the sample configuration there.
.. code-block:: console
sudo mkdir /etc/canaille
sudo virtualenv /etc/canaille
sudo /etc/canaille/bin/pip install canaille
Configuration
=============
Choose a path where to store your configuration file.
By default canaille will look for ``/etc/canaille/config.toml`` by you can pass any configuration path with the ``CONFIG`` environment variable.
.. code-block:: console
sudo cp canaille/conf/config.sample.toml /etc/canaille/config.toml
sudo cp canaille/conf/openid-configuration.sample.json /etc/canaille/openid-configuration.json
You should then edit your configuration file to adapt the values to your needs.
Automatic installation
======================
A few steps of the installation process can be automatized.
If you want to install the LDAP schemas or generate the keypair yourself, then you can jump to the manual installation section.
.. code-block:: console
env CONFIG=$CANAILLE_CONF_DIR/config.toml /opt/canaille/bin/canaille install
Manual installation
===================
LDAP schemas
============
------------
As of OpenLDAP 2.4, two configuration methods are available:
- The `deprecated <https://www.openldap.org/doc/admin24/slapdconf2.html>`_ one, based on a configuration file (generally `/etc/ldap/slapd.conf`);
- The new one, based on a configuration directory (generally `/etc/ldap/slapd.d`).
- The `deprecated <https://www.openldap.org/doc/admin24/slapdconf2.html>`_ one, based on a configuration file (generally ``/etc/ldap/slapd.conf``);
- The new one, based on a configuration directory (generally ``/etc/ldap/slapd.d``).
Depending on the configuration method you use with your OpenLDAP installation, you need to chose how to add the canaille schemas:
Old fashion: Copy the schemas in your filesystem
------------------------------------------------
````````````````````````````````````````````````
.. code-block:: console
@ -25,9 +71,9 @@ Old fashion: Copy the schemas in your filesystem
sudo service slapd restart
New fashion: Use slapadd to add the schemas
-------------------------------------------
```````````````````````````````````````````
Be careful to stop your ldap server before running `slapadd`
Be careful to stop your ldap server before running ``slapadd``
.. code-block:: console
@ -35,49 +81,128 @@ Be careful to stop your ldap server before running `slapadd`
sudo -u openldap slapadd -n0 -l schemas/*.ldif
sudo service slapd start
Canaille installation
=====================
Generate the key pair
---------------------
Choose a path to store the canaille sources, for instance `/opt/canaille`. The install canaille there in a virtualenv.
You must generate a keypair that canaille will use to sign tokens.
You can customize those commands, as long as they match the ``JWT`` section of your configuration file.
.. code-block:: console
sudo mkdir /etc/canaille
sudo virtualenv /etc/canaille
sudo /etc/canaille/bin/pip install canaille
sudo openssl genrsa -out "$CANAILLE_CONF_DIR/private.pem" 4096
sudo openssl rsa -in "$CANAILLE_CONF_DIR/private.pem" -pubout -outform PEM -out "$CANAILLE_CONF_DIR/public.pem"
Configuration check
-------------------
Configuration
=============
Choose a path to store your configuration, for instance `/etc/canaille` and then copy the sample configuration there. You should also generate a keypair that canaille will use to sign tokens.
After a manual installation, you can check your configuration file with the following command:
.. code-block:: console
sudo mkdir /etc/canaille
env CONFIG=$CANAILLE_CONF_DIR/config.toml /opt/canaille/bin/canaille check
sudo openssl genrsa -out private.pem 4096
sudo openssl rsa -in private.pem -pubout -outform PEM -out public.pem
Application service
===================
sudo cp canaille/conf/config.sample.toml /etc/canaille/config.toml
sudo cp canaille/conf/openid-configuration.sample.json /etc/canaille/openid-configuration.json
Finally you have to run canaille in a WSGI application server.
Here are some WSGI server configuration examples you can pick:
Then check your configuration file with the following command:
uwsgi
-----
.. code-block:: console
env CONFIG=/etc/canaille/config.toml /opt/canaille/bin/canaille check
[uwsgi]
hook-post-fork=chdir:/opt/canaille/src
virtualenv=/opt/canaille/env
socket=/opt/canaille/conf/uwsgi.sock
plugin=python3
module=canaille:create_app()
lazy-apps=true
master=true
processes=1
threads=10
need-app=true
thunder-lock=true
touch-chain-reload=/opt/canaille/conf/uwsgi-reload.fifo
enable-threads=true
reload-on-rss=1024
worker-reload-mercy=600
buffer-size=65535
disable-write-exception = true
env = CONFIG=/opt/canaille/conf/config.toml
Webserver
=========
Web interface
=============
Now you have to plug your WSGI application server to your webserver so it is accessible on the internet.
Here are some webserver configuration examples you can pick:
Finally you have to run the website in a WSGI server:
Nginx
-----
.. code-block:: console
sudo /opt/canaille/bin/pip install gunicorn
gunicorn "canaille:create_app()"
server {
listen 80;
listen [::]:80;
server_name auth.mydomain.tld;
return 301 https://$server_name$request_uri;
}
server {
server_name auth.mydomain.tld;
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_certificate /etc/letsencrypt/live/moncompte.nubla.fr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/moncompte.nubla.fr/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_stapling on;
ssl_stapling_verify on;
index index.html index.php;
charset utf-8;
client_max_body_size 10M;
access_log /opt/canaille/logs/nginx.access.log;
error_log /opt/canaille/logs/nginx.error.log;
gzip on;
gzip_vary on;
gzip_comp_level 4;
gzip_min_length 256;
gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "same-origin" always;
location /static {
root /opt/canaille/src/canaille;
location ~* ^.+\.(?:css|cur|js|jpe?g|gif|htc|ico|png|html|xml|otf|ttf|eot|woff|woff2|svg)$ {
access_log off;
expires 30d;
add_header Cache-Control public;
}
}
location / {
include uwsgi_params;
uwsgi_pass unix:/opt/canaille/config/uwsgi.sock;
}
}
Recurrent jobs
==============

View file

@ -137,11 +137,7 @@ def configuration(slapd_server, smtpd, keypair_path):
"BIND_PW": slapd_server.root_pw,
"USER_BASE": "ou=users",
"USER_FILTER": "(|(uid={login})(cn={login}))",
"USER_CLASS": "inetOrgPerson",
"GROUP_BASE": "ou=groups",
"GROUP_CLASS": "groupOfNames",
"GROUP_NAME_ATTRIBUTE": "cn",
"GROUP_USER_FILTER": "member={user.dn}",
"TIMEOUT": 0.1,
},
"ACL": {
@ -182,9 +178,6 @@ def configuration(slapd_server, smtpd, keypair_path):
"JWT": {
"PUBLIC_KEY": public_key_path,
"PRIVATE_KEY": private_key_path,
"ALG": "RS256",
"KTY": "RSA",
"EXP": 3600,
"MAPPING": {
"SUB": "{uid}",
"NAME": "{cn}",