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

Add decorators to protect routes

parent 062a0395
No related branches found
No related tags found
No related merge requests found
......@@ -16,6 +16,7 @@ from flask_ldap3_login import AuthenticationResponseStatus
from ..extensions import ldap_manager, db
from ..models import Item, Manufacturer, Model, Location, Status
from .. import utils
from ..decorators import jwt_groups_accepted
bp = Blueprint('api', __name__)
......@@ -82,12 +83,14 @@ def get_items():
@bp.route('/items', methods=['POST'])
@jwt_required
@jwt_groups_accepted('admin', 'create')
def create_item():
return create_generic_model(Item, mandatory_field='serial_number')
@bp.route('/items/<item_id>', methods=['PATCH'])
@jwt_required
@jwt_groups_accepted('admin', 'create')
def patch_item(item_id):
data = request.get_json()
if data is None:
......@@ -112,6 +115,7 @@ def get_manufacturers():
@bp.route('/manufacturers', methods=['POST'])
@jwt_required
@jwt_groups_accepted('admin', 'create')
def create_manufacturer():
return create_generic_model(Manufacturer)
......@@ -124,6 +128,7 @@ def get_models():
@bp.route('/models', methods=['POST'])
@jwt_required
@jwt_groups_accepted('admin', 'create')
def create_model():
return create_generic_model(Model)
......@@ -136,6 +141,7 @@ def get_locations():
@bp.route('/locations', methods=['POST'])
@jwt_required
@jwt_groups_accepted('admin', 'create')
def create_locations():
return create_generic_model(Location)
......@@ -148,5 +154,6 @@ def get_status():
@bp.route('/status', methods=['POST'])
@jwt_required
@jwt_groups_accepted('admin', 'create')
def create_status():
return create_generic_model(Status)
# -*- coding: utf-8 -*-
"""
app.decorators
~~~~~~~~~~~~~~
This module defines some useful decorators.
:copyright: (c) 2017 European Spallation Source ERIC
:license: BSD 2-Clause, see LICENSE for more details.
"""
from functools import wraps
from flask_jwt_extended import get_current_user
from .utils import InventoryError
def jwt_groups_required(*groups):
"""Decorator which specifies that a user must have all the specified groups.
Example::
@bp.route('/models', methods=['POST'])
@jwt_required
@jwt_groups_required('admin', 'create')
def create_model():
return create()
The current user must be in both 'admin' and 'create' groups
to access this route.
:param groups: required groups
"""
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
user = get_current_user()
if user is None:
raise InventoryError('Invalid indentity', status_code=403)
if not user.is_member_of_all_groups(groups):
raise InventoryError("User doesn't have the required group(s)", status_code=403)
return fn(*args, **kwargs)
return decorated_view
return wrapper
def jwt_groups_accepted(*groups):
"""Decorator which specifies that a user must have at least one of the specified groups.
Example::
@bp.route('/models', methods=['POST'])
@jwt_required
@jwt_groups_accepted('admin', 'create')
def create_model():
return create()
The current user must be in either 'admin' or 'create' group
to access this route.
:param groups: accepted groups
"""
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
user = get_current_user()
if user is None:
raise InventoryError('Invalid indentity', status_code=403)
if not user.is_member_of_one_group(groups):
raise InventoryError("User doesn't have the required group", status_code=403)
return fn(*args, **kwargs)
return decorated_view
return wrapper
......@@ -17,7 +17,7 @@ from sqlalchemy.ext.associationproxy import association_proxy
from citext import CIText
from flask import current_app
from flask_login import UserMixin
from .extensions import db, login_manager, ldap_manager
from .extensions import db, login_manager, ldap_manager, jwt
from . import utils
......@@ -61,11 +61,21 @@ def load_user(user_id):
"""User loader callback for flask-login
:param str user_id: unicode ID of a user
:returns: corresponding user object
:returns: corresponding user object or None
"""
return User.query.get(int(user_id))
@jwt.user_loader_callback_loader
def user_loader_callback(identity):
"""User loader callback for flask-jwt-extended
:param str identity: identity from the token (user_id)
:returns: corresponding user object or None
"""
return User.query.get(int(identity))
@ldap_manager.save_user
def save_user(dn, username, data, memberships):
"""User saver for flask-ldap3-login
......@@ -140,7 +150,17 @@ class User(db.Model, UserMixin):
@property
def is_admin(self):
return current_app.config['INVENTORY_ADMIN_GROUP'] in self.groups
return current_app.config['INVENTORY_LDAP_GROUPS']['admin'] in self.groups
def is_member_of_one_group(self, groups):
"""Return True if the user is at least member of one of the given groups"""
names = [current_app.config['INVENTORY_LDAP_GROUPS'].get(group) for group in groups]
return bool(set(self.groups) & set(names))
def is_member_of_all_groups(self, groups):
"""Return True if the user is member of all the given groups"""
names = [current_app.config['INVENTORY_LDAP_GROUPS'].get(group) for group in groups]
return set(names).issubset(self.groups)
def __str__(self):
return self.name
......
......@@ -34,7 +34,10 @@ LDAP_GROUP_OBJECT_FILTER = ''
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_USER_ATTRIBUTES = ['cn', 'sAMAccountName', 'mail']
LDAP_GET_GROUP_ATTRIBUTES = ['cn']
INVENTORY_ADMIN_GROUP = 'ICS Control System Infrastructure group'
INVENTORY_LDAP_GROUPS = {
'admin': 'ICS Control System Infrastructure group',
'create': 'ICS Employees',
}
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