GooDrive/backend/drive/permissions.py
Louis Mazin d94a36d9a7 parfait
2026-10-14 18:09:57 +02:00

128 lines
5.0 KiB
Python

"""Granular, non-escalating rights for spaces and individual shares."""
from .models import ItemGrant, SpaceGrant
RIGHTS = ('view', 'download', 'add', 'modify', 'delete', 'share', 'manage_access', 'create_spaces', 'manage_space')
# A file/folder share never delegates space administration or deletion.
ITEM_SHARE_RIGHTS = ('view', 'download', 'modify', 'add')
def available_item_share_rights(item):
return tuple(right for right in ITEM_SHARE_RIGHTS if right != 'add' or item.is_folder)
def normalized_item_share(value, item):
rights = normalized(value)
if not set(rights).issubset(available_item_share_rights(item)):
raise ValueError('Ce droit ne peut pas être attribué sur ce partage.')
return rights
def assert_item_delegation(issuer_rights, desired, item):
rights = normalized_item_share(desired, item)
if not set(rights).issubset(set(issuer_rights)):
raise ValueError('Tu ne peux pas attribuer des permissions supérieures aux tiennes.')
return rights
PRESETS = {
'reader': ('view', 'download'),
'contributor': ('view', 'download', 'add', 'modify'),
'manager': RIGHTS,
}
def legacy_rights(read=True, write=False, invite=False):
result = ['view', 'download'] if read else []
if write:
result += ['add', 'modify', 'delete', 'share']
if invite:
result += ['manage_access', 'create_spaces']
return result
def normalized(value):
if not isinstance(value, list) or len(value) > len(RIGHTS) or any(not isinstance(k, str) or k not in RIGHTS for k in value):
raise ValueError('Liste des permissions invalide.')
rights = set(value)
if 'view' not in rights:
raise ValueError('La permission Consulter est obligatoire.')
return sorted(rights, key=RIGHTS.index)
def _is_admin(user):
return user.is_authenticated and user.is_active and (user.is_staff or user.is_superuser)
def space_rights(user, space):
if not user.is_authenticated or not user.is_active or space is None:
return set()
if space.private_owner_id:
# Never expose someone else's private contents, including to admins.
return set(RIGHTS) if space.private_owner_id == user.pk else set()
if _is_admin(user):
return set(RIGHTS)
# A grant can also explicitly limit an assigned owner or manager, whose
# historical roles otherwise implied full access.
grant = SpaceGrant.objects.filter(space=space, user=user).first()
if grant:
return set(grant.permissions) if grant.permissions is not None else set(legacy_rights(write=grant.can_write, invite=grant.can_invite))
if space.managed_by_id == user.pk or space.user_id == user.pk:
return set(RIGHTS)
if space.parent_id and space.can_view_parent and space.user_id == user.pk:
return set(legacy_rights(write=space.can_write_parent, invite=space.can_invite))
# Historical assigned-space access (parent) for legacy sub-users.
owned = getattr(user, 'drive_space', None)
if owned and owned.parent_id == space.pk and owned.can_view_parent:
return set(owned.parent_permissions) if owned.parent_permissions is not None else set(legacy_rights(write=owned.can_write_parent, invite=owned.can_invite))
return set()
def has_space_right(user, space, right):
return right in space_rights(user, space)
def can_change_space_quota(user, space):
"""The allocated quota is decided by the administrator, not the invitee.
A non-admin may size only a shared subspace that they themselves created,
provided they still have the space-management permission. Having
``manage_space`` on an assigned/shared space must never allow increasing
the storage allocation set by its creator/administrator.
"""
if space is None or space.private_owner_id or not user.is_authenticated or not user.is_active:
return False
if _is_admin(user):
return True
return (space.parent_id is not None
and space.user_id is None
and space.managed_by_id == user.pk
and not SpaceGrant.objects.filter(user=user, space=space).exists()
and has_space_right(user, space, 'manage_space'))
def item_rights(user, item):
rights = space_rights(user, item.space)
if rights:
return rights
if not user.is_authenticated or not user.is_active:
return set()
# Share on a parent folder propagates to its descendants.
seen = set()
current = item
while current is not None:
if current.pk in seen:
return set()
seen.add(current.pk)
grant = ItemGrant.objects.filter(user=user, item=current).first()
if grant:
return (set(grant.permissions) if grant.permissions is not None else set(PRESETS['reader'])) & set(available_item_share_rights(current))
current = current.parent
return set()
def assert_delegation(issuer_rights, desired):
clean = set(normalized(desired))
if not clean.issubset(set(issuer_rights)):
raise ValueError('Tu ne peux pas attribuer des permissions supérieures aux tiennes.')
return normalized(desired)