diff --git a/.pylintrc b/.pylintrc index 038648ffa5353a85c687df0372aadf26ad72ec66..275ec9f187b45438d80eb93423c126409c103a28 100644 --- a/.pylintrc +++ b/.pylintrc @@ -560,13 +560,13 @@ valid-metaclass-classmethod-first-arg=cls max-args=7 # Maximum number of attributes for a class (see R0902). -max-attributes=7 +max-attributes=12 # Maximum number of boolean expressions in an if statement (see R0916). max-bool-expr=5 # Maximum number of branch for function / method body. -max-branches=12 +max-branches=15 # Maximum number of locals for function / method body. max-locals=15 diff --git a/uffd/role/__init__.py b/uffd/role/__init__.py index cd6180edf5bec62013017a43cb8d260db17b056f..671578662f91c82cb987ffe679c1f102dc493d1f 100644 --- a/uffd/role/__init__.py +++ b/uffd/role/__init__.py @@ -1,4 +1,3 @@ from .views import bp as bp_ui -from .models import Role, RoleGroup, RoleUser bp = [bp_ui] diff --git a/uffd/role/models.py b/uffd/role/models.py index 596f2e0d85a678a10facacea916249d70e9e6740..d8b379cdf61e52058038b709991fae58a16d3b6c 100644 --- a/uffd/role/models.py +++ b/uffd/role/models.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declared_attr from uffd.database import db -from uffd.user import User, Group +from uffd.user.models import User, Group class Role(db.Model): __tablename__ = 'role' @@ -19,10 +19,31 @@ class Role(db.Model): self.name = name self.description = description - def group_dns(self): - return map(attrgetter('dn'), self.groups) + @classmethod + def get_for_user(cls, user): + return Role.query.join(Role.members, aliased=True).filter_by(dn=user.dn) + def member_dns(self): - return map(attrgetter('dn'), self.members) + return list(map(attrgetter('dn'), self.members)) + def add_member(self, member): + newmapping = RoleUser(member.dn, self) + self.members.append(newmapping) + def del_member(self, member): + for i in self.members: + if i.dn == member.dn: + self.members.remove(i) + break + + def group_dns(self): + return list(map(attrgetter('dn'), self.groups)) + def add_group(self, group): + newmapping = RoleGroup(group.dn, self) + self.groups.append(newmapping) + def del_group(self, group): + for i in self.groups: + if i.dn == group.dn: + self.groups.remove(i) + break class LdapMapping(): id = Column(Integer(), primary_key=True, autoincrement=True) @@ -35,6 +56,10 @@ class LdapMapping(): return Column(ForeignKey('role.id')) ldapclass = None + def __init__(self, dn='', role=''): + self.dn = dn + self.role = role + def get_ldap(self): return self.ldapclass.from_ldap_dn(self.dn) diff --git a/uffd/role/views.py b/uffd/role/views.py index 737d1c6b17056cbca3a7f0d71d5693802161b317..497333fad962264b579a6f999a35891566d6f591 100644 --- a/uffd/role/views.py +++ b/uffd/role/views.py @@ -2,8 +2,8 @@ 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, RoleGroup -from uffd.user import Group +from uffd.role.models import Role +from uffd.user.models import Group from uffd.session import get_current_user, login_required, is_valid_session from uffd.database import db @@ -48,17 +48,19 @@ def update(roleid=False): role.description = request.values['description'] groups = Group.from_ldap_all() - role_group_dns = list(role.group_dns()) + role_group_dns = role.group_dns() for group in groups: if request.values.get('group-{}'.format(group.gid), False): if group.dn in role_group_dns: continue - newmapping = RoleGroup() - newmapping.dn = group.dn - newmapping.role = role - session.add(newmapping) + role.add_group(group) elif group.dn in role_group_dns: - session.delete(RoleGroup.query.filter_by(role_id=role.id, dn=group.dn).one()) + role.del_group(group) + +# usergroups = set() +# for role in Role.get_for_user(user).all(): +# usergroups.update(role.group_dns()) +# user.replace_group_dns(usergroups) session.commit() return redirect(url_for('role.index')) diff --git a/uffd/user/__init__.py b/uffd/user/__init__.py index 130d312fb98b96018dcf1d7cb3b61c6116789f1f..3d56a5689b10fdc41e607c5a15dd2fae86d90c73 100644 --- a/uffd/user/__init__.py +++ b/uffd/user/__init__.py @@ -1,5 +1,4 @@ from .views_user import bp as bp_user from .views_group import bp as bp_group -from .models import User, Group bp = [bp_user, bp_group] diff --git a/uffd/user/models.py b/uffd/user/models.py index 7da2cca0bde9e1970110ff1189bafb6f4199c9d7..42a0f12be576287c0584e4a59ccd75e2fdc9545a 100644 --- a/uffd/user/models.py +++ b/uffd/user/models.py @@ -1,18 +1,24 @@ -from ldap3 import MODIFY_REPLACE, HASHED_SALTED_SHA512 +import secrets + +from ldap3 import MODIFY_REPLACE, MODIFY_DELETE, MODIFY_ADD, HASHED_SALTED_SHA512 from ldap3.utils.hashed import hashed from flask import current_app from uffd import ldap class User(): - def __init__(self, uid=None, loginname='', displayname='', mail='', groups=None): + def __init__(self, uid=None, loginname='', displayname='', mail='', groups=None, dn=None): self.uid = uid self.loginname = loginname self.displayname = displayname self.mail = mail + self.newpassword = None + self.dn = dn + self.groups_ldap = groups or [] + self.initial_groups_ldap = groups or [] + self.groups_changed = False self._groups = None - self.newpassword = None @classmethod def from_ldap(cls, ldapobject): @@ -21,7 +27,8 @@ class User(): loginname=ldapobject['uid'].value, displayname=ldapobject['cn'].value, mail=ldapobject['mail'].value, - groups=ldap.get_ldap_array_attribute_safe(ldapobject, 'memberOf') + groups=ldap.get_ldap_array_attribute_safe(ldapobject, 'memberOf'), + dn=ldapobject.entry_dn, ) @classmethod @@ -35,11 +42,13 @@ class User(): def to_ldap(self, new=False): conn = ldap.get_conn() if new: + self.uid = ldap.get_next_uid() attributes = { - 'uidNumber': ldap.get_next_uid(), + 'uidNumber': self.uid, 'gidNumber': current_app.config['LDAP_USER_GID'], 'homeDirectory': '/home/'+self.loginname, 'sn': ' ', + 'userPassword': hashed(HASHED_SALTED_SHA512, secrets.token_hex(128)), # same as for update 'givenName': self.displayname, 'displayName': self.displayname, @@ -59,6 +68,17 @@ class User(): attributes['userPassword'] = [(MODIFY_REPLACE, [hashed(HASHED_SALTED_SHA512, self.newpassword)])] dn = ldap.uid_to_dn(self.uid) result = conn.modify(dn, attributes) + self.dn = dn + + group_conn = ldap.get_conn() + for group in self.initial_groups_ldap: + if not group in self.groups_ldap: + group_conn.modify(group, {'uniqueMember': [(MODIFY_DELETE, [self.dn])]}) + for group in self.groups_ldap: + if not group in self.initial_groups_ldap: + group_conn.modify(group, {'uniqueMember': [(MODIFY_ADD, [self.dn])]}) + self.groups_changed = False + return result def get_groups(self): @@ -71,6 +91,10 @@ class User(): groups.append(newgroup) self._groups = groups return groups + def replace_group_dns(self, values): + self._groups = None + self.groups_ldap = values + self.groups_changed = True def is_in_group(self, name): if not name: diff --git a/uffd/user/templates/user.html b/uffd/user/templates/user.html index 3ac0a02b8018fe868bb502aa93b034444816174e..76b560055574d77301268fd7f78f550bad12e1c8 100644 --- a/uffd/user/templates/user.html +++ b/uffd/user/templates/user.html @@ -3,7 +3,16 @@ {% block body %} <form action="{{ url_for("user.update", uid=user.uid) }}" method="POST"> <div class="align-self-center"> - <ul class="nav nav-tabs " id="tablist" role="tablist"> + <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("user.index") }}" class="btn btn-secondary">Cancel</a> + {% if user.uid %} + <a href="{{ url_for("user.delete", uid=user.uid) }}" class="btn btn-danger"><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 %} + </div> + <ul class="nav nav-tabs pt-2 border-0" id="tablist" role="tablist"> <li class="nav-item"> <a class="nav-link active" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="true">Profile</a> </li> @@ -14,7 +23,7 @@ <a class="nav-link" id="ldif-tab" data-toggle="tab" href="#ldif" role="tab" aria-controls="ldif" aria-selected="false">LDIF</a> </li> </ul> - <div class="tab-content border border-top-0 mb-2 pt-2" id="tabcontent"> + <div class="tab-content border mb-2 pt-2" id="tabcontent"> <div class="tab-pane fade show active" id="profile" role="tabpanel" aria-labelledby="roles-tab"> <div class="form-group col"> <label for="user-uid">uid</label> @@ -60,6 +69,34 @@ <div class="tab-pane fade" id="roles" role="tabpanel" aria-labelledby="roles-tab"> <div class="form-group col"> <span>Roles:</span> + <table class="table table-striped table-sm"> + <thead> + <tr> + <th scope="col"></th> + <th scope="col">name</th> + <th scope="col">description</th> + </tr> + </thead> + <tbody> + {% for role in roles %} + <tr id="role-{{ role.id }}"> + <td> + <div class="form-check"> + <input class="form-check-input" type="checkbox" id="role-{{ role.id }}-checkbox" name="role-{{ role.id }}" value="1" aria-label="enabled" {% if user.dn in role.member_dns() %}checked{% endif %}> + </div> + </td> + <td> + <a href="{{ url_for("role.show", roleid=role.id) }}"> + {{ role.name }} + </a> + </td> + <td> + {{ role.description }} + </td> + </tr> + {% endfor %} + </tbody> + </table> </div> <div class="form-group col"> <span>Resulting groups (only updated after save):</span> @@ -93,16 +130,6 @@ </div> </div> </div> - - <div class="form-group col pl-0"> - <button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> Save</button> - <a href="{{ url_for("user.index") }}" class="btn btn-secondary">Cancel</a> - {% if user.uid %} - <a href="{{ url_for("user.delete", uid=user.uid) }}" class="btn btn-danger"><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 %} - </div> </div> </form> {% endblock %} diff --git a/uffd/user/templates/user_list.html b/uffd/user/templates/user_list.html index bd5766faacaa77f49f8577ae0071eebd473805f9..8bd88a19dd991393515d519904c40e7f2c396e20 100644 --- a/uffd/user/templates/user_list.html +++ b/uffd/user/templates/user_list.html @@ -3,7 +3,7 @@ {% block body %} <div class="row"> <div class="col"> - <table class="table table-striped"> + <table class="table table-striped table-sm"> <thead> <tr> <th scope="col">uid</th> diff --git a/uffd/user/views_user.py b/uffd/user/views_user.py index 08547f9bd83301cde36ad5996c1423e42cedcad9..879238298f4dbf1a4245d1251b22675bcd8db211 100644 --- a/uffd/user/views_user.py +++ b/uffd/user/views_user.py @@ -5,6 +5,8 @@ from uffd.csrf import csrf_protect from uffd.selfservice import send_passwordreset from uffd.ldap import get_conn, escape_filter_chars from uffd.session import login_required, is_valid_session, get_current_user +from uffd.role.models import Role +from uffd.database import db from .models import User @@ -41,7 +43,7 @@ def show(uid=None): assert len(conn.entries) == 1 user = User.from_ldap(conn.entries[0]) ldif = conn.entries[0].entry_to_ldif() - return render_template('user.html', user=user, user_ldif=ldif) + return render_template('user.html', user=user, user_ldif=ldif, roles=Role.query.all()) @bp.route("/<int:uid>/update", methods=['POST']) @bp.route("/new", methods=['POST']) @@ -60,23 +62,42 @@ def update(uid=False): user = User.from_ldap(conn.entries[0]) if not user.set_mail(request.form['mail']): flash('Mail is invalide.') - return redirect(url_for('.user_show')) + return redirect(url_for('user.show', uid=uid)) new_displayname = request.form['displayname'] if request.form['displayname'] else request.form['loginname'] if not user.set_displayname(new_displayname): flash('Display name does not meet requirements') - return redirect(url_for('user.show')) + return redirect(url_for('user.show', uid=uid)) new_password = request.form.get('password') if new_password and not is_newuser: user.set_password(new_password) + + session = db.session + roles = Role.query.all() + for role in roles: + role_member_dns = role.member_dns() + if request.values.get('role-{}'.format(role.id), False): + if user.dn in role_member_dns: + continue + role.add_member(user) + elif user.dn in role_member_dns: + role.del_member(user) + usergroups = set() + for role in Role.get_for_user(user).all(): + usergroups.update(role.group_dns()) + user.replace_group_dns(usergroups) + if user.to_ldap(new=is_newuser): if is_newuser: send_passwordreset(user.loginname) flash('User created. We sent the user a password reset link by mail') + session.commit() else: flash('User updated') + session.commit() else: flash('Error updating user: {}'.format(conn.result['message'])) - return redirect(url_for('user.index')) + session.rollback() + return redirect(url_for('user.show', uid=user.uid)) @bp.route("/<int:uid>/del") @csrf_protect(blueprint=bp)