Skip to content

Commit

Permalink
patrons: use marshmallow for API control
Browse files Browse the repository at this point in the history
* Use Marshmallow schema to control `Patron` resources through API. The
  `role` field is now controlled to limit changes depending to current
  connected user.
* Renames `User.get_by_id()` to `User.get_record()`
* Improves code coverage
* Include `public-holdings-items` CSS into document detail-view
* Fixes policies related to `Template` resource.

Co-Authored-by: Renaud Michotte <renaud.michotte@gmail.com>
  • Loading branch information
zannkukai committed Feb 7, 2023
1 parent e07c0c6 commit 75d5bda
Show file tree
Hide file tree
Showing 28 changed files with 443 additions and 116 deletions.
2 changes: 2 additions & 0 deletions data/role_policies.json
Original file line number Diff line number Diff line change
Expand Up @@ -701,12 +701,14 @@
"pro_catalog_manager"
],
"tmpl-search": [
"pro_read_only",
"pro_full_permissions",
"pro_catalog_manager",
"pro_user_manager",
"pro_library_administrator"
],
"tmpl-read": [
"pro_read_only",
"pro_full_permissions",
"pro_catalog_manager",
"pro_user_manager",
Expand Down
31 changes: 28 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rero_ils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,7 @@ def _(x):
},
list_route='/patrons/',
record_loaders={
'application/json': lambda: Patron.load(request.get_json()),
'application/json': 'rero_ils.modules.patrons.loaders:json_v1'
},
record_class='rero_ils.modules.patrons.api:Patron',
item_route='/patrons/<pid(ptrn, record_class="rero_ils.modules.patrons.api:Patron"):pid_value>',
Expand Down
8 changes: 0 additions & 8 deletions rero_ils/modules/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,14 +198,6 @@ def _validate(self, **kwargs):
return self

json = super()._validate(**kwargs)

# Check if some record extensions has a validation method.
# DEV NOTES :: Could be part of `invenio-record.extensions`
for extension in self._extensions:
validate_method = getattr(extension, 'validate', None)
if callable(validate_method):
extension.validate(self, **kwargs)

validation_message = self.extended_validation(**kwargs)
# We only like to run pids_exist_check if validation_message is True
# and not a string with error from extended_validation
Expand Down
26 changes: 26 additions & 0 deletions rero_ils/modules/commons/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
#
# RERO ILS
# Copyright (C) 2019-2023 RERO
# Copyright (C) 2019-2023 UCLouvain
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Common models used into RERO-ILS project."""


class NoteTypes:
"""List of note type."""

PUBLIC_NOTE = 'public_note'
STAFF_NOTE = 'staff_note'
9 changes: 4 additions & 5 deletions rero_ils/modules/contributions/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,14 +389,13 @@ def sync_record(self, pid):
f'updated')
if not self.dry_run:
if old_mef_pid == new_mef_pid:
Contribution.get_record_by_id(
agent.id).replace(
new_mef_data, dbcommit=True, reindex=True)
Contribution.get_record(agent.id).replace(
new_mef_data, dbcommit=True, reindex=True)
else:
# as we have only the last mef but not the old one
# we need get it from the MEF server
# this is important as it can still used by other
# agents
# this is important as it can still be used by
# other agents
Contribution.get_record_by_pid(
pid).update_online(dbcommit=True, reindex=True)
updated = True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
{%- extends 'rero_ils/page.html' %}
{% from 'rero_ils/macros/macro.html' import div, dict_values, div_list, dl, dl_row, dl_dict, dl_list, div_json %}

{%- block css %}
{{ super() }}
{{ node_assets('@rero/rero-ils-ui/dist/public-holdings-items', ['styles.*css'], 'css') }}
{%- endblock css %}

{%- block body %}
<header class="row py-2">
<a href="javascript:history.back()" class="col-12"><i class="fa fa-arrow-left"></i> {{ _('Back') }}</a>
Expand Down
2 changes: 1 addition & 1 deletion rero_ils/modules/notifications/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ def patron_transactions(self):
.filter('term', notification__pid=self.pid)\
.source(False).scan()
for result in results:
yield PatronTransaction.get_record_by_id(result.meta.id)
yield PatronTransaction.get_record(result.meta.id)

# CLASS METHODS ===========================================================
def update_process_date(self, sent=False, status=NotificationStatus.DONE):
Expand Down
12 changes: 3 additions & 9 deletions rero_ils/modules/patrons/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@
get_patron_from_arguments, get_ref_for_pid, sorted_pids
from rero_ils.utils import create_user_from_data

from .extensions import PatronRoleManagementValidatorExtension, \
UserDataExtension
from .extensions import UserDataExtension
from .models import CommunicationChannel, PatronIdentifier, PatronMetadata
from .utils import get_patron_pid_by_email

Expand Down Expand Up @@ -117,10 +116,10 @@ class Patron(IlsRecord):
fetcher = patron_id_fetcher
provider = PatronProvider
model_cls = PatronMetadata
schema = 'patrons/patron-v0.0.1.json'

_extensions = [
UserDataExtension(),
PatronRoleManagementValidatorExtension()
UserDataExtension()
]

# =========================================================================
Expand Down Expand Up @@ -500,11 +499,6 @@ def set_keep_history(self, keep_history, dbcommit=True, reindex=True):
# =========================================================================
# CLASS METHODS
# =========================================================================
@classmethod
def load(cls, data):
"""Load the data and remove the user data."""
return cls(cls.remove_user_data(data))

@classmethod
def remove_user_data(cls, data):
"""Remove the user data."""
Expand Down
50 changes: 1 addition & 49 deletions rero_ils/modules/patrons/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Patron record extensions."""
from flask import current_app
from flask_login import current_user
from invenio_records.extensions import RecordExtension
from jsonschema.exceptions import ValidationError

