- Step 1 - Allow for access only from the owner
- Step 2 - search filter
- Step 3 - Create permissions
- Extras - Additional exercises
The goal of this tutorial is to implement record access permissions in simple and complicated cases.
Prerequisites:
- previous steps with owner field
- at least two different users
my-site users create [email protected] -a --password=123456 # create admin user ID 1
my-site users create [email protected] -a --password=123456 # create admin user ID 2
my-site users create [email protected] -a --password=123456 # create visitor user ID 3
- at least two records
curl -k --header "Content-Type: application/json" --request POST --data '{"title":"My test record", "contributors": [{"name": "Doe, John"}], "owner": 1}' https://localhost:5000/api/records/?prettyprint=1
curl -k --header "Content-Type: application/json" --request POST --data '{"title":"Second test record", "contributors": [{"name": "Copernicus, Mikolaj"}], "owner": 2}' https://localhost:5000/api/records/?prettyprint=1
Restrict the access to read, edit and delete action for the record only to its owner.
- We implement the permission factory. The permission requires a need to be fulfilled by a user for a record. In this case we remember that:
"owner": {
"type": "integer"
},
so the permission factory requires users to provide their ID as stored in the the "owner"
field of the record.
Add the following my_site/records/permissions.py
file:
from invenio_access import Permission, any_user
+from flask_principal import UserNeed
def files_permission_factory(obj, action=None):
"""Permissions factory for buckets."""
return Permission(any_user)
+
+def owner_permission_factory(record=None):
+ """Permission factory with owner access to the record."""
+ return Permission(UserNeed(record["owner"]))
- We use the permission factory in the configuration file to let the application know that this endpoint has a permission requirement (RUD). Edit
my_site/records/config.py
:
+from my_site.records.permissions import owner_permission_factory
RECORDS_REST_ENDPOINTS = {
'recid': dict(
pid_type='recid',
pid_minter='recid',
pid_fetcher='recid',
default_endpoint_prefix=True,
search_class=RecordsSearch,
indexer_class=RecordIndexer,
search_index='records',
search_type=None,
record_serializers={
'application/json': ('my_site.records.serializers'
':json_v1_response'),
},
search_serializers={
'application/json': ('my_site.records.serializers'
':json_v1_search'),
},
record_loaders={
'application/json': ('my_site.records.loaders'
':json_v1'),
},
list_route='/records/',
item_route='/records/<pid(recid):pid_value>',
default_media_type='application/json',
max_result_window=10000,
error_handlers=dict(),
create_permission_factory_imp=allow_all,
- read_permission_factory_imp=check_elasticsearch,
- update_permission_factory_imp=allow_all,
- delete_permission_factory_imp=allow_all,
+ read_permission_factory_imp=owner_permission_factory,
+ update_permission_factory_imp=owner_permission_factory,
+ delete_permission_factory_imp=owner_permission_factory,
list_permission_factory_imp=allow_all
),
}
"""REST API for my-site."""
-
log in as manager user
-
visit
/api/records/<id>
(first record)
{
"message": "You don't have the permission to access the requested resource. It is either read-protected or not readable by the server.",
"status": 403
}
- visit
/records/<id>
Record still not protected!
- Set permission factory also for UI endpoints in
my_site/records/config.py
:
RECORDS_UI_ENDPOINTS = dict(
recid=dict(
pid_type='recid',
route='/records/<pid_value>',
template='records/record.html',
record_class='invenio_records_files.api:Record',
+ permission_factory_imp='my_site.records.permissions:owner_permission_factory',
),
- visit
/records/<id>
The details pages of records are now protected. But if we visit /search?page=1&size=20&q=
, all the records are still visible in the search page. The same is true for the REST API: /api/records/
. We would like to hide the results from search if they are not owned by the current user.
- We implement a search filter that will display records in the search results only to their owner.
Let' s create
my_site/records/search.py
:
from elasticsearch_dsl import Q
from flask_security import current_user
def owner_permission_filter():
"""Search filter with permission."""
return [Q('match', owner=current_user.get_id())]
- We implement a search class that uses the implemented filter (also in
search.py
).
from elasticsearch_dsl import Q
from flask_security import current_user
+from invenio_search.api import DefaultFilter, RecordsSearch
def owner_permission_filter():
"""Search filter with permission."""
return [Q('match', owner=current_user.get_id())]
+class OwnerRecordsSearch(RecordsSearch):
+ """Class providing permission search filter."""
+
+ class Meta:
+ index = 'records'
+ default_filter = DefaultFilter(owner_permission_filter)
+ doc_types = None
- We add the search class to the configuration in
my_site/records/config.py
:
+from my_site.records.search import OwnerRecordsSearch
RECORDS_REST_ENDPOINTS = {
'recid': dict(
pid_type='recid',
pid_minter='recid',
pid_fetcher='recid',
default_endpoint_prefix=True,
+ search_class=OwnerRecordsSearch,
indexer_class=RecordIndexer,
search_index='records',
search_type=None,
record_serializers={
'application/json': ('my_site.records.serializers'
':json_v1_response'),
},
search_serializers={
'application/json': ('my_site.records.serializers'
':json_v1_search'),
},
record_loaders={
'application/json': ('my_site.records.loaders'
':json_v1'),
},
list_route='/records/',
item_route='/records/<pid(recid):pid_value>',
default_media_type='application/json',
max_result_window=10000,
error_handlers=dict(),
create_permission_factory_imp=allow_all,
read_permission_factory_imp=owner_permission_factory,
update_permission_factory_imp=owner_permission_factory,
delete_permission_factory_imp=owner_permission_factory,
list_permission_factory_imp=allow_all
),
}
"""REST API for my-site."""
-
Go to the api search page
https://127.0.0.1:5000/api/records/?prettyprint=1
and check that it displays only the records owned by the current user -
Go to the UI search page
https://127.0.0.1:5000/search?page=1&size=20&q=
and check that it displays only the records owned by the current user
- Implement the permission factory in
my_site/records/permissions.py
from invenio_access import Permission, authenticated_user
def authenticated_user_permission(record=None):
"""Return an object that evaluates if the current user is authenticated."""
return Permission(authenticated_user)
- Add the permission factory to the configuration of the records REST endpoints
in
my_site/records/config.py
-from my_site.records.permissions import owner_permission_factory
+from my_site.records.permissions import owner_permission_factory, \
+ authenticated_user_permission
RECORDS_REST_ENDPOINTS = {
'recid': dict(
pid_type='recid',
pid_minter='recid',
pid_fetcher='recid',
default_endpoint_prefix=True,
search_class=OwnerRecordsSearch,
indexer_class=RecordIndexer,
search_index='records',
search_type=None,
record_serializers={
'application/json': ('my_site.records.serializers'
':json_v1_response'),
},
search_serializers={
'application/json': ('my_site.records.serializers'
':json_v1_search'),
},
record_loaders={
'application/json': ('my_site.records.loaders'
':json_v1'),
},
list_route='/records/',
item_route='/records/<pid(recid):pid_value>',
default_media_type='application/json',
max_result_window=10000,
error_handlers=dict(),
- create_permission_factory_imp=allow_all
+ create_permission_factory_imp=authenticated_user_permission,
- Perform a POST request by using curl to test permission to create records as an unauthenticated user (should fail)
curl -k --header "Content-Type: application/json" --request POST --data '{"title":"Second test record", "contributors": [{"name": "Copernicus, Mikolaj"}], "owner": 2}' https://localhost:5000/api/records/?prettyprint=1
Use case: We would like to allow our site's managers to edit and delete records
NOTE: we have existing records already, we would not like to add the group access one by one to each record.
- Create a managers role (group)
(my-site)$ my-site roles create managers
- Connect manager user with created role
(my-site)$ my-site roles add [email protected] managers
- Create the permission factory for role and owner
from invenio_access import Permission
from flask_principal import UserNeed, RoleNeed
def owner_manager_permission_factory(record=None):
"""Returns permission for managers group."""
return Permission(UserNeed(record["owner"]), RoleNeed('managers'))
- Implement search filter for role and owner
from elasticsearch_dsl import Q
from flask_security import current_user
from invenio_search.api import DefaultFilter, RecordsSearch
def owner_manager_permission_filter():
"""Search filter with permission."""
if current_user.has_role('managers'):
return [Q(name_or_query='match_all')]
else:
return [Q('match', owner=current_user.get_id())]
class OwnerManagerRecordsSearch(RecordsSearch):
"""Class providing permission search filter."""
class Meta:
index = 'records'
default_filter = DefaultFilter(owner_manager_permission_filter)
doc_types = None
- Update the configuration file with your new filter and factory
from my_site.records.permissions import owner_permission_factory, \
authenticated_user_permission, owner_manager_permission_factory
from my_site.records.search import OwnerManagerRecordsSearch
RECORDS_REST_ENDPOINTS = {
'recid': dict(
pid_type='recid',
pid_minter='recid',
pid_fetcher='recid',
default_endpoint_prefix=True,
search_class=OwnerManagerRecordsSearch,
indexer_class=RecordIndexer,
search_index='records',
search_type=None,
record_serializers={
'application/json': ('my_site.records.serializers'
':json_v1_response'),
},
search_serializers={
'application/json': ('my_site.records.serializers'
':json_v1_search'),
},
record_loaders={
'application/json': ('my_site.records.loaders'
':json_v1'),
},
list_route='/records/',
item_route='/records/<pid(recid):pid_value>',
default_media_type='application/json',
max_result_window=10000,
error_handlers=dict(),
create_permission_factory_imp=authenticated_user_permission,
read_permission_factory_imp=owner_manager_permission_factory,
update_permission_factory_imp=owner_manager_permission_factory,
delete_permission_factory_imp=owner_manager_permission_factory,
list_permission_factory_imp=allow_all
),
}
"""REST API for my-site."""
- Visit
https://127.0.0.1:5000/search?page=1&size=20&q=
andhttps://127.0.0.1:5000/api/records/?prettyprint=1
as manager user and check if all the records are listed.
- Implement access management for the record having in mind the structure below
{
"_access": {
"read": {
"systemroles": ["campus_user"]
},
"update": {
"users": [1],
"roles": ["curators"]
}
}
}