Select Git revision
-
Martin Mareš authoredMartin Mareš authored
rights.py 8.78 KiB
# Přístupová práva
from enum import Enum, auto
from dataclasses import dataclass
from typing import Set, List, Dict, Tuple, Optional
import mo.db as db
class Right(Enum):
assign_rights = auto()
edit_region = auto()
edit_place = auto()
manage_round = auto()
manage_contest = auto()
upload_solutions = auto() # Odevzdávat za účastníka ve stavu "running"
upload_feedback = auto() # Nahrávat opravené řešení ve stavu "grading"
view_submits = auto() # Prohlížet si řešení a opravy
upload_submits = auto() # Nahrávat řešení a opravy nezávisle na stavu soutěže
edit_points = auto()
add_users = auto()
edit_users = auto()
add_orgs = auto()
edit_orgs = auto()
@dataclass
class Role:
role: db.RoleType
name: str
rights: Set[Right]
def __init__(self, role: db.RoleType, rights: Set[Right]):
self.role = role
self.name = role.friendly_name()
self.rights = rights
# Order in this list represents hierarchy for assign_rights right
# (garant could assign role of garant or garant_kraj, but garant_kraj cannot
# assign role garant).
roles: List[Role] = [
Role(
role=db.RoleType.garant,
rights={
Right.assign_rights,
Right.edit_place,
Right.manage_round,
Right.manage_contest,
Right.view_submits,
Right.upload_submits,
Right.edit_points,
Right.add_users,
Right.edit_users,
Right.add_orgs,
Right.edit_orgs,
},
),
Role(
role=db.RoleType.garant_kraj,
rights={
Right.assign_rights,
Right.edit_place,
Right.manage_contest,
Right.view_submits,
Right.upload_submits,
Right.edit_points,
Right.add_users,
Right.edit_users,
Right.add_orgs,
Right.edit_orgs,
},
),
Role(
role=db.RoleType.garant_okres,
rights={
Right.assign_rights,
Right.edit_place,
Right.manage_contest,
Right.view_submits,
Right.upload_submits,
Right.edit_points,
Right.add_users,
Right.edit_users,
Right.add_orgs,
Right.edit_orgs,
},
),
Role(
role=db.RoleType.garant_skola,
rights={
# FIXME: Až se pořádně rozjedou školní kola, asi chceme školním správcům omezit
# práva na editaci uživatelů. Viz issue #66.
Right.assign_rights,
Right.edit_place,
Right.manage_contest,
Right.view_submits,
Right.upload_submits,
Right.edit_points,
Right.add_users,
Right.edit_users,
Right.add_orgs,
Right.edit_orgs,
},
),
Role(
role=db.RoleType.dozor,
rights={
Right.view_submits,
Right.upload_solutions,
},
),
Role(
role=db.RoleType.opravovatel,
rights={
Right.view_submits,
Right.upload_feedback,
Right.edit_points,
},
),
]
roles_by_type: Dict[db.RoleType, Role] = {r.role: r for r in roles}
role_order_by_type: Dict[db.RoleType, int] = {r.role: i for (i, r) in enumerate(roles)}
class Rights:
"""Třída, která popisuje množinu práv k nějakému objektu."""
user: db.User
roles: List[db.UserRole]
rights: Set[Right]
def __repr__(self):
ros = " ".join([r.role.name for r in self.roles])
ris = " ".join([r.name for r in self.rights])
return f"Rights(uid={self.user.user_id} is_admin={self.user.is_admin} roles=<{ros}> rights=<{ris}>)"
def have_right(self, right: Right):
if self.user.is_admin:
return True
assert self.rights is not None
return right in self.rights
# Helper methods for concrete rights
def can_edit_place(self, place: db.Place):
if self.have_right(Right.edit_region):
return True
elif place.level >= 3 and self.have_right(Right.edit_place):
# level >= 3 ... city and lower
return True
return False
def can_add_place_child(self, place: db.Place):
if self.have_right(Right.edit_region):
return True
elif place.level >= 2 and self.have_right(Right.edit_place):
# Can add cities and lower
return True
return False
def can_edit_user(self, user: db.User):
if user.is_admin:
return self.user.is_admin # only admins can edit admins
elif user.is_org:
return self.have_right(Right.edit_orgs)
return self.have_right(Right.edit_users)
class Gatekeeper:
"""Dveřník rozhoduje, jaká práva má uživatel k danému objektu.
Typicky existuje jen v jedné instanci, navázaná na aktuální request
nebo právě zpracovávaný job."""
user: db.User
roles: List[db.UserRole]
parent_cache: Dict[int, List[db.Place]]
rights_cache: Dict[Tuple[Optional[int], Optional[int], Optional[str], Optional[int], Optional[db.RoleType]], Rights]
def __init__(self, user: db.User):
self.user = user
self.roles = user.roles
assert user.is_org or user.is_admin
self.parent_cache = {}
self.rights_cache = {}
def get_parents(self, place: db.Place) -> List[db.Place]:
pid = place.place_id
if pid not in self.parent_cache:
self.parent_cache[pid] = db.get_place_parents(place)
return self.parent_cache[pid]
def rights_for(
self, place: Optional[db.Place] = None, year: Optional[int] = None,
cat: Optional[str] = None, seq: Optional[int] = None,
min_role: Optional[db.RoleType] = None):
"""Posbírá role a práva, která se vztahují k danému místu (možno i tranzitivně) a soutěži.
Pokud place=None, omezení role na místo se nebere v úvahu.
Pokud year==None, vyhovují role s libovolným ročníkem; pokud year=0, vyhovují jen ty s neuvedeným ročníkem.
Podobně cat a seq.
Pokud min_role!=None, tak se uvažují jen role, které jsou v hierarchii alespoň na úrovni min_role."""
cache_key = (place.place_id if place is not None else None, year, cat, seq, min_role)
if cache_key in self.rights_cache:
return self.rights_cache[cache_key]
rights = Rights()
rights.user = self.user
rights.roles = []
rights.rights = set()
def try_role(role: db.UserRole, at: Optional[db.Place]):
if ((at is None or role.place_id == at.place_id)
and (role.year is None or year is None or role.year == year)
and (role.category is None or cat is None or role.category == cat or (role.category == 'Z' and cat.startswith('Z')))
and (role.seq is None or seq is None or role.seq == seq)
and (min_role is None or role_order_by_type[min_role] >= role_order_by_type[role.role])):
rights.roles.append(role)
r = roles_by_type[role.role]
rights.rights |= r.rights
# XXX: This might be faster...
if place is None:
for role in self.roles:
try_role(role, None)
else:
for at in self.get_parents(place):
for role in self.roles:
try_role(role, at)
self.rights_cache[cache_key] = rights
return rights
def rights_generic(self):
"""Posbírá role a práva, ale ignoruje omezení rolí na místa a soutěže. Hodí se pro práva k editaci uživatelů apod."""
return self.rights_for()
def rights_for_round(self, round: db.Round):
return self.rights_for(place=None, year=round.year, cat=round.category, seq=round.seq)
def rights_for_contest(self, contest: db.Contest):
return self.rights_for(place=contest.place, year=contest.round.year, cat=contest.round.category, seq=contest.round.seq)
def rights_for_contest_site(self, contest: db.Contest, place: db.Place):
return self.rights_for(place=place, year=contest.round.year, cat=contest.round.category, seq=contest.round.seq)
def can_set_role(self, role: db.UserRole):
# We will use get_for but we need to slightly modify its behavior. Can
# set role with * year only if current user has role with * year, thus we
# need to pass year=0 for * year (cat and seq similarly).
rights = self.rights_for(
place=role.place,
year=role.year or 0,
cat=role.category or '',
seq=role.seq or 0,
min_role=role.role
)
return rights.have_right(Right.assign_rights)