forked from Github-Mirrors/canaille
Permissions overhaul
This commit is contained in:
parent
6dc401e170
commit
d2611abadb
19 changed files with 348 additions and 196 deletions
|
@ -23,7 +23,7 @@ from .forms import (
|
||||||
ForgottenPasswordForm,
|
ForgottenPasswordForm,
|
||||||
profile_form,
|
profile_form,
|
||||||
)
|
)
|
||||||
from .flaskutils import current_user, user_needed, moderator_needed, admin_needed
|
from .flaskutils import current_user, user_needed, permissions_needed
|
||||||
from .mails import (
|
from .mails import (
|
||||||
send_password_initialization_mail,
|
send_password_initialization_mail,
|
||||||
send_invitation_mail,
|
send_invitation_mail,
|
||||||
|
@ -140,14 +140,14 @@ def firstlogin(uid):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/users")
|
@bp.route("/users")
|
||||||
@moderator_needed()
|
@permissions_needed("manage_users")
|
||||||
def users(user):
|
def users(user):
|
||||||
users = User.filter(objectClass=current_app.config["LDAP"]["USER_CLASS"])
|
users = User.filter(objectClass=current_app.config["LDAP"]["USER_CLASS"])
|
||||||
return render_template("users.html", users=users, menuitem="users")
|
return render_template("users.html", users=users, menuitem="users")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/invite", methods=["GET", "POST"])
|
@bp.route("/invite", methods=["GET", "POST"])
|
||||||
@moderator_needed()
|
@permissions_needed("manage_users")
|
||||||
def user_invitation(user):
|
def user_invitation(user):
|
||||||
form = InvitationForm(request.form or None)
|
form = InvitationForm(request.form or None)
|
||||||
|
|
||||||
|
@ -175,15 +175,10 @@ def user_invitation(user):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/profile", methods=("GET", "POST"))
|
@bp.route("/profile", methods=("GET", "POST"))
|
||||||
@moderator_needed()
|
@permissions_needed("manage_users")
|
||||||
def profile_creation(user):
|
def profile_creation(user):
|
||||||
form = profile_form(current_app.config["LDAP"]["FIELDS"])
|
form = profile_form(user.write, user.read)
|
||||||
form.process(CombinedMultiDict((request.files, request.form)) or None)
|
form.process(CombinedMultiDict((request.files, request.form)) or None)
|
||||||
try:
|
|
||||||
if "uid" in form:
|
|
||||||
del form["uid"].render_kw["readonly"]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if request.form:
|
if request.form:
|
||||||
if not form.validate():
|
if not form.validate():
|
||||||
|
@ -240,13 +235,11 @@ def registration(data, hash):
|
||||||
"groups": data[2],
|
"groups": data[2],
|
||||||
}
|
}
|
||||||
|
|
||||||
form = profile_form(current_app.config["LDAP"]["FIELDS"])
|
readable_fields = set(current_app.config["ACL"]["DEFAULT"]["READ"])
|
||||||
|
writable_fields = set(current_app.config["ACL"]["DEFAULT"]["WRITE"])
|
||||||
|
|
||||||
|
form = profile_form(writable_fields, readable_fields)
|
||||||
form.process(CombinedMultiDict((request.files, request.form)) or None, data=data)
|
form.process(CombinedMultiDict((request.files, request.form)) or None, data=data)
|
||||||
try:
|
|
||||||
if "uid" in form:
|
|
||||||
del form["uid"].render_kw["readonly"]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
form["password1"].validators = [
|
form["password1"].validators = [
|
||||||
wtforms.validators.DataRequired(),
|
wtforms.validators.DataRequired(),
|
||||||
|
@ -305,7 +298,7 @@ def profile_create(current_app, form):
|
||||||
@bp.route("/profile/<username>", methods=("GET", "POST"))
|
@bp.route("/profile/<username>", methods=("GET", "POST"))
|
||||||
@user_needed()
|
@user_needed()
|
||||||
def profile_edition(user, username):
|
def profile_edition(user, username):
|
||||||
user.moderator or username == user.uid[0] or abort(403)
|
user.can_manage_users or username == user.uid[0] or abort(403)
|
||||||
|
|
||||||
if request.method == "GET" or request.form.get("action") == "edit":
|
if request.method == "GET" or request.form.get("action") == "edit":
|
||||||
return profile_edit(user, username)
|
return profile_edit(user, username)
|
||||||
|
@ -346,7 +339,7 @@ def profile_edition(user, username):
|
||||||
|
|
||||||
def profile_edit(editor, username):
|
def profile_edit(editor, username):
|
||||||
menuitem = "profile" if username == editor.uid[0] else "users"
|
menuitem = "profile" if username == editor.uid[0] else "users"
|
||||||
fields = current_app.config["LDAP"]["FIELDS"]
|
fields = editor.read | editor.write
|
||||||
if username != editor.uid[0]:
|
if username != editor.uid[0]:
|
||||||
user = User.get(username) or abort(404)
|
user = User.get(username) or abort(404)
|
||||||
else:
|
else:
|
||||||
|
@ -363,11 +356,8 @@ def profile_edit(editor, username):
|
||||||
if "groups" in fields:
|
if "groups" in fields:
|
||||||
data["groups"] = [g.dn for g in user.groups]
|
data["groups"] = [g.dn for g in user.groups]
|
||||||
|
|
||||||
form = profile_form(fields)
|
form = profile_form(editor.write, editor.read)
|
||||||
form.process(CombinedMultiDict((request.files, request.form)) or None, data=data)
|
form.process(CombinedMultiDict((request.files, request.form)) or None, data=data)
|
||||||
form["uid"].render_kw["readonly"] = "true"
|
|
||||||
if "groups" in form and not editor.admin and not editor.moderator:
|
|
||||||
form["groups"].render_kw["disabled"] = "true"
|
|
||||||
|
|
||||||
if request.form:
|
if request.form:
|
||||||
if not form.validate():
|
if not form.validate():
|
||||||
|
@ -377,7 +367,7 @@ def profile_edit(editor, username):
|
||||||
for attribute in form:
|
for attribute in form:
|
||||||
if (
|
if (
|
||||||
attribute.name in user.may + user.must
|
attribute.name in user.may + user.must
|
||||||
and not attribute.name == "uid"
|
and attribute.name in editor.write
|
||||||
):
|
):
|
||||||
if isinstance(attribute.data, FileStorage):
|
if isinstance(attribute.data, FileStorage):
|
||||||
data = attribute.data.stream.read()
|
data = attribute.data.stream.read()
|
||||||
|
@ -388,13 +378,16 @@ def profile_edit(editor, username):
|
||||||
user[attribute.name] = data
|
user[attribute.name] = data
|
||||||
else:
|
else:
|
||||||
user[attribute.name] = [data]
|
user[attribute.name] = [data]
|
||||||
elif attribute.name == "groups" and (editor.admin or editor.moderator):
|
elif attribute.name == "groups" and "groups" in editor.write:
|
||||||
user.set_groups(attribute.data)
|
user.set_groups(attribute.data)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not form["password1"].data or user.set_password(form["password1"].data)
|
"password1" not in request.form
|
||||||
|
or not form["password1"].data
|
||||||
|
or user.set_password(form["password1"].data)
|
||||||
) and request.form["action"] == "edit":
|
) and request.form["action"] == "edit":
|
||||||
flash(_("Profile updated successfuly."), "success")
|
flash(_("Profile updated successfuly."), "success")
|
||||||
|
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
|
@ -402,7 +395,7 @@ def profile_edit(editor, username):
|
||||||
form=form,
|
form=form,
|
||||||
menuitem=menuitem,
|
menuitem=menuitem,
|
||||||
edited_user=user,
|
edited_user=user,
|
||||||
self_deletion=current_app.config.get("SELF_DELETION", True),
|
self_deletion=user.can_delete_account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -422,7 +415,7 @@ def profile_delete(user, username):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/impersonate/<username>")
|
@bp.route("/impersonate/<username>")
|
||||||
@admin_needed()
|
@permissions_needed("impersonate_users")
|
||||||
def impersonate(user, username):
|
def impersonate(user, username):
|
||||||
u = User.get(username) or abort(404)
|
u = User.get(username) or abort(404)
|
||||||
u.login()
|
u.login()
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask_themer import render_template
|
from flask_themer import render_template
|
||||||
from canaille.models import AuthorizationCode
|
from canaille.models import AuthorizationCode
|
||||||
from canaille.flaskutils import admin_needed
|
from canaille.flaskutils import permissions_needed
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("admin_authorizations", __name__)
|
bp = Blueprint("admin_authorizations", __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def index(user):
|
def index(user):
|
||||||
authorizations = AuthorizationCode.filter()
|
authorizations = AuthorizationCode.filter()
|
||||||
return render_template(
|
return render_template(
|
||||||
|
@ -17,7 +17,7 @@ def index(user):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<authorization_id>", methods=["GET", "POST"])
|
@bp.route("/<authorization_id>", methods=["GET", "POST"])
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def view(user, authorization_id):
|
def view(user, authorization_id):
|
||||||
authorization = AuthorizationCode.get(authorization_id)
|
authorization = AuthorizationCode.get(authorization_id)
|
||||||
return render_template(
|
return render_template(
|
||||||
|
|
|
@ -6,14 +6,14 @@ from flask_wtf import FlaskForm
|
||||||
from flask_babel import lazy_gettext as _
|
from flask_babel import lazy_gettext as _
|
||||||
from werkzeug.security import gen_salt
|
from werkzeug.security import gen_salt
|
||||||
from canaille.models import Client
|
from canaille.models import Client
|
||||||
from canaille.flaskutils import admin_needed
|
from canaille.flaskutils import permissions_needed
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("admin_clients", __name__)
|
bp = Blueprint("admin_clients", __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def index(user):
|
def index(user):
|
||||||
clients = Client.filter()
|
clients = Client.filter()
|
||||||
return render_template("admin/client_list.html", clients=clients, menuitem="admin")
|
return render_template("admin/client_list.html", clients=clients, menuitem="admin")
|
||||||
|
@ -127,7 +127,7 @@ class ClientAdd(FlaskForm):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/add", methods=["GET", "POST"])
|
@bp.route("/add", methods=["GET", "POST"])
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def add(user):
|
def add(user):
|
||||||
form = ClientAdd(request.form or None)
|
form = ClientAdd(request.form or None)
|
||||||
|
|
||||||
|
@ -177,7 +177,7 @@ def add(user):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/edit/<client_id>", methods=["GET", "POST"])
|
@bp.route("/edit/<client_id>", methods=["GET", "POST"])
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def edit(user, client_id):
|
def edit(user, client_id):
|
||||||
if request.method == "GET" or request.form.get("action") == "edit":
|
if request.method == "GET" or request.form.get("action") == "edit":
|
||||||
return client_edit(client_id)
|
return client_edit(client_id)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from flask import Blueprint, current_app, url_for
|
from flask import Blueprint, current_app, url_for
|
||||||
from flask_themer import render_template
|
from flask_themer import render_template
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from canaille.flaskutils import admin_needed
|
from canaille.flaskutils import permissions_needed
|
||||||
from canaille.mails import profile_hash
|
from canaille.mails import profile_hash
|
||||||
from canaille.apputils import obj_to_b64
|
from canaille.apputils import obj_to_b64
|
||||||
|
|
||||||
|
@ -10,13 +10,13 @@ bp = Blueprint("admin_mails", __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def mail_index(user):
|
def mail_index(user):
|
||||||
return render_template("admin/mails.html")
|
return render_template("admin/mails.html")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/password-init.html")
|
@bp.route("/password-init.html")
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def password_init_html(user):
|
def password_init_html(user):
|
||||||
base_url = url_for("account.index", _external=True)
|
base_url = url_for("account.index", _external=True)
|
||||||
reset_url = url_for(
|
reset_url = url_for(
|
||||||
|
@ -39,7 +39,7 @@ def password_init_html(user):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/password-init.txt")
|
@bp.route("/password-init.txt")
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def password_init_txt(user):
|
def password_init_txt(user):
|
||||||
base_url = url_for("account.index", _external=True)
|
base_url = url_for("account.index", _external=True)
|
||||||
reset_url = url_for(
|
reset_url = url_for(
|
||||||
|
@ -58,7 +58,7 @@ def password_init_txt(user):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/reset.html")
|
@bp.route("/reset.html")
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def password_reset_html(user):
|
def password_reset_html(user):
|
||||||
base_url = url_for("account.index", _external=True)
|
base_url = url_for("account.index", _external=True)
|
||||||
reset_url = url_for(
|
reset_url = url_for(
|
||||||
|
@ -81,7 +81,7 @@ def password_reset_html(user):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/reset.txt")
|
@bp.route("/reset.txt")
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def password_reset_txt(user):
|
def password_reset_txt(user):
|
||||||
base_url = url_for("account.index", _external=True)
|
base_url = url_for("account.index", _external=True)
|
||||||
reset_url = url_for(
|
reset_url = url_for(
|
||||||
|
@ -100,7 +100,7 @@ def password_reset_txt(user):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<uid>/<email>/invitation.html")
|
@bp.route("/<uid>/<email>/invitation.html")
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def invitation_html(user, uid, email):
|
def invitation_html(user, uid, email):
|
||||||
base_url = url_for("account.index", _external=True)
|
base_url = url_for("account.index", _external=True)
|
||||||
registration_url = url_for(
|
registration_url = url_for(
|
||||||
|
@ -123,7 +123,7 @@ def invitation_html(user, uid, email):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<uid>/<email>/invitation.txt")
|
@bp.route("/<uid>/<email>/invitation.txt")
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def invitation_txt(user, uid, email):
|
def invitation_txt(user, uid, email):
|
||||||
base_url = url_for("account.index", _external=True)
|
base_url = url_for("account.index", _external=True)
|
||||||
registration_url = url_for(
|
registration_url = url_for(
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask_themer import render_template
|
from flask_themer import render_template
|
||||||
from canaille.models import Token
|
from canaille.models import Token
|
||||||
from canaille.flaskutils import admin_needed
|
from canaille.flaskutils import permissions_needed
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("admin_tokens", __name__)
|
bp = Blueprint("admin_tokens", __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def index(user):
|
def index(user):
|
||||||
tokens = Token.filter()
|
tokens = Token.filter()
|
||||||
return render_template("admin/token_list.html", tokens=tokens, menuitem="admin")
|
return render_template("admin/token_list.html", tokens=tokens, menuitem="admin")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<token_id>", methods=["GET", "POST"])
|
@bp.route("/<token_id>", methods=["GET", "POST"])
|
||||||
@admin_needed()
|
@permissions_needed("manage_oidc")
|
||||||
def view(user, token_id):
|
def view(user, token_id):
|
||||||
token = Token.get(token_id)
|
token = Token.get(token_id)
|
||||||
return render_template("admin/token_view.html", token=token, menuitem="admin")
|
return render_template("admin/token_view.html", token=token, menuitem="admin")
|
||||||
|
|
|
@ -34,16 +34,12 @@ OIDC_METADATA_FILE = "canaille/conf/openid-configuration.json"
|
||||||
# SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
|
# SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
|
||||||
|
|
||||||
# If this option is set to true, when a user tries to sign in with
|
# If this option 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
|
# 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
|
# exist. If this option is set to false (the default) a message is
|
||||||
# shown saying that the password is wrong, but does not give a clue
|
# shown indicating that the password is wrong, but does not give a clue
|
||||||
# wether the login exists or not.
|
# wether the login exists or not.
|
||||||
# HIDE_INVALID_LOGINS = false
|
# HIDE_INVALID_LOGINS = false
|
||||||
|
|
||||||
# SELF_DELETION controls the ability for a user to delete his own
|
|
||||||
# account. The default value is true.
|
|
||||||
# SELF_DELETION = true
|
|
||||||
|
|
||||||
[LOGGING]
|
[LOGGING]
|
||||||
# LEVEL can be one value among:
|
# LEVEL can be one value among:
|
||||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
|
@ -68,39 +64,60 @@ USER_BASE = "ou=users,dc=mydomain,dc=tld"
|
||||||
# USER_FILTER = "(|(uid={login})(mail={login}))"
|
# USER_FILTER = "(|(uid={login})(mail={login}))"
|
||||||
USER_FILTER = "(|(uid={login})(cn={login}))"
|
USER_FILTER = "(|(uid={login})(cn={login}))"
|
||||||
|
|
||||||
# A class to use for creating new users
|
# The object class to use for creating new users
|
||||||
USER_CLASS = "inetOrgPerson"
|
USER_CLASS = "inetOrgPerson"
|
||||||
|
|
||||||
# Filter to match super admin users. Super admins can manage
|
# Where to search for groups?
|
||||||
# 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"
|
|
||||||
|
|
||||||
GROUP_BASE = "ou=groups"
|
GROUP_BASE = "ou=groups"
|
||||||
|
|
||||||
|
# The object class to use for creating new groups
|
||||||
GROUP_CLASS = "groupOfNames"
|
GROUP_CLASS = "groupOfNames"
|
||||||
|
|
||||||
|
# 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
|
||||||
GROUP_USER_FILTER = "(member={user.dn})"
|
GROUP_USER_FILTER = "(member={user.dn})"
|
||||||
|
|
||||||
# The list of ldap fields you want to be editable by the
|
# You can define access controls that define what users can do on canaille
|
||||||
# users.
|
# An access control consists in a FILTER to match users, a list of PERMISSIONS
|
||||||
FIELDS = [
|
# matched users will be able to perform, and fields users will be able
|
||||||
"uid",
|
# to READ and WRITE.
|
||||||
"mail",
|
#
|
||||||
"givenName",
|
# A 'FILTER' parameter that is a LDAP filter used to determine if a user
|
||||||
"sn",
|
# belongs to an access control. If absent, all the users will match this
|
||||||
"userPassword",
|
# access control. If your LDAP server has the 'memberof' overlay, you can
|
||||||
"telephoneNumber",
|
# filter against group membership.
|
||||||
"employeeNumber",
|
# Here are some examples
|
||||||
# "jpegPhoto",
|
# FILTER = 'uid=admin'
|
||||||
"groups",
|
# 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:
|
||||||
|
# - "manage_users" to allow other users management
|
||||||
|
# - "manage_groups" to allow group edition and creation
|
||||||
|
# - "manage_oidc" to allow OpenID Connect client managements
|
||||||
|
# - "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]
|
||||||
|
READ = ["uid", "groups"]
|
||||||
|
WRITE = ["givenName", "sn", "userPassword", "telephoneNumber"]
|
||||||
|
|
||||||
|
[ACL.ADMIN]
|
||||||
|
FILTER = "memberof=cn=moderators,ou=groups,dc=mydomain,dc=tld"
|
||||||
|
PERMISSIONS = [
|
||||||
|
"manage_users",
|
||||||
|
"manage_groups",
|
||||||
|
"manage_oidc",
|
||||||
|
"delete_account",
|
||||||
|
"impersonate_users",
|
||||||
]
|
]
|
||||||
|
READ = ["uid"]
|
||||||
|
WRITE = ["groups", "givenName", "sn", "userPassword"]
|
||||||
|
|
||||||
# The jwt configuration. You can generate a RSA keypair with:
|
# The jwt configuration. You can generate a RSA keypair with:
|
||||||
# openssl genrsa -out private.pem 4096
|
# openssl genrsa -out private.pem 4096
|
||||||
|
|
|
@ -37,26 +37,14 @@ def user_needed():
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def moderator_needed():
|
def permissions_needed(*args):
|
||||||
|
permissions = set(args)
|
||||||
|
|
||||||
def wrapper(view_function):
|
def wrapper(view_function):
|
||||||
@wraps(view_function)
|
@wraps(view_function)
|
||||||
def decorator(*args, **kwargs):
|
def decorator(*args, **kwargs):
|
||||||
user = current_user()
|
user = current_user()
|
||||||
if not user or not user.moderator:
|
if not user or not permissions.issubset(user.permissions):
|
||||||
abort(403)
|
|
||||||
return view_function(*args, user=user, **kwargs)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def admin_needed():
|
|
||||||
def wrapper(view_function):
|
|
||||||
@wraps(view_function)
|
|
||||||
def decorator(*args, **kwargs):
|
|
||||||
user = current_user()
|
|
||||||
if not user or not user.admin:
|
|
||||||
abort(403)
|
abort(403)
|
||||||
return view_function(*args, user=user, **kwargs)
|
return view_function(*args, user=user, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -144,21 +144,35 @@ PROFILE_FORM_FIELDS = dict(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def profile_form(field_names):
|
def profile_form(write_field_names, readonly_field_names):
|
||||||
if "userPassword" in field_names:
|
if "userPassword" in write_field_names:
|
||||||
field_names += ["password1", "password2"]
|
write_field_names |= {"password1", "password2"}
|
||||||
|
|
||||||
fields = {
|
fields = {
|
||||||
name: PROFILE_FORM_FIELDS.get(name)
|
name: PROFILE_FORM_FIELDS.get(name)
|
||||||
for name in field_names
|
for name in write_field_names | readonly_field_names
|
||||||
if PROFILE_FORM_FIELDS.get(name)
|
if PROFILE_FORM_FIELDS.get(name)
|
||||||
}
|
}
|
||||||
if "groups" in field_names and Group.available_groups():
|
|
||||||
|
if (
|
||||||
|
"groups" in write_field_names | readonly_field_names
|
||||||
|
and Group.available_groups()
|
||||||
|
):
|
||||||
fields["groups"] = wtforms.SelectMultipleField(
|
fields["groups"] = wtforms.SelectMultipleField(
|
||||||
_("Groups"),
|
_("Groups"),
|
||||||
choices=[(group[1], group[0]) for group in Group.available_groups()],
|
choices=[(group[1], group[0]) for group in Group.available_groups()],
|
||||||
render_kw={},
|
render_kw={},
|
||||||
)
|
)
|
||||||
return wtforms.form.BaseForm(fields)
|
|
||||||
|
form = wtforms.form.BaseForm(fields)
|
||||||
|
for field in form:
|
||||||
|
if field.name in readonly_field_names - write_field_names:
|
||||||
|
if field.name == "groups":
|
||||||
|
field.render_kw["disabled"] = "true"
|
||||||
|
else:
|
||||||
|
field.render_kw["readonly"] = "true"
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
class GroupForm(FlaskForm):
|
class GroupForm(FlaskForm):
|
||||||
|
|
|
@ -10,7 +10,7 @@ from flask import (
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_themer import render_template
|
from flask_themer import render_template
|
||||||
|
|
||||||
from .flaskutils import moderator_needed
|
from .flaskutils import permissions_needed
|
||||||
from .forms import GroupForm
|
from .forms import GroupForm
|
||||||
from .models import Group
|
from .models import Group
|
||||||
|
|
||||||
|
@ -18,14 +18,14 @@ bp = Blueprint("groups", __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
@moderator_needed()
|
@permissions_needed("manage_groups")
|
||||||
def groups(user):
|
def groups(user):
|
||||||
groups = Group.filter(objectClass=current_app.config["LDAP"]["GROUP_CLASS"])
|
groups = Group.filter(objectClass=current_app.config["LDAP"]["GROUP_CLASS"])
|
||||||
return render_template("groups.html", groups=groups, menuitem="groups")
|
return render_template("groups.html", groups=groups, menuitem="groups")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/add", methods=("GET", "POST"))
|
@bp.route("/add", methods=("GET", "POST"))
|
||||||
@moderator_needed()
|
@permissions_needed("manage_groups")
|
||||||
def create_group(user):
|
def create_group(user):
|
||||||
form = GroupForm(request.form or None)
|
form = GroupForm(request.form or None)
|
||||||
try:
|
try:
|
||||||
|
@ -52,7 +52,7 @@ def create_group(user):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<groupname>", methods=("GET", "POST"))
|
@bp.route("/<groupname>", methods=("GET", "POST"))
|
||||||
@moderator_needed()
|
@permissions_needed("manage_groups")
|
||||||
def group(user, groupname):
|
def group(user, groupname):
|
||||||
group = Group.get(groupname) or abort(404)
|
group = Group.get(groupname) or abort(404)
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,14 @@ from .ldaputils import LDAPObject
|
||||||
|
|
||||||
class User(LDAPObject):
|
class User(LDAPObject):
|
||||||
id = "cn"
|
id = "cn"
|
||||||
admin = False
|
|
||||||
moderator = False
|
|
||||||
_groups = []
|
_groups = []
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.read = set()
|
||||||
|
self.write = set()
|
||||||
|
self.permissions = set()
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, login=None, dn=None, filter=None, conn=None):
|
def get(cls, login=None, dn=None, filter=None, conn=None):
|
||||||
conn = conn or cls.ldap()
|
conn = conn or cls.ldap()
|
||||||
|
@ -25,29 +29,8 @@ class User(LDAPObject):
|
||||||
filter = current_app.config["LDAP"].get("USER_FILTER").format(login=login)
|
filter = current_app.config["LDAP"].get("USER_FILTER").format(login=login)
|
||||||
|
|
||||||
user = super().get(dn, filter, conn)
|
user = super().get(dn, filter, conn)
|
||||||
|
|
||||||
admin_filter = current_app.config["LDAP"].get("ADMIN_FILTER")
|
|
||||||
moderator_filter = current_app.config["LDAP"].get("USER_ADMIN_FILTER")
|
|
||||||
if (
|
|
||||||
admin_filter
|
|
||||||
and user
|
|
||||||
and user.dn
|
|
||||||
and conn.search_s(user.dn, ldap.SCOPE_SUBTREE, admin_filter)
|
|
||||||
):
|
|
||||||
|
|
||||||
user.admin = True
|
|
||||||
user.moderator = True
|
|
||||||
|
|
||||||
elif (
|
|
||||||
moderator_filter
|
|
||||||
and user
|
|
||||||
and user.dn
|
|
||||||
and conn.search_s(user.dn, ldap.SCOPE_SUBTREE, moderator_filter)
|
|
||||||
):
|
|
||||||
|
|
||||||
user.moderator = True
|
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
|
user.load_permissions(conn)
|
||||||
user.load_groups(conn=conn)
|
user.load_groups(conn=conn)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
@ -148,6 +131,38 @@ class User(LDAPObject):
|
||||||
group.remove_member(self, conn=conn)
|
group.remove_member(self, conn=conn)
|
||||||
self._groups = after
|
self._groups = after
|
||||||
|
|
||||||
|
def load_permissions(self, conn=None):
|
||||||
|
conn = conn or self.ldap()
|
||||||
|
|
||||||
|
for access_group_name, details in current_app.config["ACL"].items():
|
||||||
|
if not details.get("FILTER") or (
|
||||||
|
self.dn
|
||||||
|
and conn.search_s(self.dn, ldap.SCOPE_SUBTREE, details["FILTER"])
|
||||||
|
):
|
||||||
|
self.permissions |= set(details.get("PERMISSIONS", []))
|
||||||
|
self.read |= set(details.get("READ", []))
|
||||||
|
self.write |= set(details.get("WRITE", []))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_manage_users(self):
|
||||||
|
return "manage_users" in self.permissions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_manage_groups(self):
|
||||||
|
return "manage_groups" in self.permissions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_manage_oidc(self):
|
||||||
|
return "manage_oidc" in self.permissions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_delete_account(self):
|
||||||
|
return "delete_account" in self.permissions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_impersonate_users(self):
|
||||||
|
return "impersonate_users" in self.permissions
|
||||||
|
|
||||||
|
|
||||||
class Group(LDAPObject):
|
class Group(LDAPObject):
|
||||||
id = "cn"
|
id = "cn"
|
||||||
|
|
|
@ -189,7 +189,6 @@ class OpenIDHybridGrant(_OpenIDHybridGrant):
|
||||||
|
|
||||||
def get_audiences(self, request):
|
def get_audiences(self, request):
|
||||||
client = request.client
|
client = request.client
|
||||||
print(client)
|
|
||||||
return [Client.get(aud).oauthClientID for aud in client.oauthAudience]
|
return [Client.get(aud).oauthClientID for aud in client.oauthAudience]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
<a class="ui right floated button" href="{{ url_for("account.profile_creation") }}">
|
<a class="ui right floated button" href="{{ url_for("account.profile_creation") }}">
|
||||||
{{ _("Create a user") }}
|
{{ _("Create a user") }}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('account.user_invitation') }}" class="ui right floated button" name="action" value="impersonate" id="impersonate">
|
<a href="{{ url_for('account.user_invitation') }}" class="ui right floated button" name="action" value="invite" id="invite">
|
||||||
{{ _("Invite another user") }}
|
{{ _("Invite another user") }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if self_deletion or (user.moderator and edited_user) %}
|
{% if self_deletion or (user.can_manage_users and edited_user) %}
|
||||||
<div class="ui basic modal">
|
<div class="ui basic modal">
|
||||||
<div class="ui icon header">
|
<div class="ui icon header">
|
||||||
<i class="user minus icon"></i>
|
<i class="user minus icon"></i>
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
action="{{ request.url }}"
|
action="{{ request.url }}"
|
||||||
role="form"
|
role="form"
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
class="ui form info{% if user.moderator and edited_user and not edited_user.has_password() %} warning{% endif %}"
|
class="ui form info{% if user.can_manage_users and edited_user and not edited_user.has_password() %} warning{% endif %}"
|
||||||
>
|
>
|
||||||
|
|
||||||
{#{ sui.render_field(form.csrf_token) }#}
|
{#{ sui.render_field(form.csrf_token) }#}
|
||||||
|
@ -95,7 +95,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if user.moderator %}
|
{% if user.can_manage_users %}
|
||||||
|
|
||||||
{% if not edited_user %}
|
{% if not edited_user %}
|
||||||
|
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
|
|
||||||
<div class="ui right aligned container">
|
<div class="ui right aligned container">
|
||||||
<div class="ui stackable buttons">
|
<div class="ui stackable buttons">
|
||||||
{% if user.moderator and edited_user or self_deletion %}
|
{% if user.can_manage_users and edited_user or self_deletion %}
|
||||||
<button type="submit" class="ui right floated basic negative button confirm" name="action" value="delete" id="delete">
|
<button type="submit" class="ui right floated basic negative button confirm" name="action" value="delete" id="delete">
|
||||||
{% if user.uid != edited_user.uid %}
|
{% if user.uid != edited_user.uid %}
|
||||||
{{ _("Delete the user") }}
|
{{ _("Delete the user") }}
|
||||||
|
@ -166,13 +166,13 @@
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user.admin and edited_user and user.uid != edited_user.uid %}
|
{% if user.can_impersonate_users and edited_user and user.uid != edited_user.uid %}
|
||||||
<a href="{{ url_for('account.impersonate', username=edited_user.uid[0]) }}" class="ui right floated basic button" name="action" value="impersonate" id="impersonate">
|
<a href="{{ url_for('account.impersonate', username=edited_user.uid[0]) }}" class="ui right floated basic button" name="action" value="impersonate" id="impersonate">
|
||||||
{{ _("Impersonate") }}
|
{{ _("Impersonate") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user.admin and not edited_user %}
|
{% if user.can_manage_users and not edited_user %}
|
||||||
<a href="{{ url_for('account.user_invitation') }}" class="ui right floated button" name="action" value="impersonate" id="impersonate">
|
<a href="{{ url_for('account.user_invitation') }}" class="ui right floated button" name="action" value="impersonate" id="impersonate">
|
||||||
{{ _("Invite a user") }}
|
{{ _("Invite a user") }}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -43,19 +43,21 @@
|
||||||
<i class="handshake icon"></i>
|
<i class="handshake icon"></i>
|
||||||
{% trans %}My consents{% endtrans %}
|
{% trans %}My consents{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
{% if user.moderator %}
|
{% if user.can_manage_users %}
|
||||||
<a class="item {% if menuitem == "users" %}active{% endif %}"
|
<a class="item {% if menuitem == "users" %}active{% endif %}"
|
||||||
href="{{ url_for('account.users') }}">
|
href="{{ url_for('account.users') }}">
|
||||||
<i class="users icon"></i>
|
<i class="users icon"></i>
|
||||||
{% trans %}Users{% endtrans %}
|
{% trans %}Users{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.can_manage_groups %}
|
||||||
<a class="item {% if menuitem == "groups" %}active{% endif %}"
|
<a class="item {% if menuitem == "groups" %}active{% endif %}"
|
||||||
href="{{ url_for('groups.groups') }}">
|
href="{{ url_for('groups.groups') }}">
|
||||||
<i class="users cog icon"></i>
|
<i class="users cog icon"></i>
|
||||||
{% trans %}Groups{% endtrans %}
|
{% trans %}Groups{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.admin %}
|
{% if user.can_manage_oidc %}
|
||||||
<div class="ui dropdown item {% if menuitem == "admin" %}active{% endif %}">
|
<div class="ui dropdown item {% if menuitem == "admin" %}active{% endif %}">
|
||||||
<i class="settings icon"></i>
|
<i class="settings icon"></i>
|
||||||
Admin
|
Admin
|
||||||
|
|
|
@ -40,10 +40,6 @@ OIDC_METADATA_FILE = "conf/openid-configuration.json"
|
||||||
# wether the login exists or not.
|
# wether the login exists or not.
|
||||||
# HIDE_INVALID_LOGINS = false
|
# HIDE_INVALID_LOGINS = false
|
||||||
|
|
||||||
# SELF_DELETION controls the ability for a user to delete his own
|
|
||||||
# account. The default value is true.
|
|
||||||
# SELF_DELETION = true
|
|
||||||
|
|
||||||
[LOGGING]
|
[LOGGING]
|
||||||
# LEVEL can be one value among:
|
# LEVEL can be one value among:
|
||||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
|
@ -73,37 +69,64 @@ USER_FILTER = "(|(uid={login})(cn={login}))"
|
||||||
# A class to use for creating new users
|
# A class to use for creating new users
|
||||||
USER_CLASS = "inetOrgPerson"
|
USER_CLASS = "inetOrgPerson"
|
||||||
|
|
||||||
# Filter to match super admin users. Super admins can manage
|
# Where to search for groups?
|
||||||
# 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 list of ldap fields you want to be editable by the
|
|
||||||
# users.
|
|
||||||
FIELDS = [
|
|
||||||
"uid",
|
|
||||||
"mail",
|
|
||||||
"givenName",
|
|
||||||
"sn",
|
|
||||||
"userPassword",
|
|
||||||
"telephoneNumber",
|
|
||||||
"employeeNumber",
|
|
||||||
# "jpegPhoto",
|
|
||||||
"groups",
|
|
||||||
]
|
|
||||||
|
|
||||||
GROUP_BASE = "ou=groups"
|
GROUP_BASE = "ou=groups"
|
||||||
|
|
||||||
|
# The object class to use for creating new groups
|
||||||
GROUP_CLASS = "groupOfNames"
|
GROUP_CLASS = "groupOfNames"
|
||||||
|
|
||||||
|
# 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
|
||||||
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
|
||||||
|
# matched users will be able to perform, and fields users will be able
|
||||||
|
# to READ and WRITE.
|
||||||
|
#
|
||||||
|
# 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:
|
||||||
|
# - "manage_users" to allow other users management
|
||||||
|
# - "manage_groups" to allow group edition and creation
|
||||||
|
# - "manage_oidc" to allow OpenID Connect client managements
|
||||||
|
# - "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]
|
||||||
|
READ = ["uid", "groups"]
|
||||||
|
WRITE = ["givenName", "sn", "userPassword", "telephoneNumber"]
|
||||||
|
|
||||||
|
[ACL.ADMIN]
|
||||||
|
FILTER = "memberof=cn=admins,ou=groups,dc=mydomain,dc=tld"
|
||||||
|
PERMISSIONS = [
|
||||||
|
"manage_users",
|
||||||
|
"manage_groups",
|
||||||
|
"manage_oidc",
|
||||||
|
"delete_account",
|
||||||
|
"impersonate_users",
|
||||||
|
]
|
||||||
|
READ = ["uid"]
|
||||||
|
WRITE = ["groups", "givenName", "sn", "userPassword"]
|
||||||
|
|
||||||
|
[ACL.HALF_ADMIN]
|
||||||
|
FILTER = "memberof=cn=moderators,ou=groups,dc=mydomain,dc=tld"
|
||||||
|
PERMISSIONS = ["manage_users", "manage_groups", "delete_account"]
|
||||||
|
READ = ["uid"]
|
||||||
|
WRITE = ["groups", "givenName", "sn", "userPassword"]
|
||||||
|
|
||||||
# The jwt configuration. You can generate a RSA keypair with:
|
# The jwt configuration. You can generate a RSA keypair with:
|
||||||
# openssl genrsa -out private.pem 4096
|
# openssl genrsa -out private.pem 4096
|
||||||
# openssl rsa -in private.pem -pubout -outform PEM -out public.pem
|
# openssl rsa -in private.pem -pubout -outform PEM -out public.pem
|
||||||
|
|
|
@ -138,24 +138,60 @@ def configuration(slapd_server, smtpd, keypair_path):
|
||||||
"USER_BASE": "ou=users",
|
"USER_BASE": "ou=users",
|
||||||
"USER_FILTER": "(|(uid={login})(cn={login}))",
|
"USER_FILTER": "(|(uid={login})(cn={login}))",
|
||||||
"USER_CLASS": "inetOrgPerson",
|
"USER_CLASS": "inetOrgPerson",
|
||||||
"ADMIN_FILTER": "(|(uid=admin)(sn=admin))",
|
|
||||||
"USER_ADMIN_FILTER": "(|(uid=moderator)(sn=moderator))",
|
|
||||||
"FIELDS": [
|
|
||||||
"uid",
|
|
||||||
"mail",
|
|
||||||
"givenName",
|
|
||||||
"sn",
|
|
||||||
"userPassword",
|
|
||||||
"telephoneNumber",
|
|
||||||
"employeeNumber",
|
|
||||||
"groups",
|
|
||||||
],
|
|
||||||
"GROUP_BASE": "ou=groups",
|
"GROUP_BASE": "ou=groups",
|
||||||
"GROUP_CLASS": "groupOfNames",
|
"GROUP_CLASS": "groupOfNames",
|
||||||
"GROUP_NAME_ATTRIBUTE": "cn",
|
"GROUP_NAME_ATTRIBUTE": "cn",
|
||||||
"GROUP_USER_FILTER": "(member={user.dn})",
|
"GROUP_USER_FILTER": "(member={user.dn})",
|
||||||
"TIMEOUT": 0.1,
|
"TIMEOUT": 0.1,
|
||||||
},
|
},
|
||||||
|
"ACL": {
|
||||||
|
"DEFAULT": {
|
||||||
|
"READ": ["uid", "groups"],
|
||||||
|
"PERMISSIONS": [],
|
||||||
|
"WRITE": [
|
||||||
|
"mail",
|
||||||
|
"givenName",
|
||||||
|
"sn",
|
||||||
|
"userPassword",
|
||||||
|
"telephoneNumber",
|
||||||
|
"employeeNumber",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"ADMIN": {
|
||||||
|
"FILTER": "(|(uid=admin)(sn=admin))",
|
||||||
|
"PERMISSIONS": [
|
||||||
|
"manage_users",
|
||||||
|
"manage_oidc",
|
||||||
|
"delete_account",
|
||||||
|
"impersonate_users",
|
||||||
|
"manage_groups",
|
||||||
|
],
|
||||||
|
"READ": ["uid"],
|
||||||
|
"WRITE": [
|
||||||
|
"mail",
|
||||||
|
"givenName",
|
||||||
|
"sn",
|
||||||
|
"userPassword",
|
||||||
|
"telephoneNumber",
|
||||||
|
"employeeNumber",
|
||||||
|
"groups",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"MODERATOR": {
|
||||||
|
"FILTER": "(|(uid=moderator)(sn=moderator))",
|
||||||
|
"PERMISSIONS": ["manage_users", "manage_groups", "delete_account"],
|
||||||
|
"READ": ["uid"],
|
||||||
|
"WRITE": [
|
||||||
|
"mail",
|
||||||
|
"givenName",
|
||||||
|
"sn",
|
||||||
|
"userPassword",
|
||||||
|
"telephoneNumber",
|
||||||
|
"employeeNumber",
|
||||||
|
"groups",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
"JWT": {
|
"JWT": {
|
||||||
"PUBLIC_KEY": public_key_path,
|
"PUBLIC_KEY": public_key_path,
|
||||||
"PRIVATE_KEY": private_key_path,
|
"PRIVATE_KEY": private_key_path,
|
||||||
|
|
6
tests/fixtures/themes/test/base.html
vendored
6
tests/fixtures/themes/test/base.html
vendored
|
@ -43,19 +43,21 @@
|
||||||
<i class="handshake icon"></i>
|
<i class="handshake icon"></i>
|
||||||
{% trans %}My consents{% endtrans %}
|
{% trans %}My consents{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
{% if user.moderator %}
|
{% if user.can_manage_users %}
|
||||||
<a class="item {% if menuitem == "users" %}active{% endif %}"
|
<a class="item {% if menuitem == "users" %}active{% endif %}"
|
||||||
href="{{ url_for('account.users') }}">
|
href="{{ url_for('account.users') }}">
|
||||||
<i class="users icon"></i>
|
<i class="users icon"></i>
|
||||||
{% trans %}Users{% endtrans %}
|
{% trans %}Users{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.can_manage_groups %}
|
||||||
<a class="item {% if menuitem == "groups" %}active{% endif %}"
|
<a class="item {% if menuitem == "groups" %}active{% endif %}"
|
||||||
href="{{ url_for('groups.groups') }}">
|
href="{{ url_for('groups.groups') }}">
|
||||||
<i class="users cog icon"></i>
|
<i class="users cog icon"></i>
|
||||||
{% trans %}Groups{% endtrans %}
|
{% trans %}Groups{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.admin %}
|
{% if user.can_manage_oidc %}
|
||||||
<div class="ui dropdown item {% if menuitem == "admin" %}active{% endif %}">
|
<div class="ui dropdown item {% if menuitem == "admin" %}active{% endif %}">
|
||||||
<i class="settings icon"></i>
|
<i class="settings icon"></i>
|
||||||
Admin
|
Admin
|
||||||
|
|
|
@ -162,10 +162,6 @@ def test_admin_self_deletion(testclient, slapd_connection):
|
||||||
.follow(status=200)
|
.follow(status=200)
|
||||||
)
|
)
|
||||||
|
|
||||||
testclient.app.config["SELF_DELETION"] = True
|
|
||||||
|
|
||||||
testclient.app.config["SELF_DELETION"] = False
|
|
||||||
|
|
||||||
with testclient.app.app_context():
|
with testclient.app.app_context():
|
||||||
assert User.get("temp", conn=slapd_connection) is None
|
assert User.get("temp", conn=slapd_connection) is None
|
||||||
|
|
||||||
|
@ -187,11 +183,11 @@ def test_user_self_deletion(testclient, slapd_connection):
|
||||||
with testclient.session_transaction() as sess:
|
with testclient.session_transaction() as sess:
|
||||||
sess["user_dn"] = [user.dn]
|
sess["user_dn"] = [user.dn]
|
||||||
|
|
||||||
testclient.app.config["SELF_DELETION"] = False
|
testclient.app.config["ACL"]["DEFAULT"]["PERMISSIONS"] = []
|
||||||
res = testclient.get("/profile/temp")
|
res = testclient.get("/profile/temp")
|
||||||
assert "Delete my account" not in res
|
assert "Delete my account" not in res
|
||||||
|
|
||||||
testclient.app.config["SELF_DELETION"] = True
|
testclient.app.config["ACL"]["DEFAULT"]["PERMISSIONS"] = ["delete_account"]
|
||||||
res = testclient.get("/profile/temp")
|
res = testclient.get("/profile/temp")
|
||||||
assert "Delete my account" in res
|
assert "Delete my account" in res
|
||||||
res = (
|
res = (
|
||||||
|
@ -206,4 +202,4 @@ def test_user_self_deletion(testclient, slapd_connection):
|
||||||
with testclient.session_transaction() as sess:
|
with testclient.session_transaction() as sess:
|
||||||
assert not sess.get("user_dn")
|
assert not sess.get("user_dn")
|
||||||
|
|
||||||
testclient.app.config["SELF_DELETION"] = False
|
testclient.app.config["ACL"]["DEFAULT"]["PERMISSIONS"] = []
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from canaille.models import User
|
from canaille.models import User
|
||||||
|
|
||||||
|
|
||||||
def test_profile(
|
def test_edition(
|
||||||
testclient, slapd_server, slapd_connection, logged_user, admin, foo_group, bar_group
|
testclient, slapd_server, slapd_connection, logged_user, admin, foo_group, bar_group
|
||||||
):
|
):
|
||||||
res = testclient.get("/profile/user", status=200)
|
res = testclient.get("/profile/user", status=200)
|
||||||
|
@ -49,6 +49,81 @@ def test_profile(
|
||||||
assert logged_user.check_password("correct horse battery staple")
|
assert logged_user.check_password("correct horse battery staple")
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_permissions_none(
|
||||||
|
testclient, slapd_server, slapd_connection, logged_user
|
||||||
|
):
|
||||||
|
testclient.get("/profile/user", status=200)
|
||||||
|
with testclient.app.app_context():
|
||||||
|
logged_user.telephoneNumber = ["555-666-777"]
|
||||||
|
logged_user.save(conn=slapd_connection)
|
||||||
|
|
||||||
|
testclient.app.config["ACL"]["DEFAULT"] = {"READ": ["uid"], "WRITE": []}
|
||||||
|
|
||||||
|
res = testclient.get("/profile/user", status=200)
|
||||||
|
assert "telephoneNumber" not in res.form.fields
|
||||||
|
|
||||||
|
testclient.post(
|
||||||
|
"/profile/user", {"action": "edit", "telephoneNumber": "000-000-000"}
|
||||||
|
)
|
||||||
|
with testclient.app.app_context():
|
||||||
|
user = User.get(dn=logged_user.dn, conn=slapd_connection)
|
||||||
|
assert user.telephoneNumber == ["555-666-777"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_permissions_read(
|
||||||
|
testclient, slapd_server, slapd_connection, logged_user
|
||||||
|
):
|
||||||
|
testclient.get("/profile/user", status=200)
|
||||||
|
with testclient.app.app_context():
|
||||||
|
logged_user.telephoneNumber = ["555-666-777"]
|
||||||
|
logged_user.save(conn=slapd_connection)
|
||||||
|
|
||||||
|
testclient.app.config["ACL"]["DEFAULT"] = {
|
||||||
|
"READ": ["uid", "telephoneNumber"],
|
||||||
|
"WRITE": [],
|
||||||
|
}
|
||||||
|
res = testclient.get("/profile/user", status=200)
|
||||||
|
assert "telephoneNumber" in res.form.fields
|
||||||
|
|
||||||
|
testclient.post(
|
||||||
|
"/profile/user", {"action": "edit", "telephoneNumber": "000-000-000"}
|
||||||
|
)
|
||||||
|
with testclient.app.app_context():
|
||||||
|
user = User.get(dn=logged_user.dn, conn=slapd_connection)
|
||||||
|
assert user.telephoneNumber == ["555-666-777"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_permissions_write(
|
||||||
|
testclient, slapd_server, slapd_connection, logged_user
|
||||||
|
):
|
||||||
|
testclient.get("/profile/user", status=200)
|
||||||
|
with testclient.app.app_context():
|
||||||
|
logged_user.telephoneNumber = ["555-666-777"]
|
||||||
|
logged_user.save(conn=slapd_connection)
|
||||||
|
|
||||||
|
testclient.app.config["ACL"]["DEFAULT"] = {
|
||||||
|
"READ": ["uid"],
|
||||||
|
"WRITE": ["telephoneNumber"],
|
||||||
|
}
|
||||||
|
res = testclient.get("/profile/user", status=200)
|
||||||
|
assert "telephoneNumber" in res.form.fields
|
||||||
|
|
||||||
|
testclient.post(
|
||||||
|
"/profile/user", {"action": "edit", "telephoneNumber": "000-000-000"}
|
||||||
|
)
|
||||||
|
with testclient.app.app_context():
|
||||||
|
user = User.get(dn=logged_user.dn, conn=slapd_connection)
|
||||||
|
assert user.telephoneNumber == ["000-000-000"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple_user_cannot_edit_other(testclient, logged_user):
|
||||||
|
testclient.get("/profile/user", status=200)
|
||||||
|
testclient.get("/profile/admin", status=403)
|
||||||
|
testclient.post("/profile/admin", {"action": "edit"}, status=403)
|
||||||
|
testclient.post("/profile/admin", {"action": "delete"}, status=403)
|
||||||
|
testclient.get("/users", status=403)
|
||||||
|
|
||||||
|
|
||||||
def test_bad_email(testclient, slapd_connection, logged_user):
|
def test_bad_email(testclient, slapd_connection, logged_user):
|
||||||
res = testclient.get("/profile/user", status=200)
|
res = testclient.get("/profile/user", status=200)
|
||||||
|
|
||||||
|
@ -114,14 +189,6 @@ def test_password_change_fail(testclient, slapd_connection, logged_user):
|
||||||
assert logged_user.check_password("correct horse battery staple")
|
assert logged_user.check_password("correct horse battery staple")
|
||||||
|
|
||||||
|
|
||||||
def test_simple_user_cannot_edit_other(testclient, logged_user):
|
|
||||||
testclient.get("/profile/user", status=200)
|
|
||||||
testclient.get("/profile/admin", status=403)
|
|
||||||
testclient.post("/profile/admin", {"action": "edit"}, status=403)
|
|
||||||
testclient.post("/profile/admin", {"action": "delete"}, status=403)
|
|
||||||
testclient.get("/users", status=403)
|
|
||||||
|
|
||||||
|
|
||||||
def test_admin_bad_request(testclient, logged_moderator):
|
def test_admin_bad_request(testclient, logged_moderator):
|
||||||
testclient.post("/profile/admin", {"action": "foobar"}, status=400)
|
testclient.post("/profile/admin", {"action": "foobar"}, status=400)
|
||||||
testclient.get("/profile/foobar", status=404)
|
testclient.get("/profile/foobar", status=404)
|
||||||
|
|
Loading…
Reference in a new issue