From 4d6673fe7f2f54d3ea74b5fda8a010bfd2f32736 Mon Sep 17 00:00:00 2001 From: Julian Rother <julianr@fsmpi.rwth-aachen.de> Date: Mon, 14 Jun 2021 01:30:20 +0200 Subject: [PATCH] Add locking mechanism for externally managed roles --- .../a594d3b3e05b_added_role_locked.py | 23 +++++++++++ uffd/role/models.py | 7 +++- uffd/role/templates/role/show.html | 16 +++++--- uffd/role/views.py | 40 ++++++++++++------- 4 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 migrations/versions/a594d3b3e05b_added_role_locked.py diff --git a/migrations/versions/a594d3b3e05b_added_role_locked.py b/migrations/versions/a594d3b3e05b_added_role_locked.py new file mode 100644 index 00000000..5ca1b89a --- /dev/null +++ b/migrations/versions/a594d3b3e05b_added_role_locked.py @@ -0,0 +1,23 @@ +"""added role.locked + +Revision ID: a594d3b3e05b +Revises: 5cab70e95bf8 +Create Date: 2021-06-14 00:32:47.792794 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'a594d3b3e05b' +down_revision = '5cab70e95bf8' +branch_labels = None +depends_on = None + +def upgrade(): + with op.batch_alter_table('role', schema=None) as batch_op: + batch_op.add_column(sa.Column('locked', sa.Boolean(name=op.f('ck_role_locked')), nullable=False, default=False)) + +def downgrade(): + with op.batch_alter_table('role', schema=None) as batch_op: + batch_op.drop_column('locked') diff --git a/uffd/role/models.py b/uffd/role/models.py index 4839b4ff..dad00f53 100644 --- a/uffd/role/models.py +++ b/uffd/role/models.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, String, Integer, Text, ForeignKey +from sqlalchemy import Column, String, Integer, Text, ForeignKey, Boolean from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declared_attr @@ -91,6 +91,11 @@ class Role(db.Model): db_groups = relationship("RoleGroup", backref="role", cascade="all, delete-orphan") groups = DBRelationship('db_groups', Group, RoleGroup, backattr='role', backref='roles') + # Roles that are managed externally (e.g. by Ansible) can be locked to + # prevent accidental editing of name, moderator group, included roles + # and groups as well as deletion in the web interface. + locked = Column(Boolean(), default=False, nullable=False) + @property def indirect_members(self): users = set() diff --git a/uffd/role/templates/role/show.html b/uffd/role/templates/role/show.html index 8cf25a5d..81d96395 100644 --- a/uffd/role/templates/role/show.html +++ b/uffd/role/templates/role/show.html @@ -1,13 +1,19 @@ {% extends 'base.html' %} {% block body %} +{% if role.locked %} +<div class="alert alert-warning" role="alert"> +Name, moderator group, included roles and groups of this role are managed externally. <a href="{{ url_for("role.unlock", roleid=role.id) }}" class="alert-link">Unlock this role</a> to edit them at the risk of having your changes overwritten. +</div> +{% endif %} + <form action="{{ url_for("role.update", roleid=role.id) }}" method="POST"> <div class="align-self-center"> <div class="float-sm-right pb-2"> <button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> Save</button> <a href="{{ url_for("role.index") }}" class="btn btn-secondary">Cancel</a> {% if role.id %} - <a href="{{ url_for("role.delete", roleid=role.id) }}" onClick="return confirm('Are you sure?');" class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a> + <a href="{{ url_for("role.delete", roleid=role.id) }}" onClick="return confirm('Are you sure?');" class="btn btn-danger {{ 'disabled' if role.locked }}"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a> {% else %} <a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a> {% endif %} @@ -28,7 +34,7 @@ <div class="tab-pane fade show active" id="settings" role="tabpanel" aria-labelledby="settings-tab"> <div class="form-group col"> <label for="role-name">Role Name</label> - <input type="text" class="form-control" id="role-name" name="name" value="{{ role.name or '' }}"> + <input type="text" class="form-control" id="role-name" name="name" value="{{ role.name or '' }}" {{ 'disabled' if role.locked }}> <small class="form-text text-muted"> </small> </div> @@ -40,7 +46,7 @@ </div> <div class="form-group col"> <label for="moderator-group">Moderator Group</label> - <select class="form-control" id="moderator-group" name="moderator-group"> + <select class="form-control" id="moderator-group" name="moderator-group" {{ 'disabled' if role.locked }}> <option value="" class="text-muted">No Moderator Group</option> {% for group in groups %} <option value="{{ group.dn }}" {{ 'selected' if group == role.moderator_group }}>{{ group.name }}</option> @@ -82,7 +88,7 @@ <td> <div class="form-check"> <input class="form-check-input" type="checkbox" id="include-role-{{ r.id }}-checkbox" name="include-role-{{ r.id }}" value="1" aria-label="enabled" - {% if r == role %}disabled{% endif %} + {% if r == role or role.locked %}disabled{% endif %} {% if r in role.included_roles %}checked{% endif %}> </div> </td> @@ -121,7 +127,7 @@ <tr id="group-{{ group.gid }}"> <td> <div class="form-check"> - <input class="form-check-input" type="checkbox" id="group-{{ group.gid }}-checkbox" name="group-{{ group.gid }}" value="1" aria-label="enabled" {% if group in role.groups %}checked{% endif %}> + <input class="form-check-input" type="checkbox" id="group-{{ group.gid }}-checkbox" name="group-{{ group.gid }}" value="1" aria-label="enabled" {% if group in role.groups %}checked{% endif %} {{ 'disabled' if role.locked }}> </div> </td> <td> diff --git a/uffd/role/views.py b/uffd/role/views.py index 9cf9eb9b..a613a191 100644 --- a/uffd/role/views.py +++ b/uffd/role/views.py @@ -71,22 +71,23 @@ def update(roleid=None): db.session.add(role) else: role = Role.query.filter_by(id=roleid).one() - role.name = request.values['name'] role.description = request.values['description'] - if not request.values['moderator-group']: - role.moderator_group_dn = None - else: - role.moderator_group = Group.query.get(request.values['moderator-group']) - for included_role in Role.query.all(): - if included_role != role and request.values.get('include-role-{}'.format(included_role.id)): - role.included_roles.append(included_role) - elif included_role in role.included_roles: - role.included_roles.remove(included_role) - for group in Group.query.all(): - if request.values.get('group-{}'.format(group.gid), False): - role.groups.add(group) + if not role.locked: + role.name = request.values['name'] + if not request.values['moderator-group']: + role.moderator_group_dn = None else: - role.groups.discard(group) + role.moderator_group = Group.query.get(request.values['moderator-group']) + for included_role in Role.query.all(): + if included_role != role and request.values.get('include-role-{}'.format(included_role.id)): + role.included_roles.append(included_role) + elif included_role in role.included_roles: + role.included_roles.remove(included_role) + for group in Group.query.all(): + if request.values.get('group-{}'.format(group.gid), False): + role.groups.add(group) + else: + role.groups.discard(group) role.update_member_groups() db.session.commit() ldap.session.commit() @@ -96,6 +97,9 @@ def update(roleid=None): @csrf_protect(blueprint=bp) def delete(roleid): role = Role.query.filter_by(id=roleid).one() + if role.locked: + flash('Locked roles cannot be deleted') + return redirect(url_for('role.show', roleid=role.id)) oldmembers = set(role.members).union(role.indirect_members) role.members.clear() db.session.delete(role) @@ -104,3 +108,11 @@ def delete(roleid): db.session.commit() ldap.session.commit() return redirect(url_for('role.index')) + +@bp.route("/<int:roleid>/unlock") +@csrf_protect(blueprint=bp) +def unlock(roleid): + role = Role.query.filter_by(id=roleid).one() + role.locked = False + db.session.commit() + return redirect(url_for('role.show', roleid=role.id)) -- GitLab