forked from Github-Mirrors/canaille
Show groups and enable group creation
This commit is contained in:
parent
3a03f927d1
commit
aed6b18aa8
10 changed files with 203 additions and 8 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -19,3 +19,4 @@ canaille/conf/openid-configuration.json
|
|||
canaille/conf/*.pem
|
||||
canaille/conf/*.pub
|
||||
canaille/conf/*.key
|
||||
.vscode
|
|
@ -11,6 +11,7 @@ import canaille.consents
|
|||
import canaille.commands.clean
|
||||
import canaille.oauth
|
||||
import canaille.account
|
||||
import canaille.groups
|
||||
import canaille.well_known
|
||||
|
||||
from cryptography.hazmat.primitives import serialization as crypto_serialization
|
||||
|
@ -135,6 +136,7 @@ def setup_app(app):
|
|||
config_oauth(app)
|
||||
setup_ldap_tree(app)
|
||||
app.register_blueprint(canaille.account.bp)
|
||||
app.register_blueprint(canaille.groups.bp, url_prefix="/groups")
|
||||
app.register_blueprint(canaille.oauth.bp, url_prefix="/oauth")
|
||||
app.register_blueprint(canaille.commands.clean.bp)
|
||||
app.register_blueprint(canaille.consents.bp, url_prefix="/consent")
|
||||
|
|
|
@ -145,3 +145,13 @@ def profile_form(field_names):
|
|||
render_kw={},
|
||||
)
|
||||
return wtforms.form.BaseForm(fields)
|
||||
|
||||
|
||||
class GroupForm(FlaskForm):
|
||||
name = wtforms.StringField(
|
||||
_("Name"),
|
||||
validators=[wtforms.validators.DataRequired()],
|
||||
render_kw={
|
||||
"placeholder": _("group"),
|
||||
},
|
||||
)
|
||||
|
|
62
canaille/groups.py
Normal file
62
canaille/groups.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
from flask import Blueprint, render_template, redirect, url_for, request, flash, current_app, abort
|
||||
from flask_babel import gettext as _
|
||||
|
||||
from .flaskutils import moderator_needed
|
||||
from .forms import GroupForm
|
||||
from .models import Group
|
||||
|
||||
bp = Blueprint("groups", __name__)
|
||||
|
||||
@bp.route("/")
|
||||
@moderator_needed()
|
||||
def groups(user):
|
||||
groups = Group.filter(objectClass=current_app.config["LDAP"]["GROUP_CLASS"])
|
||||
return render_template("groups.html", groups=groups, menuitem="groups")
|
||||
|
||||
@bp.route("/add", methods=("GET", "POST"))
|
||||
@moderator_needed()
|
||||
def create_group(user):
|
||||
form = GroupForm(request.form or None)
|
||||
try:
|
||||
if "name" in form:
|
||||
del form["name"].render_kw["disabled"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if request.form:
|
||||
if not form.validate():
|
||||
flash(_("Group creation failed."), "error")
|
||||
else:
|
||||
group = Group(objectClass=current_app.config["LDAP"]["GROUP_CLASS"])
|
||||
group.member = [user.dn]
|
||||
group.cn = [form.name.data]
|
||||
group.save()
|
||||
return redirect(url_for("groups.groups"))
|
||||
|
||||
return render_template(
|
||||
"group.html",
|
||||
form=form,
|
||||
edited_group=None,
|
||||
members=None
|
||||
)
|
||||
|
||||
@bp.route("/<groupname>", methods=("GET", "POST"))
|
||||
@moderator_needed()
|
||||
def group(user, groupname):
|
||||
group = Group.get(groupname) or abort(404)
|
||||
form = GroupForm(request.form or None, data={"name": group.name})
|
||||
form["name"].render_kw["disabled"] = "true"
|
||||
|
||||
if request.form:
|
||||
if form.validate():
|
||||
group.save()
|
||||
else:
|
||||
flash(_("Group edition failed."), "error")
|
||||
|
||||
return render_template(
|
||||
"group.html",
|
||||
form=form,
|
||||
edited_group=group,
|
||||
members=group.get_members()
|
||||
)
|
||||
|
|
@ -154,6 +154,11 @@ class Group(LDAPObject):
|
|||
attribute = current_app.config["LDAP"].get("GROUP_NAME_ATTRIBUTE")
|
||||
return [(group[attribute][0], group.dn) for group in groups]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
attribute = current_app.config["LDAP"].get("GROUP_NAME_ATTRIBUTE")
|
||||
return self[attribute][0]
|
||||
|
||||
def get_members(self, conn=None):
|
||||
return [User.get(dn=user_dn, conn=conn) for user_dn in self.member]
|
||||
|
||||
|
|
|
@ -47,6 +47,11 @@
|
|||
<i class="users icon"></i>
|
||||
{% trans %}Users{% endtrans %}
|
||||
</a>
|
||||
<a class="item {% if menuitem == "groups" %}active{% endif %}"
|
||||
href="{{ url_for('groups.groups') }}">
|
||||
<i class="users icon"></i>
|
||||
{% trans %}Groups{% endtrans %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user.admin %}
|
||||
<div class="ui dropdown item {% if menuitem == "admin" %}active{% endif %}">
|
||||
|
|
58
canaille/templates/group.html
Normal file
58
canaille/templates/group.html
Normal file
|
@ -0,0 +1,58 @@
|
|||
{% extends 'base.html' %}
|
||||
{% import 'fomanticui.j2' as sui %}
|
||||
{% import 'flask.j2' as flask %}
|
||||
|
||||
{% block content %}
|
||||
{% if edited_group %}
|
||||
<div class="ui segment">
|
||||
<h2 class="ui header">{% trans %}Members{% endtrans %}</h2>
|
||||
<ul>
|
||||
{% for member in members %}
|
||||
<div class="ui left icon">
|
||||
<i class="user icon"></i>
|
||||
<a href="{{ url_for('account.profile_edition', username=member.uid[0]) }}">{{ member.name }}</a></li>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="ui clearing segment">
|
||||
<h2 class="ui center aligned header">
|
||||
<div class="content">
|
||||
{% if not edited_group %}
|
||||
{% trans %}Group creation{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}Group edition{% endtrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="sub header">
|
||||
{% if not edited_group %}
|
||||
{% trans %}Create a new group{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}Edit informations about a group{% endtrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
{{ flask.messages() }}
|
||||
|
||||
<form method="POST"
|
||||
id="{{ form.__class__.__name__|lower }}"
|
||||
action="{{ request.url }}"
|
||||
role="form"
|
||||
class="ui form"
|
||||
>
|
||||
{{ form.hidden_tag() if form.hidden_tag }}
|
||||
{{ sui.render_field(form.name) }}
|
||||
<button type="submit" class="ui right floated primary button">
|
||||
{% if not edited_group %}
|
||||
{% trans %}Create group{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}Submit{% endtrans %}
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
19
canaille/templates/groups.html
Normal file
19
canaille/templates/groups.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="ui segment">
|
||||
<a class="ui primary button" href="{{ url_for('groups.create_group') }}">{% trans %}Add a group{% endtrans %}</a>
|
||||
</div>
|
||||
|
||||
<div class="ui segment">
|
||||
<h2 class="ui header">{% trans %}Groups{% endtrans %}</h2>
|
||||
<ul>
|
||||
{% for group in groups %}
|
||||
<div class="ui left icon">
|
||||
<i class="users icon"></i>
|
||||
<a href="{{ url_for('groups.group', groupname=group.name) }}">{{ group.name }}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -380,8 +380,3 @@ def bar_group(app, admin, slapd_connection):
|
|||
yield g
|
||||
admin._groups = []
|
||||
g.delete(conn=slapd_connection)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def groups(foo_group, bar_group, slapd_connection):
|
||||
return (foo_group, bar_group)
|
||||
|
|
|
@ -9,7 +9,7 @@ def test_no_group(app, slapd_connection):
|
|||
def test_set_groups(app, slapd_connection, user, foo_group, bar_group):
|
||||
with app.app_context():
|
||||
Group.attr_type_by_name(conn=slapd_connection)
|
||||
a = User.attr_type_by_name(conn=slapd_connection)
|
||||
User.attr_type_by_name(conn=slapd_connection)
|
||||
|
||||
user = User.get(dn=user.dn, conn=slapd_connection)
|
||||
assert set(Group.available_groups(conn=slapd_connection)) == {
|
||||
|
@ -21,14 +21,52 @@ def test_set_groups(app, slapd_connection, user, foo_group, bar_group):
|
|||
assert user.groups[0].dn == foo_group.dn
|
||||
|
||||
user.set_groups([foo_group, bar_group], conn=slapd_connection)
|
||||
|
||||
bar_dns = {g.dn for g in bar_group.get_members(conn=slapd_connection)}
|
||||
assert user.dn in bar_dns
|
||||
|
||||
assert user.groups[1].dn == bar_group.dn
|
||||
|
||||
user.set_groups([foo_group], conn=slapd_connection)
|
||||
|
||||
foo_dns = {g.dn for g in foo_group.get_members(conn=slapd_connection)}
|
||||
bar_dns = {g.dn for g in bar_group.get_members(conn=slapd_connection)}
|
||||
|
||||
assert user.dn in foo_dns
|
||||
assert user.dn not in bar_dns
|
||||
|
||||
def test_group_creation(testclient, slapd_connection, logged_moderator, foo_group):
|
||||
# The group does not exist
|
||||
res = testclient.get("/groups", status=200)
|
||||
with testclient.app.app_context():
|
||||
assert Group.get("bar", conn=slapd_connection) is None
|
||||
assert Group.get("foo", conn=slapd_connection) == foo_group
|
||||
assert "bar" not in res.text
|
||||
assert "foo" in res.text
|
||||
|
||||
# Fill the form for a new group
|
||||
res = testclient.get("/groups/add", status=200)
|
||||
res.form["name"] = "bar"
|
||||
|
||||
# Group has been created
|
||||
res = res.form.submit(status=302).follow(status=200)
|
||||
|
||||
with testclient.app.app_context():
|
||||
bar_group = Group.get("bar", conn=slapd_connection)
|
||||
assert bar_group.name == "bar"
|
||||
assert [member.dn for member in bar_group.get_members(conn=slapd_connection)] == [logged_moderator.dn] # Group cannot be empty so creator is added in it
|
||||
assert "bar" in res.text
|
||||
|
||||
# Group name can not be edited
|
||||
res = testclient.get("/groups/bar", status=200)
|
||||
res.form["name"] = "bar2"
|
||||
|
||||
res = res.form.submit(status=200)
|
||||
|
||||
with testclient.app.app_context():
|
||||
bar_group = Group.get("bar", conn=slapd_connection)
|
||||
assert bar_group.name == "bar"
|
||||
assert Group.get("bar2", conn=slapd_connection) is None
|
||||
members = bar_group.get_members(conn=slapd_connection)
|
||||
for member in members:
|
||||
assert member.name in res.text
|
||||
|
||||
# TODO: Group is deleted
|
||||
|
|
Loading…
Reference in a new issue