forked from Github-Mirrors/canaille
Documentation improvements
This commit is contained in:
parent
04ca88ddcd
commit
18e4b0c42c
13 changed files with 515 additions and 97 deletions
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
240
doc/configuration.rst
Normal 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.
|
|
@ -34,6 +34,7 @@ Table of contents
|
|||
:maxdepth: 2
|
||||
|
||||
install
|
||||
configuration
|
||||
contributing
|
||||
changelog
|
||||
|
||||
|
|
185
doc/install.rst
185
doc/install.rst
|
@ -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
|
||||
==============
|
||||
|
|
|
@ -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}",
|
||||
|
|
Loading…
Reference in a new issue