diff --git a/uffd/__init__.py b/uffd/__init__.py
index c14c1bd82e06b870ddd2d12a82cf6e01992f6c2d..521aba6287d26343744b0d4b0d8d7e97b2b3d470 100644
--- a/uffd/__init__.py
+++ b/uffd/__init__.py
@@ -39,10 +39,10 @@ def create_app(test_config=None):
 
 	db.init_app(app)
 	# pylint: disable=C0415
-	from uffd import user, selfservice, session, csrf, ldap
+	from uffd import user, selfservice, role, session, csrf, ldap
 	# pylint: enable=C0415
 
-	for i in user.bp + selfservice.bp + session.bp + csrf.bp + ldap.bp:
+	for i in user.bp + selfservice.bp + role.bp + session.bp + csrf.bp + ldap.bp:
 		app.register_blueprint(i)
 
 	@app.route("/")
diff --git a/uffd/role/__init__.py b/uffd/role/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..671578662f91c82cb987ffe679c1f102dc493d1f
--- /dev/null
+++ b/uffd/role/__init__.py
@@ -0,0 +1,3 @@
+from .views import bp as bp_ui
+
+bp = [bp_ui]
diff --git a/uffd/role/models.py b/uffd/role/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..f91d9cc2ef98a44684361fe4ebb0bcac0da73199
--- /dev/null
+++ b/uffd/role/models.py
@@ -0,0 +1,41 @@
+from sqlalchemy import Column, String, Integer, Text, ForeignKey
+from sqlalchemy.orm import relationship
+from sqlalchemy.ext.declarative import declared_attr
+
+from uffd.database import db
+from uffd.user.models import User, Group
+
+class Role(db.Model):
+	__tablename__ = 'role'
+	id = Column(Integer(), primary_key=True, autoincrement=True)
+	name = Column(String(32), unique=True)
+	description = Column(Text())
+	members = relationship("RoleUser", backref="role")
+	groups = relationship("RoleGroup", backref="role")
+
+	def __init__(self, name='', description=''):
+		self.name = name
+		self.description = description
+
+class LdapMapping():
+	id = Column(Integer(), primary_key=True, autoincrement=True)
+	dn = Column(String(128))
+	@declared_attr
+	def role_id(cls):
+		return Column(ForeignKey('role.id'))
+	ldapclass = None
+
+	def get_ldap(self):
+		return self.ldapclass.from_ldap_dn(dn)
+
+	def set_ldap(self, value):
+		self.dn = value['dn']
+
+class RoleGroup(LdapMapping, db.Model):
+	__tablename__ = 'role-group'
+	ldapclass = User
+
+class RoleUser(LdapMapping, db.Model):
+	__tablename__ = 'role-user'
+	ldapclass = Group
+
diff --git a/uffd/role/templates/role.html b/uffd/role/templates/role.html
new file mode 100644
index 0000000000000000000000000000000000000000..45672dafdedb5d0e451ec9e13d60b923f3c51487
--- /dev/null
+++ b/uffd/role/templates/role.html
@@ -0,0 +1,30 @@
+{% extends 'base.html' %}
+
+{% block body %}
+<form action="{{ url_for("role.update", roleid=role.id) }}" method="POST">
+<div class="align-self-center">
+	<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 }}">
+		<small class="form-text text-muted">
+		</small>
+	</div>
+	<div class="form-group col">
+		<label for="role-description">Description</label>
+		<textarea class="form-control" id="role-description" name="description" rows="5">{{ role.description }}</textarea>
+		<small class="form-text text-muted">
+		</small>
+	</div>
+
+	<div class="form-group col">
+		<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) }}" 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/role/templates/role_list.html b/uffd/role/templates/role_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..4cfcb60d745323e5d11a1f6e1d09fe363e4ff08e
--- /dev/null
+++ b/uffd/role/templates/role_list.html
@@ -0,0 +1,42 @@
+{% extends 'base.html' %}
+
+{% block body %}
+<div class="row">
+	<div class="col">
+		<table class="table table-striped">
+			<thead>
+				<tr>
+					<th scope="col">name</th>
+					<th scope="col">description</th>
+					<th scope="col">
+						<p class="text-right">
+							<a type="button" class="btn btn-primary" href="{{ url_for("role.show") }}">
+								<i class="fa fa-plus" aria-hidden="true"></i> New
+							</a>
+						</p>
+					</th>
+				</tr>
+			</thead>
+			<tbody>
+				{% for role in roles|sort(attribute="name") %}
+				<tr id="role-{{ role.id }}">
+					<th scope="row">
+						{{ role.name }}
+					</th>
+					<td>
+						{{ role.description }}
+					</td>
+					<td>
+						<p class="text-right">
+							<a href="{{ url_for("role.show", roleid=role.id) }}" class="btn btn-primary">
+								<i class="fa fa-edit" aria-hidden="true"></i> Edit
+							</a>
+						</p>
+					</td>
+				</tr>
+				{% endfor %}
+			</tbody>
+		</table>
+	</div>
+</dev>
+{% endblock %}
diff --git a/uffd/role/views.py b/uffd/role/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d4a28c8924d5669e81dc2c2b6182115a3d918ba
--- /dev/null
+++ b/uffd/role/views.py
@@ -0,0 +1,60 @@
+from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app
+
+from uffd.navbar import register_navbar
+from uffd.csrf import csrf_protect
+from uffd.user.models import User, Group
+from uffd.role.models import Role
+from uffd.session import get_current_user, login_required, is_valid_session
+from uffd.ldap import loginname_to_dn
+from uffd.database import db
+
+bp = Blueprint("role", __name__, template_folder='templates', url_prefix='/role/')
+@bp.before_request
+@login_required()
+def role_acl(): #pylint: disable=inconsistent-return-statements
+	if not role_acl_check():
+		flash('Access denied')
+		return redirect(url_for('index'))
+
+def role_acl_check():
+	return is_valid_session() and get_current_user().is_in_group(current_app.config['ACL_ADMIN_GROUP'])
+
+@bp.route("/")
+@register_navbar('Roles', icon='key', blueprint=bp, visible=role_acl_check)
+def index():
+	return render_template('role_list.html', roles=Role.query.all())
+
+@bp.route("/<int:roleid>")
+@bp.route("/new")
+def show(roleid=False):
+	if not roleid:
+		role = Role()
+	else:
+		role = Role.query.get_or_404(roleid)
+	return render_template('role.html', role=role)
+
+@bp.route("/<int:roleid>/update", methods=['POST'])
+@bp.route("/new", methods=['POST'])
+@csrf_protect(blueprint=bp)
+def update(roleid=False):
+	is_newrole = bool(not roleid)
+	session = db.session
+	if is_newrole:
+		role = Role()
+		session.add(role)
+	else:
+		role = session.query(Role).get_or_404(roleid)
+	role.name = request.values['name']
+	role.description = request.values['description']
+	print(role)
+	session.commit()
+	return redirect(url_for('role.index'))
+
+@bp.route("/<int:roleid>/del")
+@csrf_protect(blueprint=bp)
+def delete(roleid):
+	session = db.session
+	role = session.query(Role).get_or_404(roleid)
+	session.delete(role)
+	session.commit()
+	return redirect(url_for('role.index'))