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

1""" 

2Validate that a given user can see various objects 

3or perform various tasks 

4""" 

5 

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) 

25 

26 

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 

40 

41 @param multiproject - some actions cover mutliple projects, thus it's not possible 

42 to pass the Project into this function 

43 """ 

44 

45 if action not in perms.ALL_PERMISSIONS: 

46 raise ValueError(f"Action {action} is not associated with a valid permission") 

47 

48 e = ValidationErrors(action) 

49 

50 if deny_restricted and user.is_restricted: 

51 e.auth_failure("Action not permitted for Restricted (Domain Expert) users") 

52 

53 if not user.has_permission(action): 

54 e.auth_failure(f"User lacks permission for {action}") 

55 

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 ) 

65 

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 ) 

70 

71 if user.is_restricted and user not in project.restricted_users: 

72 e.auth_failure("User not granted permissions to this project") 

73 

74 if action not in PROJECT_STATUS_ACTIONS[project.status]: 

75 e.invalid_project_status(project.status) 

76 

77 project_rules = ProjectRules(project, user, e) 

78 project_rules.check(action) 

79 

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 ) 

92 

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 ) 

103 

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) 

110 

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 ) 

116 

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 ) 

140 

141 if e.has_errors: 

142 raise AuthorizationFailure(errors=e) 

143 

144 

145class ProjectRules(object): 

146 """Business logic applying to specific project settings""" 

147 

148 def __init__(self, project, user, errors): 

149 self.project = project 

150 self.user = user 

151 self.errors = errors 

152 

153 def check(self, action): 

154 actionCheck = getattr(self, action, None) 

155 if actionCheck is not None: 

156 actionCheck() 

157 

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 ) 

163 

164 if self.project.lock_issues: 

165 self.errors.auth_failure( 

166 "Cannot view answers while the Project is set to Lock Responses" 

167 )