From 2c4e3d3fcd5e662b2bb3a807808b30b54bc0029c Mon Sep 17 00:00:00 2001
From: Martin Mares <mj@ucw.cz>
Date: Tue, 28 Sep 2021 20:19:15 +0200
Subject: [PATCH] =?UTF-8?q?Omezen=C3=AD=20pr=C3=A1v=20=C5=A1koln=C3=ADch?=
 =?UTF-8?q?=20garant=C5=AF=20k=20u=C5=BEivatel=C5=AFm?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Školní garant smí pracovat jen s uživateli ze své školy (studujícími
na dané škole nebo účastnícími se příslušného školního kola).
---
 mo/rights.py                    | 81 +++++++++++++++++++++++++++++----
 mo/web/org_users.py             | 19 +++++++-
 mo/web/templates/org_users.html |  3 ++
 3 files changed, 93 insertions(+), 10 deletions(-)

diff --git a/mo/rights.py b/mo/rights.py
index c8d549a3..b0e8878a 100644
--- a/mo/rights.py
+++ b/mo/rights.py
@@ -2,9 +2,12 @@
 
 from enum import Enum, auto
 from dataclasses import dataclass
+from sqlalchemy import or_
+from sqlalchemy.orm.query import Query
 from typing import Set, List, Dict, Tuple, Optional
 
 import mo
+import mo.config as config
 import mo.db as db
 
 
@@ -23,7 +26,10 @@ class Right(Enum):
     edit_points = auto()            # Přidělovat body ve stavu "grading"
     view_statement = auto()         # Prohlížet zadání, pokud je dostupné pro dozor
     add_users = auto()
-    edit_users = auto()
+    view_all_users = auto()         # Prohlížet všechny uživatele
+    view_school_users = auto()      # Prohlížet uživatele ze své školy (jen garant_skola)
+    edit_all_users = auto()         # Editovat všechny účastníky
+    edit_school_users = auto()      # Editovat uživatele ze své školy (jen garant_skola)
     add_orgs = auto()
     edit_orgs = auto()
 
@@ -57,7 +63,8 @@ roles: List[Role] = [
             Right.upload_submits,
             Right.edit_points,
             Right.add_users,
-            Right.edit_users,
+            Right.view_all_users,
+            Right.edit_all_users,
             Right.add_orgs,
             Right.edit_orgs,
         },
@@ -75,7 +82,8 @@ roles: List[Role] = [
             Right.edit_points,
             Right.view_statement,
             Right.add_users,
-            Right.edit_users,
+            Right.view_all_users,
+            Right.edit_all_users,
             Right.add_orgs,
             Right.edit_orgs,
         },
@@ -93,7 +101,8 @@ roles: List[Role] = [
             Right.edit_points,
             Right.view_statement,
             Right.add_users,
-            Right.edit_users,
+            Right.view_all_users,
+            Right.edit_all_users,
             Right.add_orgs,
             Right.edit_orgs,
         },
@@ -101,8 +110,6 @@ roles: List[Role] = [
     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,
@@ -112,7 +119,8 @@ roles: List[Role] = [
             Right.edit_points,
             Right.view_statement,
             Right.add_users,
-            Right.edit_users,
+            Right.view_school_users,
+            Right.edit_school_users,
             Right.add_orgs,
             Right.edit_orgs,
         },
@@ -191,12 +199,69 @@ class Rights:
 
     # Práva na práci s uživateli
 
+    def can_view_user(self, user: db.User) -> bool:
+        if user.is_admin or user.is_org:
+            return True
+        elif self.have_right(Right.view_all_users):
+            return True
+        elif self.have_right(Right.view_school_users):
+            schools = self.get_user_schools(Right.view_school_users)
+            if schools:
+                q = db.get_session().query(db.User).filter_by(user_id=user.user_id)
+                q = self.restrict_user_query(q, schools)
+                if q.first():
+                    return True
+            return False
+        else:
+            return False
+
     def can_edit_user(self, user: db.User) -> bool:
         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)
