Skip to content
Snippets Groups Projects
Commit 90184b32 authored by Benjamin Bertrand's avatar Benjamin Bertrand
Browse files

Replace roles with LDAP groups

parent 8770dec7
No related branches found
No related tags found
No related merge requests found
...@@ -14,7 +14,7 @@ from flask import (current_app, Blueprint, jsonify, request) ...@@ -14,7 +14,7 @@ from flask import (current_app, Blueprint, jsonify, request)
from flask_jwt_extended import create_access_token, jwt_required from flask_jwt_extended import create_access_token, jwt_required
from flask_ldap3_login import AuthenticationResponseStatus from flask_ldap3_login import AuthenticationResponseStatus
from ..extensions import ldap_manager, db from ..extensions import ldap_manager, db
from ..models import User, Item, Manufacturer, Model, Location, Status from ..models import Item, Manufacturer, Model, Location, Status
from .. import utils from .. import utils
bp = Blueprint('api', __name__) bp = Blueprint('api', __name__)
...@@ -61,12 +61,11 @@ def login(): ...@@ -61,12 +61,11 @@ def login():
response = ldap_manager.authenticate(username, password) response = ldap_manager.authenticate(username, password)
if response.status == AuthenticationResponseStatus.success: if response.status == AuthenticationResponseStatus.success:
current_app.logger.debug(f'{username} successfully logged in') current_app.logger.debug(f'{username} successfully logged in')
user = User.query.filter_by(username=username).first() user = ldap_manager._save_user(
if user is None: response.user_dn,
# LDAP user not in database yet - create it response.user_id,
user = User(username, name=response.user_info['cn'], email=response.user_info['mail']) response.user_info,
db.session.add(user) response.user_groups)
db.session.commit()
payload = {'access_token': create_access_token(identity=user.id)} payload = {'access_token': create_access_token(identity=user.id)}
return jsonify(payload), 200 return jsonify(payload), 200
raise utils.InventoryError('Invalid credentials', status_code=401) raise utils.InventoryError('Invalid credentials', status_code=401)
......
...@@ -13,9 +13,6 @@ from . import models ...@@ -13,9 +13,6 @@ from . import models
defaults = [ defaults = [
models.Role(id=0, name='admin'),
models.Role(id=1, name='user'),
models.Location(name='ICS lab'), models.Location(name='ICS lab'),
models.Location(name='Utgård'), models.Location(name='Utgård'),
models.Location(name='Site'), models.Location(name='Site'),
......
...@@ -13,7 +13,7 @@ import sqlalchemy as sa ...@@ -13,7 +13,7 @@ import sqlalchemy as sa
from flask import Flask from flask import Flask
from . import settings from . import settings
from .extensions import db, migrate, login_manager, ldap_manager, bootstrap, admin, mail, jwt from .extensions import db, migrate, login_manager, ldap_manager, bootstrap, admin, mail, jwt
from .models import User, Role, Action, Manufacturer, Model, Location, Status from .models import User, Group, Action, Manufacturer, Model, Location, Status
from .admin.views import AdminModelView, ItemAdmin from .admin.views import AdminModelView, ItemAdmin
from .main.views import bp as main from .main.views import bp as main
from .users.views import bp as users from .users.views import bp as users
...@@ -89,7 +89,7 @@ def create_app(): ...@@ -89,7 +89,7 @@ def create_app():
jwt.init_app(app) jwt.init_app(app)
admin.init_app(app) admin.init_app(app)
admin.add_view(AdminModelView(Role, db.session)) admin.add_view(AdminModelView(Group, db.session))
admin.add_view(AdminModelView(User, db.session)) admin.add_view(AdminModelView(User, db.session))
admin.add_view(AdminModelView(Action, db.session)) admin.add_view(AdminModelView(Action, db.session))
admin.add_view(AdminModelView(Manufacturer, db.session)) admin.add_view(AdminModelView(Manufacturer, db.session))
......
...@@ -13,7 +13,9 @@ import uuid ...@@ -13,7 +13,9 @@ import uuid
import qrcode import qrcode
from sqlalchemy.types import TypeDecorator, CHAR from sqlalchemy.types import TypeDecorator, CHAR
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.associationproxy import association_proxy
from citext import CIText from citext import CIText
from flask import current_app
from flask_login import UserMixin from flask_login import UserMixin
from .extensions import db, login_manager, ldap_manager from .extensions import db, login_manager, ldap_manager
from . import utils from . import utils
...@@ -73,26 +75,41 @@ def save_user(dn, username, data, memberships): ...@@ -73,26 +75,41 @@ def save_user(dn, username, data, memberships):
""" """
user = User.query.filter_by(username=username).first() user = User.query.filter_by(username=username).first()
if user is None: if user is None:
user = User(username, name=data['cn'], email=data['mail']) user = User(username,
db.session.add(user) name=utils.attribute_to_string(data['cn']),
db.session.commit() email=utils.attribute_to_string(data['mail']))
else: # Always update the user groups to keep them up-to-date
pass user.groups = [utils.attribute_to_string(group['cn']) for group in memberships]
# TODO: update the user in the database? db.session.add(user)
# probably not needed for the name and email fields db.session.commit()
# maybe when we add groups from LDAP
return user return user
class Role(db.Model): # Table required for Many-to-Many relationships between users and groups
usergroups_table = db.Table(
'usergroups',
db.Column('user_id', db.Integer, db.ForeignKey('user_account.id')),
db.Column('group_id', db.Integer, db.ForeignKey('group.id'))
)
class Group(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True) name = db.Column(db.String(100), nullable=False, unique=True)
users = db.relationship('User', backref='role')
def __init__(self, name):
self.name = name
def __str__(self): def __str__(self):
return self.name return self.name
def find_or_create_group(name):
"""Return the existing group or a newly created one"""
group = Group.query.filter_by(name=name).first()
return group or Group(name=name)
class User(db.Model, UserMixin): class User(db.Model, UserMixin):
# "user" is a reserved word in postgresql # "user" is a reserved word in postgresql
# so let's use another name # so let's use another name
...@@ -102,11 +119,15 @@ class User(db.Model, UserMixin): ...@@ -102,11 +119,15 @@ class User(db.Model, UserMixin):
username = db.Column(db.String(50), unique=True) username = db.Column(db.String(50), unique=True)
name = db.Column(db.String(100)) name = db.Column(db.String(100))
email = db.Column(db.String(100)) email = db.Column(db.String(100))
role_id = db.Column(db.Integer, db.ForeignKey('role.id')) grp = db.relationship('Group', secondary=usergroups_table,
backref=db.backref('members', lazy='dynamic'))
def __init__(self, username, name, email, role='user'): # Proxy the 'name' attribute from the 'grp' relationship
# See http://docs.sqlalchemy.org/en/latest/orm/extensions/associationproxy.html
groups = association_proxy('grp', 'name',
creator=find_or_create_group)
def __init__(self, username, name, email):
self.username = username self.username = username
self.role = Role.query.filter_by(name=role).first()
self.name = name self.name = name
self.email = email self.email = email
...@@ -119,7 +140,7 @@ class User(db.Model, UserMixin): ...@@ -119,7 +140,7 @@ class User(db.Model, UserMixin):
@property @property
def is_admin(self): def is_admin(self):
return self.role.name == 'admin' return current_app.config['INVENTORY_ADMIN_GROUP'] in self.groups
def __str__(self): def __str__(self):
return self.name return self.name
...@@ -129,6 +150,9 @@ class QRCodeMixin: ...@@ -129,6 +150,9 @@ class QRCodeMixin:
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(CIText, nullable=False, unique=True) name = db.Column(CIText, nullable=False, unique=True)
def __init__(self, name=None):
self.name = name
def image(self): def image(self):
"""Return a QRCode image to identify a record """Return a QRCode image to identify a record
......
...@@ -21,11 +21,20 @@ ADMIN_EMAILS = ['admin@example.com'] ...@@ -21,11 +21,20 @@ ADMIN_EMAILS = ['admin@example.com']
EMAIL_SENDER = 'inventory@esss.se' EMAIL_SENDER = 'inventory@esss.se'
LDAP_HOST = 'esss.lu.se' LDAP_HOST = 'esss.lu.se'
LDAP_BASE_DN = 'OU=ESS Users,DC=esss,DC=lu,DC=se' LDAP_BASE_DN = 'DC=esss,DC=lu,DC=se'
LDAP_USER_DN = 'OU=ESS Users'
LDAP_GROUP_DN = ''
LDAP_BIND_USER_DN = 'ldapuser' LDAP_BIND_USER_DN = 'ldapuser'
LDAP_BIND_USER_PASSWORD = 'secret' LDAP_BIND_USER_PASSWORD = 'secret'
LDAP_USER_RDN_ATTR = 'cn' LDAP_USER_RDN_ATTR = 'cn'
LDAP_USER_LOGIN_ATTR = 'sAMAccountName' LDAP_USER_LOGIN_ATTR = 'sAMAccountName'
LDAP_ALWAYS_SEARCH_BIND = True LDAP_ALWAYS_SEARCH_BIND = True
LDAP_USER_OBJECT_FILTER = '(samAccountType=805306368)' LDAP_USER_OBJECT_FILTER = '(samAccountType=805306368)'
LDAP_GROUP_OBJECT_FILTER = ''
LDAP_USER_SEARCH_SCOPE = 'SUBTREE' LDAP_USER_SEARCH_SCOPE = 'SUBTREE'
LDAP_GROUP_SEARCH_SCOPE = 'SUBTREE'
LDAP_GROUP_MEMBERS_ATTR = 'member'
LDAP_GET_USER_ATTRIBUTES = ['cn', 'sAMAccountName', 'mail', 'memberOf']
LDAP_GET_GROUP_ATTRIBUTES = ['cn']
INVENTORY_ADMIN_GROUP = 'ICS Control System Infrastructure group'
...@@ -12,8 +12,8 @@ ...@@ -12,8 +12,8 @@
<dd>{{user.name}}</dd> <dd>{{user.name}}</dd>
<dt>Email</dt> <dt>Email</dt>
<dd>{{user.email}}</dd> <dd>{{user.email}}</dd>
<dt>Role</dt> <dt>Groups</dt>
<dd>{{ user.role }}</dd> <dd>{{ user.groups | join(', ') }}</dd>
{% if token %} {% if token %}
<dt>Token</dt> <dt>Token</dt>
<dd>{{ token }}</dd> <dd>{{ token }}</dd>
......
...@@ -83,3 +83,21 @@ def convert_to_model(item, model): ...@@ -83,3 +83,21 @@ def convert_to_model(item, model):
raise InventoryError(f'{item} is not a valid {model.__name__.lower()}') raise InventoryError(f'{item} is not a valid {model.__name__.lower()}')
return instance return instance
return item return item
def attribute_to_string(value):
"""Return the attribute as a string
If the attribute is defined in the schema as multi valued
then the attribute value is returned as a list
See http://ldap3.readthedocs.io/tutorial_searches.html#entries-retrieval
This function returns the first item of the list if it's a list
:param value: string or list
:returns: string
"""
if isinstance(value, list):
return value[0]
else:
return value
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment