Skip to content
This repository was archived by the owner on Sep 16, 2022. It is now read-only.

Commit 863d154

Browse files
author
Hannu Kamarainen
committed
CSCMETAX-393: [ADD] Add permission class ServicePermissions, which reads app_config and controls general read and write access for services per each api
1 parent 779543b commit 863d154

File tree

22 files changed

+197
-56
lines changed

22 files changed

+197
-56
lines changed

src/metax_api/api/rest/base/views/api_error_view.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
23
from django.http import Http404
34
from rest_framework.decorators import list_route
45
from rest_framework.response import Response
@@ -23,9 +24,6 @@
2324

2425
class ApiErrorViewSet(CommonViewSet):
2526

26-
authentication_classes = ()
27-
permission_classes = ()
28-
2927
def initial(self, request, *args, **kwargs):
3028
if request.user.username != 'metax':
3129
raise Http403

src/metax_api/api/rest/base/views/common_view.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33

44
from django.http import Http404
55
from rest_framework import status
6+
from rest_framework.exceptions import PermissionDenied, MethodNotAllowed
67
from rest_framework.generics import get_object_or_404
78
from rest_framework.response import Response
89
from rest_framework.viewsets import ModelViewSet
910

1011
from metax_api.exceptions import Http403
12+
from metax_api.permissions import ServicePermissions
1113
from metax_api.services import CommonService as CS, ApiErrorService
1214
from metax_api.utils import RedisSentinelCache
1315

@@ -22,6 +24,9 @@ class CommonViewSet(ModelViewSet):
2224
which include fields like modified and created timestamps, uuid, active flags etc.
2325
"""
2426

27+
authentication_classes = ()
28+
permission_classes = (ServicePermissions,)
29+
2530
lookup_field_internal = None
2631
cache = RedisSentinelCache()
2732

@@ -49,7 +54,7 @@ def handle_exception(self, exc):
4954
Store request and response data to disk for later inspection
5055
"""
5156
response = super(CommonViewSet, self).handle_exception(exc)
52-
if type(exc) not in (Http403, Http404):
57+
if type(exc) not in (Http403, Http404, PermissionDenied, MethodNotAllowed):
5358
ApiErrorService.store_error_details(self.request, response, exc)
5459
return response
5560

