Skip to content

Commit

Permalink
permission: add 'create' to permission API
Browse files Browse the repository at this point in the history
* Updates the permission API adding a section about the 'create'
permission.
* Updates `SearchSerializer` to remove permissions from hit result.

Co-Authored-by: Renaud Michotte <renaud.michotte@gmail.com>
  • Loading branch information
zannkukai committed May 27, 2020
1 parent dbc5be8 commit 89693cd
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 262 deletions.
4 changes: 1 addition & 3 deletions rero_ils/modules/documents/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,9 @@ def preprocess_record(self, pid, record, links_factory=None, **kwargs):
if person:
authors.append(person.dumps_for_document())
rec['authors'] = authors
data = super(JSONSerializer, self).preprocess_record(
return super(JSONSerializer, self).preprocess_record(
pid=pid, record=rec, links_factory=links_factory, kwargs=kwargs)

return JSONSerializer.add_item_links_and_permissions(record, data, pid)

def post_process_serialize_search(self, results, pid_fetcher):
"""Post process the search results."""
# Item filters.
Expand Down
78 changes: 42 additions & 36 deletions rero_ils/modules/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,44 +20,50 @@

from flask import jsonify

from .utils import get_record_class_update_permission_from_route
from .utils import get_record_class_permissions_factories_from_route


def jsonify_permission_api_response(
can_update=False, can_delete=False, reasons={}):
"""Jsonify api response."""
return jsonify({
'update': {'can': can_update},
'delete': {'can': can_delete, 'reasons': reasons}
})


def record_update_delete_permissions(record_pid=None, route_name=None):
def record_permissions(record_pid=None, route_name=None):
"""Return record permissions."""
try:
rec_class, update_permission, delete_permission = \
get_record_class_update_permission_from_route(route_name)
record = rec_class.get_record_by_pid(record_pid)

if not record:
return jsonify({'status': 'error: Record not found.'}), 404

# We have two behavior for 'can_delete'. Either the record has linked
# resource and so children resources should be deleted before ; either
# the `delete_permissions_factory` for this record should be called. If
# this call send 'False' then the reason_not_to_delete should be
# "permission denied"
can_delete = record.can_delete and delete_permission(record).can()
reasons = record.reasons_not_to_delete()
if not can_delete and not reasons:
# in this case, it's because config delete factory return `False`
# So the reason is 'Permission denied'
reasons = {'others': {'permission': 'permission denied'}}

return jsonify_permission_api_response(
can_update=update_permission(record).can(),
can_delete=can_delete,
reasons=reasons
)
except Exception as error:
rec_class, create_permission, update_permission, delete_permission = \
get_record_class_permissions_factories_from_route(route_name)

# To check create permission, we don't need to check if the record_pid
# exists. Just call the create permission (if exists) with `None` value
# as record.
permissions = {
'create': {'can': True}
}
if create_permission:
permissions['create']['can'] = create_permission(record=None).can()

# If record_pid is not None, we can check about 'delete' and 'update'
# permissions.
if record_pid:
record = rec_class.get_record_by_pid(record_pid)
if not record:
return jsonify({'status': 'error: Record not found.'}), 404

# To check if the record could be update, just call the update
# permission factory to get the answer
permissions['update'] = {'can': update_permission(record).can()}

# We have two behaviors for 'can_delete'. Either the record has
# linked resources and so children resources should be deleted
# before ; either the `delete_permissions_factory` for this record
# should be called. If this call send 'False' then the
# reason_not_to_delete should be "permission denied"
permissions['delete'] = {
'can': record.can_delete and delete_permission(record).can()
}
reasons = record.reasons_not_to_delete()
if not permissions['delete']['can'] and not reasons:
# in this case, it's because config delete factory return
# `False`, so the reason is 'Permission denied'
reasons = {'others': {'permission': 'permission denied'}}
permissions['delete']['reasons'] = reasons

return jsonify(permissions)
except Exception:
return jsonify({'status': 'error: Bad request'}), 400
112 changes: 14 additions & 98 deletions rero_ils/modules/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,13 @@

"""Record serialization."""

from flask import current_app, json, request, url_for
from invenio_pidstore.errors import PIDDoesNotExistError
from invenio_records_rest import current_records_rest
from flask import json, request, url_for
from invenio_records_rest.schemas import \
RecordSchemaJSONV1 as _RecordSchemaJSONV1
from invenio_records_rest.serializers.json import \
JSONSerializer as _JSONSerializer
from invenio_records_rest.serializers.response import record_responsify, \
search_responsify
from invenio_records_rest.utils import obj_or_import_string
from marshmallow import fields


Expand All @@ -48,117 +45,36 @@ def preprocess_record(self, pid, record, links_factory=None, **kwargs):
rec = record
if request and request.args.get('resolve') == '1':
rec = record.replace_refs()
data = super(JSONSerializer, self).preprocess_record(
return super(JSONSerializer, self).preprocess_record(
pid=pid, record=rec, links_factory=links_factory, kwargs=kwargs)

return JSONSerializer.add_item_links_and_permissions(record, data, pid)

@staticmethod
def preprocess_search_hit(pid, record_hit, links_factory=None, **kwargs):
"""Prepare a record hit from Elasticsearch for serialization."""
from invenio_records.api import Record
from invenio_pidstore.models import PersistentIdentifier
data = super(JSONSerializer, JSONSerializer).preprocess_search_hit(
pid=pid, record_hit=record_hit,
links_factory=links_factory, kwargs=kwargs)
record_class = obj_or_import_string(
current_app.config
.get('RECORDS_REST_ENDPOINTS')
.get(pid.pid_type).get('record_class', Record))
try:
persistent_identifier = PersistentIdentifier.get(
pid.pid_type, pid.pid_value)
record = record_class.get_record(
persistent_identifier.object_uuid
)
json = JSONSerializer.add_item_links_and_permissions(
record, data, pid
)
permissions = json.get('permissions')
except PIDDoesNotExistError:
permissions = {
'cannot_update': {'permisson': 'permission denied'},
'cannot_delete': {'permisson': 'permission denied'}
}
super(JSONSerializer, JSONSerializer).preprocess_search_hit(
pid=pid,
record_hit=record_hit,
links_factory=links_factory,
kwargs=kwargs
)
search_hit = dict(
pid=pid,
metadata=record_hit['_source'],
links=links_factory(pid, record_hit=record_hit, **kwargs),
revision=record_hit['_version'],
permissions=permissions
revision=record_hit['_version']
)
if record_hit.get('_explanation'):
search_hit['explanation'] = record_hit.get('_explanation')
return search_hit

@staticmethod
def add_item_links_and_permissions(record, data, pid):
"""Update the record with action links and permissions."""
# TODO: remove this function and use the permission api
if pid.pid_type != 'doc':
actions = [
'update',
'delete'
]
permissions = {}
action_links = {}
for action in actions:
permission = JSONSerializer.get_permission(
action, pid.pid_type)
if permission:
can = permission(record, credentials_only=True).can()
if can:
action_links[action] = url_for(
'invenio_records_rest.{pid_type}_item'.format(
pid_type=pid.pid_type),
pid_value=pid.pid_value, _external=True)
else:
action_key = 'cannot_{action}'.format(action=action)
permissions[action_key] = {
'permission': "permission denied"}
if not record.can_delete:
permissions.setdefault(
'cannot_delete',
{}
).update(record.reasons_not_to_delete())
data['links'].update(action_links)
data['permissions'] = permissions
return data

@staticmethod
def get_permission(action, pid_type):
"""Get the permission given an action."""
default_action = getattr(
current_records_rest,
'{action}_permission_factory'.format(action=action))
permission = obj_or_import_string(
current_app.config
.get('RECORDS_REST_ENDPOINTS')
.get(pid_type)
.get(
'{action}_permission_factory_imp'.format(action=action),
default_action))
return permission

def post_process_serialize_search(self, results, pid_fetcher):
"""Post process the search results."""
pid_type = pid_fetcher('foo', dict(pid='1')).pid_type

# add permissions and links actions
permission = self.get_permission('create', pid_type)
permissions = {}
links = {}
if permission:
can = permission(record=None).can()
if can:
links['create'] = url_for(
'invenio_records_rest.{pid_type}_list'.format(
pid_type=pid_type), _external=True)
else:
permissions['cannot_create'] = {
'permission': "permission denied"}
results['permissions'] = permissions
results['links'].update(links)
results['links'].update({
'create': url_for('invenio_records_rest.{pid_type}_list'.format(
pid_type=pid_type), _external=True
)
})
return results

def serialize_search(self, pid_fetcher, search_result, links=None,
Expand Down
9 changes: 6 additions & 3 deletions rero_ils/modules/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,22 @@ def read_json_record(json_file, buf_size=1024, decoder=JSONDecoder()):
buffer = buffer[1:].lstrip()


def get_record_class_update_permission_from_route(route_name):
"""Return the record class for a given record route name."""
def get_record_class_permissions_factories_from_route(route_name):
"""Get record class and permission factories for a record route name."""
endpoints = current_app.config.get('RECORDS_REST_ENDPOINTS')
for endpoint in endpoints.items():
record = endpoint[1]
list_route = record.get('list_route').replace('/', '')
if list_route == route_name:
record_class = obj_or_import_string(record.get('record_class'))
create_permission = obj_or_import_string(
record.get('create_permission_factory_imp'))
update_permission = obj_or_import_string(
record.get('update_permission_factory_imp'))
delete_permission = obj_or_import_string(
record.get('delete_permission_factory_imp'))
return record_class, update_permission, delete_permission
return record_class, create_permission, \
update_permission, delete_permission


def get_endpoint_configuration(module):
Expand Down
16 changes: 9 additions & 7 deletions rero_ils/modules/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from flask_babelex import get_domain
from flask_login import current_user

from .permissions import record_update_delete_permissions
from .permissions import record_permissions
from ..permissions import librarian_permission

api_blueprint = Blueprint(
Expand All @@ -50,16 +50,18 @@ def decorated_view(*args, **kwargs):
return decorated_view


@api_blueprint.route(
'/permissions/<route_name>/<record_pid>', methods=['GET'])
@api_blueprint.route('/permissions/<route_name>', methods=['GET'])
@api_blueprint.route('/permissions/<route_name>/<record_pid>', methods=['GET'])
@check_authentication
def permissions(route_name, record_pid):
def permissions(route_name, record_pid=None):
"""HTTP GET request for record permissions.
Required parameters: route_name, record_pid
:param route_name : the list route of the resource
:param record_pid : the record pid
:return a JSON object with create/update/delete permissions for this
record/resource
"""
return record_update_delete_permissions(
record_pid=record_pid, route_name=route_name)
return record_permissions(record_pid=record_pid, route_name=route_name)


@api_blueprint.route('/translations/<ln>.json')
Expand Down
Loading

0 comments on commit 89693cd

Please sign in to comment.