+        elif self.have_right(Right.edit_all_users):
+            return True
+        elif self.have_right(Right.edit_school_users):
+            schools = self.get_user_schools(Right.edit_school_users)
+            if schools:
+                q = db.get_session().query(db.User).filter_by(user_id=user.user_id)
+                q = self.restrict_user_query(q, schools)
+                if q.first():
+                    return True
+            return False
+        else:
+            return False
+
+    def get_user_schools(self, right: Right) -> Set[db.Place]:
+        """Vrátí seznam škol, kde má organizátor právo spravovat uživatele."""
+        places: Set[db.Place] = set()
+        for role in self.user_roles:
+            r = roles_by_type[role.role]
+            if right in r.rights:
+                places.add(role.place)
+        return places
+
+    def restrict_user_query(self, q: Query, schools: Set[db.Place]) -> Query:
+        """Přidá k dotazu na hledání uživatelů podmínku na školy z dané množiny."""
+        sess = db.get_session()
+        school_ids = {s.place_id for s in schools}
+        q = q.filter(or_(
+            db.User.user_id.in_(
+                sess.query(db.Participant.user_id)
+                .filter(db.Participant.school.in_(school_ids))
+                .filter(db.Participant.year >= config.CURRENT_YEAR - 1)
+            ),
+            db.User.user_id.in_(
+                sess.query(db.Participation.user_id)
+                .select_from(db.Participation)
+                .join(db.Contest)
+                .join(db.Round)
+                .filter(or_(db.Contest.place_id.in_(school_ids), db.Participation.place_id.in_(school_ids)))
+                .filter(db.Round.year == config.CURRENT_YEAR)
+            )
+        ))
+        return q
 
 
 class RoundRights(Rights):
diff --git a/mo/web/org_users.py b/mo/web/org_users.py
index 11d56053..0f8b055a 100644
--- a/mo/web/org_users.py
+++ b/mo/web/org_users.py
@@ -52,6 +52,11 @@ def org_users():
     sess = db.get_session()
     rr = g.gatekeeper.rights_generic()
 
+    if rr.have_right(Right.view_all_users):
+        schools = None
+    else:
+        schools = rr.get_user_schools(Right.view_school_users)
+
     q = sess.query(db.User).filter_by(is_admin=False, is_org=False).options(
         subqueryload(db.User.participants).joinedload(db.Participant.school_place)
     )
@@ -108,6 +113,9 @@ def org_users():
     if participation_filter_apply:
         q = q.filter(db.User.user_id.in_(participation_filter))
 
+    if schools is not None:
+        q = rr.restrict_user_query(q, schools)
+
     # print(str(q))
     (count, q) = filter.apply_limits(q, pagesize=50)
     users = q.all()
@@ -115,8 +123,9 @@ def org_users():
     return render_template(
         'org_users.html', users=users, count=count,
         filter=filter,
-        can_edit=rr.have_right(Right.edit_users),
+        can_edit=rr.have_right(Right.edit_all_users) or rr.have_right(Right.edit_school_users),
         can_add=rr.have_right(Right.add_users),
+        is_restricted=(schools is not None),
     )
 
 
@@ -265,6 +274,8 @@ def org_org(id: int):
         raise werkzeug.exceptions.NotFound()
 
     rr = g.gatekeeper.rights_generic()
+    if not rr.can_view_user(user):
+        raise werkzeug.exceptions.Forbidden()
     can_assign_rights = rr.have_right(Right.assign_rights)
 
     resend_invite_form: Optional[ResendInviteForm] = None
@@ -348,6 +359,10 @@ def org_user(id: int):
         return redirect(url_for('org_org', id=id))
 
     rr = g.gatekeeper.rights_generic()
+    can_edit = rr.can_edit_user(user)
+    can_view = can_edit or rr.can_view_user(user)   # Zkratka, abychom se vyhnuli drahému dotazu
+    if not can_view:
+        raise werkzeug.exceptions.Forbidden()
 
     resend_invite_form: Optional[ResendInviteForm] = None
     if user.last_login_at is None and rr.can_edit_user(user):
@@ -369,7 +384,7 @@ def org_user(id: int):
     )
 
     return render_template(
-        'org_user.html', user=user, can_edit=rr.can_edit_user(user),
+        'org_user.html', user=user, can_edit=can_edit,
         can_incarnate=g.user.is_admin,
         participants=participants, participations=participations,
         resend_invite_form=resend_invite_form,
diff --git a/mo/web/templates/org_users.html b/mo/web/templates/org_users.html
index c8c16ddf..b55111cf 100644
--- a/mo/web/templates/org_users.html
+++ b/mo/web/templates/org_users.html
@@ -60,6 +60,9 @@
 	{% else %}
 		<b>Nebyly nalezeny žádné záznamy soutěžících.</b>
 	{% endif %}
+	{% if is_restricted %}
+		Pozor, vidíte pouze účastníky ze svých škol.
+	{% endif %}
 	<input type="hidden" name="offset" value="{{filter.offset.data}}">
 	<input type="hidden" name="limit" value="{{filter.limit.data}}">
 </form>
-- 
GitLab