Using capabilities instead of roles for access control.
This commit is contained in:
@@ -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()
|
||||||
|
@@ -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.
|
||||||
|
@@ -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
|
||||||
|
@@ -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()
|
||||||
|
@@ -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()
|
||||||
|
Reference in New Issue
Block a user