Coverage for rfpy/api/validate.py: 99%
76 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-24 10:52 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-24 10:52 +0000
1"""
2Validate that a given user can see various objects
3or perform various tasks
4"""
6from typing import Optional
7from rfpy.auth import AuthorizationFailure, ValidationErrors
8from rfpy.auth import perms
9from rfpy.model.project import Project
10from rfpy.model.issue import Issue
11from rfpy.model.humans import (
12 Organisation,
13 OrganisationType,
14 User,
15 ConsultantOrganisation,
16)
17from rfpy.auth.actions import (
18 PROJECT_ACTIONS,
19 QUESTIONNAIRE_ACTIONS,
20 ADMIN_ACTIONS,
21 ISSUE_ACTIONS,
22 ISSUE_STATUS_ACTIONS,
23 PROJECT_STATUS_ACTIONS,
24)
27def check(
28 user: User,
29 action: str,
30 project: Optional[Project] = None,
31 issue: Optional[Issue] = None,
32 multiproject: bool = False,
33 section_id: Optional[int] = None,
34 target_user: Optional[User] = None,
35 target_org: Optional[Organisation] = None,
36 deny_restricted: bool = True,
37):
38 """
39 Centralised statement of auth business logic, all API calls should invoke this
41 @param multiproject - some actions cover mutliple projects, thus it's not possible
42 to pass the Project into this function
43 """
45 if action not in perms.ALL_PERMISSIONS:
46 raise ValueError(f"Action {action} is not associated with a valid permission")
48 e = ValidationErrors(action)
50 if deny_restricted and user.is_restricted:
51 e.auth_failure("Action not permitted for Restricted (Domain Expert) users")
53 if not user.has_permission(action):
54 e.auth_failure(f"User lacks permission for {action}")
56 if action in PROJECT_ACTIONS and not multiproject:
57 if project is None:
58 raise ValueError("project must be provided if checking project actions")
59 if user.org_id != project.org_id:
60 # Participant, not owner org checks
61 if user.organisation not in project.participants:
62 e.auth_failure(
63 f"{user.organisation} is not a participant in project {project.title}"
64 )
66 if action not in project.participant_role_permissions(user):
67 e.auth_failure(
68 f"Participant {user.organisation} lacks permission to {action} in project {project}"
69 )
71 if user.is_restricted and user not in project.restricted_users:
72 e.auth_failure("User not granted permissions to this project")
74 if action not in PROJECT_STATUS_ACTIONS[project.status]:
75 e.invalid_project_status(project.status)
77 project_rules = ProjectRules(project, user, e)
78 project_rules.check(action)
80 if action in QUESTIONNAIRE_ACTIONS:
81 if user.is_restricted:
82 if section_id is None:
83 raise ValueError(
84 "section_id must be provided to validate() for "
85 + "QUESTIONNAIRE ACTION with a Restricted User"
86 )
87 else:
88 if not user.can_view_section_id(section_id):
89 e.auth_failure(
90 f"User has not been granted permission to section {section_id}"
91 )
93 if section_id is not None:
94 if project is None:
95 raise ValueError(
96 "project must be provided if checking questionnaire actions for a section"
97 )
98 if not project.contains_section_id(section_id):
99 e.auth_failure(
100 f"Section ID {section_id} does not belong to "
101 + f"project {project.id} ({project.title})"
102 )
104 if action in ISSUE_ACTIONS:
105 if issue is None:
106 if action not in ISSUE_STATUS_ACTIONS["__new__"] | PROJECT_ACTIONS:
107 e.auth_failure(f"Action '{action}' not permitted for a new Issue ")
108 elif action not in ISSUE_STATUS_ACTIONS[issue.status]:
109 e.invalid_issue_status(issue.status)
111 if action in ADMIN_ACTIONS and user.organisation is not target_org:
112 if target_org is None and target_user is None:
113 raise ValueError(
114 "Either target_org or target_user must be set for admin validation"
115 )
117 if not user.organisation.is_consultant:
118 e.auth_failure(
119 "Only consultants can perform operations on users in a different org"
120 )
121 else:
122 if target_org is None:
123 if target_user is None:
124 raise ValueError("target_user must be set if target_org is not")
125 target_org = target_user.organisation
126 elif target_user is not None and target_user.organisation is not target_org:
127 raise ValueError(
128 f"Target org '{target_org.id}' must be the same as that of user: "
129 f"{target_user.id}, ({target_user.org_id})"
130 )
131 assert isinstance(user.organisation, ConsultantOrganisation)
132 if (
133 target_org is not user.organisation
134 and target_org.type is not OrganisationType.RESPONDENT
135 and target_org not in user.organisation.clients
136 ):
137 e.auth_failure(
138 f"Organisation {target_org.id} is not a client of consultant {user.org_id}"
139 )
141 if e.has_errors:
142 raise AuthorizationFailure(errors=e)
145class ProjectRules(object):
146 """Business logic applying to specific project settings"""
148 def __init__(self, project, user, errors):
149 self.project = project
150 self.user = user
151 self.errors = errors
153 def check(self, action):
154 actionCheck = getattr(self, action, None)
155 if actionCheck is not None:
156 actionCheck()
158 def viewAnswers(self):
159 if self.project.hide_responses and not self.project.deadline_passed:
160 self.errors.auth_failure(
161 "Cannot view answers until the after the Project deadline"
162 )
164 if self.project.lock_issues:
165 self.errors.auth_failure(
166 "Cannot view answers while the Project is set to Lock Responses"
167 )