from rero_ils.modules.users.api import User

Expand All @@ -36,51 +33,6 @@ def pre_dump(self, record, data, dumper=None):
:param dumper: Dumper to use when dumping the record.
:return the future dumped data.
"""
user = User.get_by_id(record.get('user_id'))
user = User.get_record(record.get('user_id'))
user_info = user.dumps_metadata()
return data.update(user_info)


class PatronRoleManagementValidatorExtension(RecordExtension):
"""Check about patron role management."""

def _validate(self, record, **kwargs):
"""Validate changes on the record.
:param record: the record containing data to validate.
:param **kwargs: any other named arguments.
:raises ValidationError: If an error is detected during the validation
check. This error could be serialized to get the error message.
"""
# First, determine if a user is connected. If not, no check must be
# done about role management (probably it's a console script/user).
if not current_user:
return

# Now determine the roles difference between original record and
# the record that is updated. If no changes are detected, no more
# check will be done
original_record = record.db_record() or {}
record_roles = set(record.get('roles', []))
original_roles = set(original_record.get('roles', []))
role_changes = original_roles.symmetric_difference(record_roles)
if not role_changes:
return

# Depending on the current logged user roles, determine which roles
# this user can manage reading the configuration setting. If any role
# from `role_changes` are not present in manageable role, an error
# should be raised.
key_config = 'RERO_ILS_PATRON_ROLES_MANAGEMENT_RESTRICTIONS'
config_roles = current_app.config.get(key_config, {})
manageable_roles = set()
for role in current_user.roles:
manageable_roles = manageable_roles.union(
config_roles.get(role.name, {}))
# If any difference are found between both sets, disallow the operation
if role_diffs := role_changes.difference(manageable_roles):
error_roles = ', '.join(role_diffs)
raise ValidationError(f'Unable to manage role(s) : {error_roles}')

validate = _validate
pre_delete = _validate
29 changes: 29 additions & 0 deletions rero_ils/modules/patrons/loaders/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
#
# RERO ILS
# Copyright (C) 2019-2023 RERO
# Copyright (C) 2019-2023 UCLouvain
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Loaders for `Patron` resources."""

from invenio_records_rest.loaders.marshmallow import marshmallow_loader

from ..schemas.json import PatronMetadataSchemaV1

json_v1 = marshmallow_loader(PatronMetadataSchemaV1)

__all__ = (
'json_v1',
)
29 changes: 27 additions & 2 deletions rero_ils/modules/patrons/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

"""Permissions for patrons."""
from flask import g
from invenio_access import action_factory
from flask_login import current_user
from invenio_access import action_factory, any_user
from invenio_records_permissions.generators import Generator

from rero_ils.modules.permissions import AllowedByAction, \
AllowedByActionRestrictByOrganisation, \
Expand All @@ -27,6 +29,7 @@
from rero_ils.modules.users.models import UserRole

from .api import Patron, current_librarian
from .utils import validate_role_changes

# Actions to control patron permission policy
search_action = action_factory('ptrn-search')
Expand Down Expand Up @@ -67,6 +70,27 @@ def needs(self, record=None, *args, **kwargs):
return super().needs(record, *args, **kwargs)


class RestrictDeleteDependOnPatronRolesManagement(Generator):
"""Restrict deletion of patron depending on role management restrictions.
Depending on the current logged user profile, some roles' management
updates could be disallowed. Manageable roles are defined into the
`RERO_ILS_PATRON_ROLES_MANAGEMENT_RESTRICTIONS` configuration attribute.
"""

def excludes(self, record=None, **kwargs):
"""Disallow operation check.
:param record; the record to check.
:param kwargs: extra named arguments.
:returns: a list of Needs to disable access.
"""
roles = set(record.get('roles', []))
if not validate_role_changes(current_user, roles, raise_exc=False):
return [any_user]
return []


class PatronPermissionPolicy(RecordPermissionPolicy):
"""Patron Permission Policy used by the CRUD operations."""

Expand All @@ -82,7 +106,8 @@ class PatronPermissionPolicy(RecordPermissionPolicy):
AllowedByActionRestrictStaffByManageableLibrary(update_action)
]
can_delete = [
AllowedByActionRestrictStaffByManageableLibrary(delete_action)
AllowedByActionRestrictStaffByManageableLibrary(delete_action),
RestrictDeleteDependOnPatronRolesManagement()
]


Expand Down
19 changes: 19 additions & 0 deletions rero_ils/modules/patrons/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
#
# RERO ILS
# Copyright (C) 2019-2023 RERO
# Copyright (C) 2019-2023 UCLouvain
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Patrons schemas."""
Loading

0 comments on commit 75d5bda

Please sign in to comment.