diff --git a/app/api/main.py b/app/api/main.py
index 1d3e9e86f2a9d780fa8b328dcfd9667b0ab71ef2..07655356ad1a5a4225c97299c44010226304755d 100644
--- a/app/api/main.py
+++ b/app/api/main.py
@@ -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)
diff --git a/app/decorators.py b/app/decorators.py
new file mode 100644
index 0000000000000000000000000000000000000000..18c31589676b966c579b2ef9eb0e944ab8393be0
--- /dev/null
+++ b/app/decorators.py
@@ -0,0 +1,70 @@
+# -*- 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
diff --git a/app/models.py b/app/models.py
index 3309c008eb30cdedc611fff4191ae08ccf89c224..40680d58b15f8e6d09009eb05d09ddf8214d0cae 100644
--- a/app/models.py
+++ b/app/models.py
@@ -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
diff --git a/app/settings.py b/app/settings.py
index 546323029590d5a8892339248bc5a443addc0b9b..b9a4349ce078a43613246c60b2d6ffdfbf49cac5 100644
--- a/app/settings.py
+++ b/app/settings.py
@@ -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',
+}