Skip to content
Snippets Groups Projects
Verified Commit 8e73775e authored by nd's avatar nd
Browse files

add users to roles

parent 890d55db
Branches
No related tags found
No related merge requests found
...@@ -560,13 +560,13 @@ valid-metaclass-classmethod-first-arg=cls ...@@ -560,13 +560,13 @@ valid-metaclass-classmethod-first-arg=cls
max-args=7 max-args=7
# Maximum number of attributes for a class (see R0902). # 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). # Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5 max-bool-expr=5
# Maximum number of branch for function / method body. # Maximum number of branch for function / method body.
max-branches=12 max-branches=15
# Maximum number of locals for function / method body. # Maximum number of locals for function / method body.
max-locals=15 max-locals=15
......
from .views import bp as bp_ui from .views import bp as bp_ui
from .models import Role, RoleGroup, RoleUser
bp = [bp_ui] bp = [bp_ui]
...@@ -5,7 +5,7 @@ from sqlalchemy.orm import relationship ...@@ -5,7 +5,7 @@ from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from uffd.database import db from uffd.database import db
from uffd.user import User, Group from uffd.user.models import User, Group
class Role(db.Model): class Role(db.Model):
__tablename__ = 'role' __tablename__ = 'role'
...@@ -19,10 +19,31 @@ class Role(db.Model): ...@@ -19,10 +19,31 @@ class Role(db.Model):
self.name = name self.name = name
self.description = description self.description = description
def group_dns(self): @classmethod
return map(attrgetter('dn'), self.groups) def get_for_user(cls, user):
return Role.query.join(Role.members, aliased=True).filter_by(dn=user.dn)
def member_dns(self): 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(): class LdapMapping():
id = Column(Integer(), primary_key=True, autoincrement=True) id = Column(Integer(), primary_key=True, autoincrement=True)
...@@ -35,6 +56,10 @@ class LdapMapping(): ...@@ -35,6 +56,10 @@ class LdapMapping():
return Column(ForeignKey('role.id')) return Column(ForeignKey('role.id'))
ldapclass = None ldapclass = None
def __init__(self, dn='', role=''):
self.dn = dn
self.role = role
def get_ldap(self): def get_ldap(self):
return self.ldapclass.from_ldap_dn(self.dn) return self.ldapclass.from_ldap_dn(self.dn)
......
...@@ -2,8 +2,8 @@ from flask import Blueprint, render_template, request, url_for, redirect, flash, ...@@ -2,8 +2,8 @@ from flask import Blueprint, render_template, request, url_for, redirect, flash,
from uffd.navbar import register_navbar from uffd.navbar import register_navbar
from uffd.csrf import csrf_protect from uffd.csrf import csrf_protect
from uffd.role.models import Role, RoleGroup from uffd.role.models import Role
from uffd.user import Group from uffd.user.models import Group
from uffd.session import get_current_user, login_required, is_valid_session from uffd.session import get_current_user, login_required, is_valid_session
from uffd.database import db from uffd.database import db
...@@ -48,17 +48,19 @@ def update(roleid=False): ...@@ -48,17 +48,19 @@ def update(roleid=False):
role.description = request.values['description'] role.description = request.values['description']
groups = Group.from_ldap_all() groups = Group.from_ldap_all()
role_group_dns = list(role.group_dns()) role_group_dns = role.group_dns()
for group in groups: for group in groups:
if request.values.get('group-{}'.format(group.gid), False): if request.values.get('group-{}'.format(group.gid), False):
if group.dn in role_group_dns: if group.dn in role_group_dns:
continue continue
newmapping = RoleGroup() role.add_group(group)
newmapping.dn = group.dn
newmapping.role = role
session.add(newmapping)
elif group.dn in role_group_dns: 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() session.commit()
return redirect(url_for('role.index')) return redirect(url_for('role.index'))
......
from .views_user import bp as bp_user from .views_user import bp as bp_user
from .views_group import bp as bp_group from .views_group import bp as bp_group
from .models import User, Group
bp = [bp_user, bp_group] bp = [bp_user, bp_group]
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 ldap3.utils.hashed import hashed
from flask import current_app from flask import current_app
from uffd import ldap from uffd import ldap
class User(): 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.uid = uid
self.loginname = loginname self.loginname = loginname
self.displayname = displayname self.displayname = displayname
self.mail = mail self.mail = mail
self.newpassword = None
self.dn = dn
self.groups_ldap = groups or [] self.groups_ldap = groups or []
self.initial_groups_ldap = groups or []
self.groups_changed = False
self._groups = None self._groups = None
self.newpassword = None
@classmethod @classmethod
def from_ldap(cls, ldapobject): def from_ldap(cls, ldapobject):
...@@ -21,7 +27,8 @@ class User(): ...@@ -21,7 +27,8 @@ class User():
loginname=ldapobject['uid'].value, loginname=ldapobject['uid'].value,
displayname=ldapobject['cn'].value, displayname=ldapobject['cn'].value,
mail=ldapobject['mail'].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 @classmethod
...@@ -35,11 +42,13 @@ class User(): ...@@ -35,11 +42,13 @@ class User():
def to_ldap(self, new=False): def to_ldap(self, new=False):
conn = ldap.get_conn() conn = ldap.get_conn()
if new: if new:
self.uid = ldap.get_next_uid()
attributes = { attributes = {
'uidNumber': ldap.get_next_uid(), 'uidNumber': self.uid,
'gidNumber': current_app.config['LDAP_USER_GID'], 'gidNumber': current_app.config['LDAP_USER_GID'],
'homeDirectory': '/home/'+self.loginname, 'homeDirectory': '/home/'+self.loginname,
'sn': ' ', 'sn': ' ',
'userPassword': hashed(HASHED_SALTED_SHA512, secrets.token_hex(128)),
# same as for update # same as for update
'givenName': self.displayname, 'givenName': self.displayname,
'displayName': self.displayname, 'displayName': self.displayname,
...@@ -59,6 +68,17 @@ class User(): ...@@ -59,6 +68,17 @@ class User():
attributes['userPassword'] = [(MODIFY_REPLACE, [hashed(HASHED_SALTED_SHA512, self.newpassword)])] attributes['userPassword'] = [(MODIFY_REPLACE, [hashed(HASHED_SALTED_SHA512, self.newpassword)])]
dn = ldap.uid_to_dn(self.uid) dn = ldap.uid_to_dn(self.uid)
result = conn.modify(dn, attributes) 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 return result
def get_groups(self): def get_groups(self):
...@@ -71,6 +91,10 @@ class User(): ...@@ -71,6 +91,10 @@ class User():
groups.append(newgroup) groups.append(newgroup)
self._groups = groups self._groups = groups
return 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): def is_in_group(self, name):
if not name: if not name:
......
...@@ -3,7 +3,16 @@ ...@@ -3,7 +3,16 @@
{% block body %} {% block body %}
<form action="{{ url_for("user.update", uid=user.uid) }}" method="POST"> <form action="{{ url_for("user.update", uid=user.uid) }}" method="POST">
<div class="align-self-center"> <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"> <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> <a class="nav-link active" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="true">Profile</a>
</li> </li>
...@@ -14,7 +23,7 @@ ...@@ -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> <a class="nav-link" id="ldif-tab" data-toggle="tab" href="#ldif" role="tab" aria-controls="ldif" aria-selected="false">LDIF</a>
</li> </li>
</ul> </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="tab-pane fade show active" id="profile" role="tabpanel" aria-labelledby="roles-tab">
<div class="form-group col"> <div class="form-group col">
<label for="user-uid">uid</label> <label for="user-uid">uid</label>
...@@ -60,6 +69,34 @@ ...@@ -60,6 +69,34 @@
<div class="tab-pane fade" id="roles" role="tabpanel" aria-labelledby="roles-tab"> <div class="tab-pane fade" id="roles" role="tabpanel" aria-labelledby="roles-tab">
<div class="form-group col"> <div class="form-group col">
<span>Roles:</span> <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>
<div class="form-group col"> <div class="form-group col">
<span>Resulting groups (only updated after save):</span> <span>Resulting groups (only updated after save):</span>
...@@ -93,16 +130,6 @@ ...@@ -93,16 +130,6 @@
</div> </div>
</div> </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> </div>
</form> </form>
{% endblock %} {% endblock %}
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
{% block body %} {% block body %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table class="table table-striped"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">uid</th> <th scope="col">uid</th>
......
...@@ -5,6 +5,8 @@ from uffd.csrf import csrf_protect ...@@ -5,6 +5,8 @@ from uffd.csrf import csrf_protect
from uffd.selfservice import send_passwordreset from uffd.selfservice import send_passwordreset
from uffd.ldap import get_conn, escape_filter_chars from uffd.ldap import get_conn, escape_filter_chars
from uffd.session import login_required, is_valid_session, get_current_user 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 from .models import User
...@@ -41,7 +43,7 @@ def show(uid=None): ...@@ -41,7 +43,7 @@ def show(uid=None):
assert len(conn.entries) == 1 assert len(conn.entries) == 1
user = User.from_ldap(conn.entries[0]) user = User.from_ldap(conn.entries[0])
ldif = conn.entries[0].entry_to_ldif() 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("/<int:uid>/update", methods=['POST'])
@bp.route("/new", methods=['POST']) @bp.route("/new", methods=['POST'])
...@@ -60,23 +62,42 @@ def update(uid=False): ...@@ -60,23 +62,42 @@ def update(uid=False):
user = User.from_ldap(conn.entries[0]) user = User.from_ldap(conn.entries[0])
if not user.set_mail(request.form['mail']): if not user.set_mail(request.form['mail']):
flash('Mail is invalide.') 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'] new_displayname = request.form['displayname'] if request.form['displayname'] else request.form['loginname']
if not user.set_displayname(new_displayname): if not user.set_displayname(new_displayname):
flash('Display name does not meet requirements') 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') new_password = request.form.get('password')
if new_password and not is_newuser: if new_password and not is_newuser:
user.set_password(new_password) 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 user.to_ldap(new=is_newuser):
if is_newuser: if is_newuser:
send_passwordreset(user.loginname) send_passwordreset(user.loginname)
flash('User created. We sent the user a password reset link by mail') flash('User created. We sent the user a password reset link by mail')
session.commit()
else: else:
flash('User updated') flash('User updated')
session.commit()
else: else:
flash('Error updating user: {}'.format(conn.result['message'])) 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") @bp.route("/<int:uid>/del")
@csrf_protect(blueprint=bp) @csrf_protect(blueprint=bp)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment