Skip to content

Commit

Permalink
files: fix files permissions
Browse files Browse the repository at this point in the history
* Adds files restriction for documents.
* Adds files restriction for deposits.
* Adds files restriction for organisations.
* Adds files restriction for collections.

Co-Authored-by: Johnny Mariéthoz <[email protected]>
  • Loading branch information
jma committed Jan 26, 2022
1 parent 2422b85 commit 54b1ee9
Show file tree
Hide file tree
Showing 19 changed files with 1,305 additions and 65 deletions.
62 changes: 61 additions & 1 deletion sonar/modules/collections/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@

"""Record permissions."""

from flask import request

from sonar.modules.documents.api import DocumentSearch
from sonar.modules.organisations.api import current_organisation
from sonar.modules.permissions import RecordPermission as BaseRecordPermission

from sonar.modules.permissions import FilesPermission as BaseFilesPermission
from .api import Record


Expand Down Expand Up @@ -104,3 +106,61 @@ def delete(cls, user, record):
return False

return cls.read(user, record)

class FilesPermission(BaseFilesPermission):
"""Collection files permissions.
Follows the same rules than the corresponding collection except for read
which is always accessible.
"""

def __init__(self, record, func, user):
"""Constructor."""
super().__init__(record, func, user)

@classmethod
def get_collection(cls):
"""Get the document from the URL path."""
pid_value_args = request.view_args.get('pid_value')
record = None
if pid_value_args:
(pid, record) = pid_value_args.data
return Record.get_record_by_pid(record['pid'])

@classmethod
def read(cls, user, record):
"""Read permission check.
:param user: Current user record.
:param record: Record to check.
:returns: True is action can be done.
"""
return True

@classmethod
def update(cls, user, record):
"""Update permission check.
:param user: Current user record.
:param recor: Record to check.
:returns: True is action can be done.
"""
# Superuser is allowed.
if user and user.is_superuser:
return True
collection = cls.get_collection()
if collection:
can = RecordPermission.update(user, collection)
if can:
return True
return False

@classmethod
def delete(cls, user, record):
"""Delete permission check.
:param user: Current user record.
:param recor: Record to check.
:returns: True is action can be done.
"""
return cls.update(user, record)
76 changes: 75 additions & 1 deletion sonar/modules/deposits/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Permissions for deposits."""
from flask import request

from sonar.modules.deposits.api import DepositRecord
from sonar.modules.organisations.api import current_organisation
from sonar.modules.permissions import RecordPermission
from sonar.modules.permissions import FilesPermission, RecordPermission


class DepositPermission(RecordPermission):
Expand Down Expand Up @@ -123,3 +124,76 @@ def delete(cls, user, record):

# Same rules as read.
return cls.read(user, record)

class DepositFilesPermission(FilesPermission):
"""Deposits files permissions.
Follows the same rules than the corresponding deposit.
"""

def __init__(self, record, func, user):
"""Constructor."""
super().__init__(record, func, user)

@classmethod
def get_deposit(cls):
"""Get the document from the URL path."""
pid_value_args = request.view_args.get('pid_value')
record = None
if pid_value_args:
(pid, record) = pid_value_args.data
return DepositRecord.get_record_by_pid(record['pid'])

@classmethod
def read(cls, user, record):
"""Read permission check.
:param user: Current user record.
:param record: Record to check.
:returns: True is action can be done.
"""
# Superuser is allowed.
if user and user.is_superuser:
return True
deposit = cls.get_deposit()
if deposit:
can = DepositPermission.read(user, deposit)
if can:
return True
return False

@classmethod
def update(cls, user, record):
"""Update permission check.
:param user: Current user record.
:param recor: Record to check.
:returns: True is action can be done.
"""
# Superuser is allowed.
if user and user.is_superuser:
return True
deposit = cls.get_deposit()
if deposit:
can = DepositPermission.update(user, deposit)
if can:
return True
return False

@classmethod
def delete(cls, user, record):
"""Delete permission check.
:param user: Current user record.
:param recor: Record to check.
:returns: True is action can be done.
"""
# Superuser is allowed.
if user and user.is_superuser:
return True
deposit = cls.get_deposit()
if deposit:
can = DepositPermission.delete(user, deposit)
if can:
return True
return False
22 changes: 21 additions & 1 deletion sonar/modules/documents/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from sonar.modules.documents.minters import id_minter
from sonar.modules.pdf_extractor.utils import extract_text_from_content
from sonar.modules.utils import change_filename_extension, \
create_thumbnail_from_file
create_thumbnail_from_file, get_current_ip, is_ip_in_list

from ..api import SonarIndexer, SonarRecord, SonarSearch
from ..ark.api import current_ark
Expand Down Expand Up @@ -359,6 +359,26 @@ def is_open_access(self):

return True

def is_masked(self):
"""Check if record is masked.
:returns: True if record is masked
:rtype: boolean
"""
if not self.get('masked'):
return False

if self['masked'] == 'masked_for_all':
return True

if self['masked'] == 'masked_for_external_ips' and self.get(
'organisation') and not is_ip_in_list(
get_current_ip(), self['organisation'][0].get(
'allowedIps', '').split('\n')):
return True

return False

def get_ark_resolver_url(self):
"""Get the ark resolver url.
Expand Down
107 changes: 93 additions & 14 deletions sonar/modules/documents/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
"""Permissions for documents."""

from flask import request
from invenio_files_rest.models import Bucket

from sonar.modules.documents.api import DocumentRecord
from sonar.modules.organisations.api import current_organisation
from sonar.modules.permissions import RecordPermission
from sonar.modules.permissions import FilesPermission, RecordPermission

from .utils import get_file_restriction, get_organisations


class DocumentPermission(RecordPermission):
Expand Down Expand Up @@ -64,21 +67,20 @@ def read(cls, user, record):
"""Read permission check.
:param user: Current user record.
:param recor: Record to check.
:param record: Record to check.
:returns: True is action can be done.
"""
# Only for moderator users.
if not user or not user.is_moderator:
return False

# Superuser is allowed.
if user.is_superuser:
if user and user.is_superuser:
return True

document = DocumentRecord.get_record_by_pid(record['pid'])
document = document.replace_refs()

return document.has_organisation(current_organisation['pid'])
# Moderator can read their own documents.
if user and user.is_moderator:
if document.has_organisation(current_organisation['pid']):
return True
return not document.is_masked()

@classmethod
def update(cls, user, record):
Expand All @@ -88,18 +90,19 @@ def update(cls, user, record):
:param recor: Record to check.
:returns: True is action can be done.
"""
# Same rules as read
can_read = cls.read(user, record)

if not can_read:
if not user or not user.is_moderator:
return False

if user.is_admin:
if user.is_superuser:
return True

document = DocumentRecord.get_record_by_pid(record['pid'])
document = document.replace_refs()

# Moderator can update their own documents.
if not document.has_organisation(current_organisation['pid']):
return False

user = user.replace_refs()

return document.has_subdivision(user.get('subdivision', {}).get('pid'))
Expand All @@ -118,3 +121,79 @@ def delete(cls, user, record):

# Same rules as update
return cls.update(user, record)

class DocumentFilesPermission(FilesPermission):
"""Documents files permissions.
Write operations are limited to admin users, read depends if the
corresponding document is masked or if the file is restricted.
"""

def __init__(self, record, func, user):
"""Constructor."""
super().__init__(record, func, user)

@classmethod
def get_document(cls):
"""Get the document from the URL path."""
pid_value_args = request.view_args.get('pid_value')
record = None
if pid_value_args:
(pid, record) = pid_value_args.data
return DocumentRecord.get_record_by_pid(record['pid'])

@classmethod
def read(cls, user, record):
"""Read permission check.
:param user: current user record.
:param record: Record to check.
:returns: True is action can be done.
"""
# Superuser is allowed.
if user and user.is_superuser:
return True
document = cls.get_document()
if document:
can_read_document = DocumentPermission.read(user, document)
if not can_read_document:
return False

# read the bucket metadata
# TODO: filter the list of files based on embargo
if isinstance(record, Bucket):
return True
file_type = document.files[record.key]['type']
if file_type == 'fulltext' and (not user or not user.is_admin):
return False
file_restriction = get_file_restriction(
document.files[record.key],
get_organisations(document),
True
)
return not file_restriction.get('restricted', True)

@classmethod
def update(cls, user, record):
"""Update permission check.
:param user: Current user record.
:param recor: Record to check.
:returns: True is action can be done.
"""
if user and user.is_superuser:
return True
document = cls.get_document()
if document:
return DocumentPermission.update(user, document)
return False

@classmethod
def delete(cls, user, record):
"""Delete permission check.
:param user: Current user record.
:param recor: Record to check.
:returns: True is action can be done.
"""
return cls.update(user, record)
10 changes: 4 additions & 6 deletions sonar/modules/documents/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,13 @@ def get_file_links(file, record):
return links


def get_file_restriction(file, organisations):
def get_file_restriction(file, organisations, for_permission=True):
"""Check if current file can be displayed.
:param file: File dict.
:param organisations: List of organisations.
:returns: Object containing result as boolean and possibly embargo date.
"""

def is_allowed_by_scope():
"""Check if file is fully restricted or only outside organisation.
Expand Down Expand Up @@ -150,20 +149,19 @@ def is_allowed_by_scope():
not_restricted = {'restricted': False, 'date': None}

# We are in admin, no restrictions are applied.
if not request.args.get('view') and not request.view_args.get(
'view') and request.url_rule.rule != '/oai2d':
if not for_permission and not request.args.get('view') and\
not request.view_args.get('view') and\
request.url_rule.rule != '/oai2d':
return not_restricted

# No specific access or specific access is open access
if not file.get('access') or file['access'] == 'coar:c_abf2':
return not_restricted

# Access is embargoed
if file['access'] == 'coar:c_f1cf':
# No embargo date
if not file.get('embargo_date'):
return not_restricted

try:
embargo_date = datetime.strptime(file['embargo_date'], '%Y-%m-%d')
except Exception:
Expand Down
Loading

0 comments on commit 54b1ee9

Please sign in to comment.