@@ -225,3 +230,12 @@ def _check_and_store_bulk_error(self, request, response):
225230
"""
226231
if 'failed' in response.data and len(response.data['failed']):
227232
ApiErrorService.store_error_details(request, response, other={ 'bulk_request': True })
233+
234+
def get_api_name(self):
235+
"""
236+
Return api name, example: DatasetViewSet -> datasets.
237+
Some views where the below formula does not produce a sensible result
238+
(for example, directories-api), will inherit this and return a customized
239+
result.
240+
"""
241+
return '%ss' % self.__class__.__name__.split('ViewSet')[0].lower()

src/metax_api/api/rest/base/views/contract_view.py

-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import logging
2-
31
from rest_framework import status
42
from rest_framework.decorators import detail_route
53
from rest_framework.response import Response
@@ -9,15 +7,9 @@
97
from .common_view import CommonViewSet
108
from ..serializers import ContractSerializer, CatalogRecordSerializer
119

12-
_logger = logging.getLogger(__name__)
13-
d = logging.getLogger(__name__).debug
14-
1510

1611
class ContractViewSet(CommonViewSet):
1712

18-
authentication_classes = ()
19-
permission_classes = ()
20-
2113
serializer_class = ContractSerializer
2214
object = Contract
2315

src/metax_api/api/rest/base/views/data_catalog_view.py

-9
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
1-
import logging
2-
31
from django.http import Http404
42

53
from metax_api.models import DataCatalog
64
from .common_view import CommonViewSet
75
from ..serializers import DataCatalogSerializer
86

9-
_logger = logging.getLogger(__name__)
10-
d = logging.getLogger(__name__).debug
11-
127

138
class DataCatalogViewSet(CommonViewSet):
149

15-
authentication_classes = ()
16-
permission_classes = ()
17-
1810
serializer_class = DataCatalogSerializer
1911
object = DataCatalog
20-
2112
lookup_field = 'pk'
2213

2314
def __init__(self, *args, **kwargs):

src/metax_api/api/rest/base/views/dataset_view.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,11 @@
1616
from ..serializers import CatalogRecordSerializer, FileSerializer
1717

1818
_logger = logging.getLogger(__name__)
19-
d = logging.getLogger(__name__).debug
19+
d = _logger.debug
2020

2121

2222
class DatasetViewSet(CommonViewSet):
2323

24-
authentication_classes = ()
25-
permission_classes = ()
26-
2724
serializer_class = CatalogRecordSerializer
2825
object = CatalogRecord
2926
select_related = ['data_catalog', 'contract']

src/metax_api/api/rest/base/views/directory_view.py

+6-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from collections import defaultdict
2-
import logging
32

43
from rest_framework.decorators import detail_route, list_route
54
from rest_framework.response import Response
@@ -10,21 +9,20 @@
109
from metax_api.services import CommonService, FileService
1110
from .common_view import CommonViewSet
1211

13-
_logger = logging.getLogger(__name__)
14-
d = logging.getLogger(__name__).debug
15-
1612

1713
class DirectoryViewSet(CommonViewSet):
1814

19-
authentication_classes = ()
20-
permission_classes = ()
21-
2215
serializer_class = DirectorySerializer
2316
object = Directory
2417
select_related = ['parent_directory']
25-
2618
lookup_field_other = 'identifier'
2719

20+
def get_api_name(self):
21+
"""
22+
Overrided due to not being able to follow common plural pattern...
23+
"""
24+
return 'directories'
25+
2826
def list(self, request, *args, **kwargs):
2927
raise Http501()
3028

src/metax_api/api/rest/base/views/file_storage_view.py

-4
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,8 @@
1010

1111
class FileStorageViewSet(CommonViewSet):
1212

13-
authentication_classes = ()
14-
permission_classes = ()
15-
1613
serializer_class = FileStorageSerializer
1714
object = FileStorage
18-
1915
lookup_field = 'pk'
2016

2117
def __init__(self, *args, **kwargs):

src/metax_api/api/rest/base/views/file_view.py

-4
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,12 @@
1818
from ..serializers import FileSerializer, XmlMetadataSerializer
1919

2020
_logger = logging.getLogger(__name__)
21-
d = logging.getLogger(__name__).debug
2221

2322

2423
# none of the methods in this class use atomic requests by default! see method dispatch()
2524
@transaction.non_atomic_requests
2625
class FileViewSet(CommonViewSet):
2726

28-
authentication_classes = ()
29-
permission_classes = ()
30-
3127
serializer_class = FileSerializer
3228
object = File
3329
select_related = ['file_storage', 'parent_directory']
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
1-
import logging
2-
31
from rest_framework import viewsets
4-
from metax_api.services import SchemaService
5-
62

7-
_logger = logging.getLogger(__name__)
8-
d = logging.getLogger(__name__).debug
3+
from metax_api.permissions import ServicePermissions
4+
from metax_api.services import SchemaService
95

106

117
class SchemaViewSet(viewsets.ReadOnlyModelViewSet):
128

139
authentication_classes = ()
14-
permission_classes = ()
10+
permission_classes = (ServicePermissions,)
1511

1612
def list(self, request, *args, **kwargs):
1713
return SchemaService.get_all_schemas()
@@ -21,3 +17,9 @@ def retrieve(self, request, *args, **kwargs):
2117

2218
def get_queryset(self):
2319
return self.list(None)
20+
21+
def get_api_name(self):
22+
"""
23+
Does not inherit from common...
24+
"""
25+
return 'schemas'

src/metax_api/middleware/identifyapicaller.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,4 @@ def _identify_api_caller(self, request):
128128
class _IdentifyApiCallerDummy(_IdentifyApiCaller):
129129

130130
def _get_api_users(self):
131-
return [
132-
django_settings.API_METAX_USER,
133-
django_settings.API_TEST_USER,
134-
]
131+
return django_settings.API_TEST_USERS

src/metax_api/permissions/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .permissions import *
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import logging
2+
3+
from django.conf import settings
4+
from rest_framework.exceptions import MethodNotAllowed
5+
from rest_framework.permissions import BasePermission
6+
7+
8+
_logger = logging.getLogger(__name__)
9+
READ_METHODS = ('GET', 'HEAD', 'OPTIONS')
10+
WRITE_METHODS = ('POST', 'PUT', 'PATCH', 'DELETE')
11+
12+
13+
class ServicePermissions(BasePermission):
14+
15+
"""
16+
Permission-object to control api-resource-wide read and write access for each
17+
service, such as /datasets or /files. If access to individual view method (url)
18+
needs to be restricted, add additional checks directly inside that view method.
19+
20+
The source of service permissions is the app_config file.
21+
"""
22+
23+
perms = {}
24+
25+
def __init__(self, *args, **kwargs):
26+
self.set_perms()
27+
28+
def set_perms(self):
29+
"""
30+
self.perms dict looks like:
31+
{
32+
'datasets': {
33+
'read': ['service1', 'service2'],
34+
'write': ['service1', 'service3']
35+
}
36+
'files': {
37+
...
38+
},
39+
...
40+
}
41+
"""
42+
self.perms = settings.API_ACCESS
43+
44+
def has_permission(self, request, view):
45+
"""
46+
Return `True` if permission is granted, `False` otherwise.
47+
"""
48+
api_name = view.get_api_name()
49+
50+
if api_name not in self.perms: # pragma: no cover
51+
_logger.info(
52+
'api_name %s not specified in self.perms - forbidding. this probably should not happen' % api_name
53+
)
54+
return False
55+
56+
elif request.method in READ_METHODS:
57+
if 'all' in self.perms[api_name]['read']:
58+
return True
59+
return request.user.username in self.perms[api_name]['read']
60+
61+
elif request.method in WRITE_METHODS:
62+
return request.user.username in self.perms[api_name]['write']
63+
64+
else:
65+
raise MethodNotAllowed
66+
67+
return False
68+
69+
def has_object_permission(self, request, view, obj):
70+
"""
71+
Return `True` if permission is granted, `False` otherwise.
72+
"""
73+
# default
74+
return True

src/metax_api/settings.py

+24-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
2727
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
2828

29-
3029
# Quick-start development settings - unsuitable for production
3130
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
3231

@@ -46,13 +45,36 @@
4645
'username': 'metax',
4746
'password': 'metaxpassword'
4847
}
48+
API_AUTH_TEST_USER = {
49+
'username': 'api_auth_user',
50+
'password': 'assword'
51+
}
4952

53+
API_TEST_USERS = [
54+
API_TEST_USER,
55+
API_METAX_USER,
56+
API_AUTH_TEST_USER,
57+
]
58+
59+
API_ACCESS = {
60+
"apierrors": { "read": ["testuser", "metax"], "write": ["testuser", "metax"] },
61+
"contracts": { "read": ["testuser", "metax"], "write": ["testuser", "metax"] },
62+
"datacatalogs": { "read": ["all"], "write": ["testuser", "metax"] },
63+
"datasets": { "read": ["all"], "write": ["testuser", "metax", "api_auth_user"] },
64+
"directories": { "read": ["testuser", "metax"], "write": ["testuser", "metax"] },
65+
"files": { "read": ["testuser", "metax", "api_auth_user"], "write": ["testuser", "metax"] },
66+
"filestorages": { "read": ["testuser", "metax"], "write": ["testuser", "metax"] },
67+
"schemas": { "read": ["all"], "write": ["testuser", "metax"] }
68+
}
69+
else:
70+
API_ACCESS = app_config_dict['API_ACCESS']
71+
72+
if executing_test_case() or executing_in_travis:
5073
ERROR_FILES_PATH = '/tmp/metax-api-tests/errors'
5174
else:
5275
# location to store information about exceptions occurred during api requests
5376
ERROR_FILES_PATH = '/var/log/metax-api/errors'
5477

55-
5678
# Consider enabling these
5779
#CSRF_COOKIE_SECURE = True
5880
#SECURE_SSL_REDIRECT = True
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
from .auth import *
12
from .read import *
23
from .write import *

0 commit comments

Comments
 (0)