Using capabilities instead of roles for access control.

This commit is contained in:
2017-08-24 14:21:33 +02:00
parent c4071c1e03
commit c7b83d2d8b
5 changed files with 29 additions and 33 deletions

View File

@@ -15,15 +15,20 @@ import attract.shots_and_assets
EXTENSION_NAME = 'attract' EXTENSION_NAME = 'attract'
# Roles required to view task or shot details.
ROLES_REQUIRED_TO_VIEW_ITEMS = {'demo', 'subscriber', 'admin'}
class AttractExtension(PillarExtension): class AttractExtension(PillarExtension):
has_project_settings = True has_project_settings = True
user_roles = {'attract-user'} user_roles = {'attract-user'}
user_roles_indexable = {'attract-user'} user_roles_indexable = {'attract-user'}
user_caps = {
'attract-user': {'attract-view', 'attract-use'},
'org-attract': {'attract-view', 'attract-use'},
'subscriber': {'attract-view'},
'demo': {'attract-view'},
'admin': {'attract-view', 'attract-use'},
}
def __init__(self): def __init__(self):
self._log = logging.getLogger('%s.AttractExtension' % __name__) self._log = logging.getLogger('%s.AttractExtension' % __name__)
self.task_manager = attract.tasks.TaskManager() self.task_manager = attract.tasks.TaskManager()

View File

@@ -6,10 +6,6 @@ import bson
from pillar import attrs_extra from pillar import attrs_extra
# Having either of these roles is minimum requirement for using Attract.
ROLES_REQUIRED_TO_USE_ATTRACT = {'attract-user', 'admin'}
ROLES_REQUIRED_TO_VIEW_ATTRACT = {'admin', 'subscriber', 'demo'}
# Having any of these methods on a project means you can use Attract. # Having any of these methods on a project means you can use Attract.
# Prerequisite: the project is set up for Attract and has a Manager assigned to it. # Prerequisite: the project is set up for Attract and has a Manager assigned to it.
PROJECT_METHODS_TO_USE_ATTRACT = {'PUT'} PROJECT_METHODS_TO_USE_ATTRACT = {'PUT'}
@@ -20,10 +16,10 @@ class Actions(enum.Enum):
USE = 'use' USE = 'use'
# Required roles for a given action. # Required capability for a given action.
req_roles = { req_cap = {
Actions.VIEW: ROLES_REQUIRED_TO_VIEW_ATTRACT, Actions.VIEW: 'attract-view',
Actions.USE: ROLES_REQUIRED_TO_USE_ATTRACT, Actions.USE: 'attract-use',
} }
@@ -45,21 +41,19 @@ class Auth(object):
"""Returns True iff the user has Attract User role.""" """Returns True iff the user has Attract User role."""
from pillar import current_app from pillar import current_app
from pillar.auth import UserClass
assert isinstance(user_id, bson.ObjectId) assert isinstance(user_id, bson.ObjectId)
# TODO: move role checking code to Pillar. # TODO: move role checking code to Pillar.
users_coll = current_app.db('users') users_coll = current_app.db('users')
user = users_coll.find_one({'_id': user_id}, {'roles': 1}) db_user = users_coll.find_one({'_id': user_id}, {'roles': 1})
if not user: if not db_user:
self._log.debug('user_is_attract_user: User %s not found', user_id) self._log.debug('user_is_attract_user: User %s not found', user_id)
return False return False
user_roles = set(user.get('roles', [])) user = UserClass.construct('', db_user)
require_roles = set(ROLES_REQUIRED_TO_USE_ATTRACT) return user.has_cap('attract-use')
intersection = require_roles.intersection(user_roles)
return bool(intersection)
def current_user_may(self, action: Actions, project_id: bson.ObjectId = None) -> bool: def current_user_may(self, action: Actions, project_id: bson.ObjectId = None) -> bool:
"""Returns True iff the user is authorised to use/view Attract on the current project. """Returns True iff the user is authorised to use/view Attract on the current project.
@@ -86,20 +80,18 @@ class Auth(object):
g.attract_rights is a frozenset that contains zero or more Actions. g.attract_rights is a frozenset that contains zero or more Actions.
""" """
from pillar.api.utils.authorization import user_matches_roles from pillar.auth import current_user
from pillar.api.utils.authentication import current_user_id
from pillar.api.projects.utils import user_rights_in_project from pillar.api.projects.utils import user_rights_in_project
user_id = current_user_id() if current_user.is_anonymous:
if not user_id:
self._log.debug('Anonymous user never has access to Attract.') self._log.debug('Anonymous user never has access to Attract.')
flask.g.attract_rights = frozenset() flask.g.attract_rights = frozenset()
return return
rights = set() rights = set()
for action in Actions: for action in Actions:
roles = req_roles[action] cap = req_cap[action]
if user_matches_roles(roles): if current_user.has_cap(cap):
rights.add(action) rights.add(action)
# TODO Sybren: possibly split this up into a manager-fetching func + authorisation func. # TODO Sybren: possibly split this up into a manager-fetching func + authorisation func.

