From 590c51a8a0eabf46da425462d323dce16af4101f Mon Sep 17 00:00:00 2001 From: Julian Rother <julianr@fsmpi.rwth-aachen.de> Date: Sat, 17 Apr 2021 17:22:32 +0200 Subject: [PATCH] Added role moderator pages --- uffd/__init__.py | 4 +- uffd/invite/models.py | 3 +- uffd/invite/templates/invite/list.html | 2 +- uffd/invite/templates/invite/new.html | 2 +- uffd/invite/views.py | 1 + uffd/rolemod/__init__.py | 3 ++ uffd/rolemod/templates/rolemod/list.html | 30 +++++++++++ uffd/rolemod/templates/rolemod/show.html | 65 +++++++++++++++++++++++ uffd/rolemod/views.py | 67 ++++++++++++++++++++++++ uffd/signup/models.py | 2 +- 10 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 uffd/rolemod/__init__.py create mode 100644 uffd/rolemod/templates/rolemod/list.html create mode 100644 uffd/rolemod/templates/rolemod/show.html create mode 100644 uffd/rolemod/views.py diff --git a/uffd/__init__.py b/uffd/__init__.py index 557044ca..0ea45a23 100644 --- a/uffd/__init__.py +++ b/uffd/__init__.py @@ -49,10 +49,10 @@ def create_app(test_config=None): # pylint: disable=too-many-locals db.init_app(app) Migrate(app, db, render_as_batch=True) # pylint: disable=C0415 - from uffd import user, selfservice, role, mail, session, csrf, mfa, oauth2, services, signup, invite + from uffd import user, selfservice, role, mail, session, csrf, mfa, oauth2, services, signup, rolemod, invite # pylint: enable=C0415 - for i in user.bp + selfservice.bp + role.bp + mail.bp + session.bp + csrf.bp + mfa.bp + oauth2.bp + services.bp: + for i in user.bp + selfservice.bp + role.bp + mail.bp + session.bp + csrf.bp + mfa.bp + oauth2.bp + services.bp + rolemod.bp: app.register_blueprint(i) if app.config['LDAP_SERVICE_USER_BIND'] and (app.config['ENABLE_INVITE'] or diff --git a/uffd/invite/models.py b/uffd/invite/models.py index fcfc948f..eb441fb9 100644 --- a/uffd/invite/models.py +++ b/uffd/invite/models.py @@ -30,7 +30,7 @@ class Invite(db.Model): disabled = Column(Boolean, default=False, nullable=False) roles = relationship('Role', secondary=invite_roles) signups = relationship('InviteSignup', back_populates='invite', lazy=True) - grants = relationship('InviteGrant', backref='invite', lazy=True) + grants = relationship('InviteGrant', back_populates='invite', lazy=True) @property def expired(self): @@ -76,6 +76,7 @@ class InviteGrant(db.Model): __tablename__ = 'invite_grant' id = Column(Integer(), primary_key=True, autoincrement=True) invite_id = Column(Integer(), ForeignKey('invite.id'), nullable=False) + invite = relationship('Invite', back_populates='grants') user_dn = Column(String(128), nullable=False) user = DBRelationship('user_dn', User) diff --git a/uffd/invite/templates/invite/list.html b/uffd/invite/templates/invite/list.html index bb6ad283..a371f9e4 100644 --- a/uffd/invite/templates/invite/list.html +++ b/uffd/invite/templates/invite/list.html @@ -89,7 +89,7 @@ <li><b>Expires:</b> {{ invite.valid_until.strftime('%Y-%m-%d %H:%M:%S') }}</li> <li><b>Permissions:</b> <ul> - {% if invite.signup %} + {% if invite.allow_signup %} <li>Link allows account registration</li> {% else %} <li>No account registration allowed</li> diff --git a/uffd/invite/templates/invite/new.html b/uffd/invite/templates/invite/new.html index 7f2d9847..d1f913ac 100644 --- a/uffd/invite/templates/invite/new.html +++ b/uffd/invite/templates/invite/new.html @@ -39,7 +39,7 @@ <tr> <td> <div class="form-check"> - <input class="form-check-input" type="checkbox" id="role-{{ role.id }}" name="role-{{ role.id }}" value="1"> + <input class="form-check-input" type="checkbox" id="role-{{ role.id }}" name="role-{{ role.id }}" value="1" {{ 'checked' if 'role-%d'%role.id in request.values }}> </div> </td> <td>{{ role.name }}</td> diff --git a/uffd/invite/views.py b/uffd/invite/views.py index 8e83dd77..b42ff07a 100644 --- a/uffd/invite/views.py +++ b/uffd/invite/views.py @@ -131,6 +131,7 @@ def use(token): @bp.route('/<token>/grant', methods=['POST']) @login_required() +@csrf_protect(blueprint=bp) def grant(token): invite = Invite.query.filter_by(token=token).first_or_404() invite_grant = InviteGrant(invite=invite, user=get_current_user()) diff --git a/uffd/rolemod/__init__.py b/uffd/rolemod/__init__.py new file mode 100644 index 00000000..67157866 --- /dev/null +++ b/uffd/rolemod/__init__.py @@ -0,0 +1,3 @@ +from .views import bp as bp_ui + +bp = [bp_ui] diff --git a/uffd/rolemod/templates/rolemod/list.html b/uffd/rolemod/templates/rolemod/list.html new file mode 100644 index 00000000..f727ba6c --- /dev/null +++ b/uffd/rolemod/templates/rolemod/list.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block body %} +<div class="row"> + <div class="col"> + <table class="table table-striped table-sm"> + <thead> + <tr> + <th scope="col">name</th> + <th scope="col">description</th> + </tr> + </thead> + <tbody> + {% for role in roles|sort(attribute='name') %} + <tr id="role-{{ role.id }}"> + <th scope="row"> + <a href="{{ url_for('rolemod.show', role_id=role.id) }}"> + {{ role.name or '<empty name>' }} + </a> + </th> + <td> + {{ role.description }} + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> +</div> +{% endblock %} diff --git a/uffd/rolemod/templates/rolemod/show.html b/uffd/rolemod/templates/rolemod/show.html new file mode 100644 index 00000000..1fcb2558 --- /dev/null +++ b/uffd/rolemod/templates/rolemod/show.html @@ -0,0 +1,65 @@ +{% extends 'base.html' %} + +{% block body %} + +<form method="POST" action="{{ url_for("rolemod.update", role_id=role.id) }}"> + <div class="float-sm-right pb-2"> + {% if config['ENABLE_INVITE'] %} + <a href="{{ url_for("invite.new", **{"role-%d"%role.id: 1}) }}" class="btn btn-primary mr-2"><i class="fa fa-link" aria-hidden="true"></i> Invite Members</a> + {% endif %} + <button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> Save</button> + <a href="{{ url_for("rolemod.index") }}" class="btn btn-secondary">Cancel</a> + </div> + <ul class="nav nav-tabs pt-2 border-0" id="tablist" role="tablist"> + <li class="nav-item"> + <a class="nav-link active" id="overview-tab" data-toggle="tab" href="#overview" role="tab" aria-controls="overview" aria-selected="true">Overview</a> + </li> + <li class="nav-item"> + <a class="nav-link" id="members-tab" data-toggle="tab" href="#members" role="tab" aria-controls="groups" aria-selected="false">Members <span class="badge badge-pill badge-secondary">{{ role.members|length }}</span></a> + </li> + </ul> + <div class="tab-content border mb-2 pt-2" id="tabcontent"> + <div class="tab-pane fade show active" id="overview" role="tabpanel" aria-labelledby="overview-tab"> + <div class="form-group col"> + <label for="role-name">Role Name</label> + <input type="text" class="form-control" id="role-name" value="{{ role.name }}" readonly> + </div> + <div class="form-group col"> + <label for="role-description">Description</label> + <textarea class="form-control" id="role-description" rows="5" name="description">{{ role.description }}</textarea> + </div> + <div class="form-group col"> + <label for="role-moderators">Moderators:</label> + <ul> + {% for moderator in role.moderator_group.members %} + <li>{{ moderator.displayname }} ({{ moderator.loginname }})</li> + {% endfor %} + </ul> + </div> + </div> + <div class="tab-pane fade" id="members" role="tabpanel" aria-labelledby="members-tab"> + <div class="col"> + <span>Role members:</span> + <table class="table table-striped table-sm"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col"></th> + </tr> + </thead> + <tbody> + {% for member in role.members|sort(attribute="loginname") %} + <tr> + <td>{{ member.displayname }} ({{ member.loginname }})</td> + <td class="text-right"> + <a type="button" class="btn btn-danger py-0" href="{{ url_for('rolemod.delete_member', role_id=role.id, member_dn=member.dn) }}">Remove</a> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> +</form> +{% endblock %} diff --git a/uffd/rolemod/views.py b/uffd/rolemod/views.py new file mode 100644 index 00000000..174d923b --- /dev/null +++ b/uffd/rolemod/views.py @@ -0,0 +1,67 @@ +from flask import Blueprint, render_template, request, url_for, redirect, flash + +from uffd.navbar import register_navbar +from uffd.csrf import csrf_protect +from uffd.role.models import Role +from uffd.user.models import User +from uffd.session import get_current_user, login_required, is_valid_session +from uffd.database import db +from uffd.ldap import ldap + +bp = Blueprint('rolemod', __name__, template_folder='templates', url_prefix='/rolemod/') + +def user_is_rolemod(): + return is_valid_session() and Role.query.filter(Role.moderator_group_dn.in_(get_current_user().group_dns)).count() + +@bp.before_request +@login_required() +def acl_check(): #pylint: disable=inconsistent-return-statements + if not user_is_rolemod(): + flash('Access denied') + return redirect(url_for('index')) + +@bp.route("/") +@register_navbar('Moderation', icon='user-lock', blueprint=bp, visible=user_is_rolemod) +def index(): + roles = Role.query.filter(Role.moderator_group_dn.in_(get_current_user().group_dns)).all() + return render_template('rolemod/list.html', roles=roles) + +@bp.route("/<int:role_id>") +def show(role_id): + # prefetch all users so the ldap orm can cache them and doesn't run one ldap query per user + User.query.all() + role = Role.query.get_or_404(role_id) + if role.moderator_group not in get_current_user().groups: + flash('Access denied') + return redirect(url_for('index')) + return render_template('rolemod/show.html', role=role) + +@bp.route("/<int:role_id>", methods=['POST']) +@csrf_protect(blueprint=bp) +def update(role_id): + role = Role.query.get_or_404(role_id) + if role.moderator_group not in get_current_user().groups: + flash('Access denied') + return redirect(url_for('index')) + if request.form['description'] != role.description: + if len(request.form['description']) > 256: + flash('Description too long') + return redirect(url_for('.show', role_id=role.id)) + role.description = request.form['description'] + db.session.commit() + return redirect(url_for('.show', role_id=role.id)) + +@bp.route("/<int:role_id>/delete_member/<member_dn>") +@csrf_protect(blueprint=bp) +def delete_member(role_id, member_dn): + role = Role.query.get_or_404(role_id) + if role.moderator_group not in get_current_user().groups: + flash('Access denied') + return redirect(url_for('index')) + member = User.query.get_or_404(member_dn) + role.members.discard(member) + member.update_groups() + ldap.session.commit() + db.session.commit() + flash('Member removed') + return redirect(url_for('.show', role_id=role.id)) diff --git a/uffd/signup/models.py b/uffd/signup/models.py index 0a7532ae..a2f4b721 100644 --- a/uffd/signup/models.py +++ b/uffd/signup/models.py @@ -35,7 +35,7 @@ class Signup(db.Model): mail = Column(Text) pwhash = Column(Text) user_dn = Column(String(128)) # Set after successful confirmation - user = DBRelationship('user_dn', User) + user = DBRelationship('user_dn', User, backref='signups') type = Column(String(50)) __mapper_args__ = { -- GitLab