View File

@@ -5,7 +5,7 @@ from flask import Blueprint, render_template, redirect, url_for, request, jsonif
import flask_login import flask_login
import werkzeug.exceptions as wz_exceptions import werkzeug.exceptions as wz_exceptions
from pillar.auth import current_web_user as current_user from pillar.auth import current_user as current_user
from pillar.api.utils import str2id from pillar.api.utils import str2id
from pillar.web.utils import attach_project_pictures from pillar.web.utils import attach_project_pictures
import pillar.web.subquery import pillar.web.subquery

View File

@@ -1,14 +1,14 @@
import logging import logging
import flask import flask
import flask_login
import werkzeug.exceptions as wz_exceptions import werkzeug.exceptions as wz_exceptions
import pillarsdk import pillarsdk
from pillar.web.system_util import pillar_api from pillar.web.system_util import pillar_api
from pillar.web.utils import get_file from pillar.web.utils import get_file
from pillar.auth import current_user
from attract import current_attract, ROLES_REQUIRED_TO_VIEW_ITEMS from attract import current_attract
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -53,12 +53,10 @@ def for_project(node_type_name, task_types_for_nt, project, attract_props,
def view_node(project, node_id, node_type_name): def view_node(project, node_id, node_type_name):
"""Returns the node if the user has access. """Returns the node if the user has access.
Uses attract.ROLES_REQUIRED_TO_VIEW_ITEMS to check permissions.
""" """
# asset list is public, asset details are not. # asset list is public, asset details are not.
if not flask_login.current_user.has_role(*ROLES_REQUIRED_TO_VIEW_ITEMS): if not current_user.has_cap('attract-view'):
raise wz_exceptions.Forbidden() raise wz_exceptions.Forbidden()
api = pillar_api() api = pillar_api()

View File

@@ -10,11 +10,12 @@ import pillarsdk
from pillar.web.system_util import pillar_api from pillar.web.system_util import pillar_api
import pillar.api.utils import pillar.api.utils
import pillar.web.subquery import pillar.web.subquery
from pillar.auth import current_user
from attract.routes import attract_project_view from attract.routes import attract_project_view
from attract.node_types.task import node_type_task from attract.node_types.task import node_type_task
from attract.node_types.shot import node_type_shot from attract.node_types.shot import node_type_shot
from attract import current_attract, ROLES_REQUIRED_TO_VIEW_ITEMS, EXTENSION_NAME from attract import current_attract, EXTENSION_NAME
blueprint = Blueprint('attract.tasks', __name__, url_prefix='/tasks') blueprint = Blueprint('attract.tasks', __name__, url_prefix='/tasks')
perproject_blueprint = Blueprint('attract.tasks.perproject', __name__, perproject_blueprint = Blueprint('attract.tasks.perproject', __name__,
@@ -65,7 +66,7 @@ def view_task(project, attract_props, task_id):
return for_project(project, task_id=task_id) return for_project(project, task_id=task_id)
# Task list is public, task details are not. # Task list is public, task details are not.
if not flask_login.current_user.has_role(*ROLES_REQUIRED_TO_VIEW_ITEMS): if not current_user.has_cap('attract-view'):
raise wz_exceptions.Forbidden() raise wz_exceptions.Forbidden()
api = pillar_api() api = pillar_api()