CourseSecurity.java

1
package edu.ucsb.cs156.frontiers.config;
2
3
import edu.ucsb.cs156.frontiers.entities.Course;
4
import edu.ucsb.cs156.frontiers.models.CurrentUser;
5
import edu.ucsb.cs156.frontiers.repositories.CourseRepository;
6
import edu.ucsb.cs156.frontiers.repositories.DownloadRequestRepository;
7
import edu.ucsb.cs156.frontiers.repositories.RosterStudentRepository;
8
import edu.ucsb.cs156.frontiers.services.CurrentUserService;
9
import java.util.Collection;
10
import java.util.Optional;
11
import lombok.extern.slf4j.Slf4j;
12
import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations;
13
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
14
import org.springframework.security.access.prepost.PreAuthorize;
15
import org.springframework.security.core.GrantedAuthority;
16
import org.springframework.stereotype.Component;
17
18
/**
19
 * CourseSecurity provides methods to check permissions for managing courses and roster students. It
20
 * uses the CurrentUserService to get the current user and RoleHierarchy to check roles.
21
 *
22
 * <p>The methods defined here are used as annotations (e.g. <code>
23
 *   @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #id)")</code>) in the
24
 * CourseController and RosterStudentController to enforce security checks.
25
 *
26
 * <p>Note that for a method with a courseId, you <em>still</em> need to verify in each method,
27
 * whether the course exists or not. These annotations will <em>only</em> check whether or not the
28
 * particular user has access to a particular course, for example.
29
 *
30
 * <p>When testing, use <code>@WithInstructorCoursePermissions</code> or <code>
31
 * @WithStaffCoursePermissions</code> to mock a user with the appropriate roles.
32
 */
33
@Slf4j
34
@Component("CourseSecurity")
35
public class CourseSecurity {
36
  private final CurrentUserService currentUserService;
37
  private final RoleHierarchy roleHierarchy;
38
  private final CourseRepository courseRepository;
39
  private final RosterStudentRepository rosterStudentRepository;
40
  private final DownloadRequestRepository downloadRequestRepository;
41
42
  public CourseSecurity(
43
      CurrentUserService currentUserService,
44
      RoleHierarchy roleHierarchy,
45
      CourseRepository courseRepository,
46
      RosterStudentRepository rosterStudentRepository,
47
      DownloadRequestRepository downloadRequestRepository) {
48
    this.currentUserService = currentUserService;
49
    this.roleHierarchy = roleHierarchy;
50
    this.courseRepository = courseRepository;
51
    this.rosterStudentRepository = rosterStudentRepository;
52
    this.downloadRequestRepository = downloadRequestRepository;
53
  }
54
55
  /**
56
   * Use this when you want to check whether the user is either a staff member, instructor or admin
57
   * for the course.
58
   *
59
   * @param operations
60
   * @param courseId
61
   * @return true if the user has manage permissions for the course, false otherwise.
62
   */
63
  @PreAuthorize("hasRole('ROLE_USER')")
64
  public Boolean hasManagePermissions(
65
      MethodSecurityExpressionOperations operations, Long courseId) {
66
    Optional<Course> course = courseRepository.findById(courseId);
67 1 1. hasManagePermissions : negated conditional → KILLED
    if (course.isEmpty()) {
68 1 1. hasManagePermissions : replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasManagePermissions → KILLED
      return true;
69
    }
70 2 1. hasManagePermissions : replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasManagePermissions → KILLED
2. hasManagePermissions : replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasManagePermissions → KILLED
    return baseHasManagePermissions(operations, course.get());
71
  }
72
73
  /**
74
   * Use this for operations that only an instructor can do, but not a staff member, such as adding
75
   * or deleting a course staff member.
76
   *
77
   * @param operations
78
   * @param courseId
79
   * @return true if the user has instructor permissions for the course, false otherwise.
80
   */
81
  @PreAuthorize("hasRole('ROLE_INSTRUCTOR')")
82
  public Boolean hasInstructorPermissions(
83
      MethodSecurityExpressionOperations operations, Long courseId) {
84
    CurrentUser currentUser = currentUserService.getCurrentUser();
85
    Collection<? extends GrantedAuthority> authorities =
86
        roleHierarchy.getReachableGrantedAuthorities(currentUser.getRoles());
87 3 1. hasInstructorPermissions : negated conditional → KILLED
2. lambda$hasInstructorPermissions$0 : replaced boolean return with false for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$hasInstructorPermissions$0 → KILLED
3. lambda$hasInstructorPermissions$0 : replaced boolean return with true for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$hasInstructorPermissions$0 → KILLED
    if (authorities.stream().anyMatch(role -> role.getAuthority().equals("ROLE_ADMIN"))) {
88 1 1. hasInstructorPermissions : replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasInstructorPermissions → KILLED
      return true;
89
    } else {
90
      Optional<Course> course = courseRepository.findById(courseId);
91 1 1. hasInstructorPermissions : negated conditional → KILLED
      if (course.isEmpty()) {
92 1 1. hasInstructorPermissions : replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasInstructorPermissions → KILLED
        return true;
93
      }
94 2 1. hasInstructorPermissions : replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasInstructorPermissions → KILLED
2. hasInstructorPermissions : replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasInstructorPermissions → KILLED
      return currentUser.getUser().getEmail().equals(course.get().getInstructorEmail());
95
    }
96
  }
97
98
  /**
99
   * This method checks if the current user has management permissions for the course associated
100
   * with the given rosterStudent. This allows us to create endpoints that just take a roster
101
   * student id, not a course id, and still check permissions. This one works for both staff and
102
   * instructor permissions.
103
   *
104
   * @param operations
105
   * @param rosterStudentId
106
   * @return
107
   */
108
  @PreAuthorize("hasRole('ROLE_USER')")
109
  public Boolean hasRosterStudentManagementPermissions(
110
      MethodSecurityExpressionOperations operations, Long rosterStudentId) {
111 2 1. hasRosterStudentManagementPermissions : replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasRosterStudentManagementPermissions → KILLED
2. hasRosterStudentManagementPermissions : replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasRosterStudentManagementPermissions → KILLED
    return rosterStudentRepository
112
        .findById(rosterStudentId)
113 2 1. lambda$hasRosterStudentManagementPermissions$1 : replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$hasRosterStudentManagementPermissions$1 → KILLED
2. lambda$hasRosterStudentManagementPermissions$1 : replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$hasRosterStudentManagementPermissions$1 → KILLED
        .map(rosterStudent -> baseHasManagePermissions(operations, rosterStudent.getCourse()))
114
        .orElse(true);
115
  }
116
117
  /**
118
   * This is a helper method that checks if the current user has management permissions for the
119
   * given course.
120
   *
121
   * @param operations
122
   * @param course
123
   * @return
124
   */
125
  public Boolean baseHasManagePermissions(
126
      MethodSecurityExpressionOperations operations, Course course) {
127
    CurrentUser currentUser = currentUserService.getCurrentUser();
128
    Collection<? extends GrantedAuthority> authorities =
129
        roleHierarchy.getReachableGrantedAuthorities(currentUser.getRoles());
130 3 1. baseHasManagePermissions : negated conditional → KILLED
2. lambda$baseHasManagePermissions$2 : replaced boolean return with true for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$baseHasManagePermissions$2 → KILLED
3. lambda$baseHasManagePermissions$2 : replaced boolean return with false for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$baseHasManagePermissions$2 → KILLED
    if (authorities.stream().anyMatch(role -> role.getAuthority().equals("ROLE_ADMIN"))) {
131 1 1. baseHasManagePermissions : replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::baseHasManagePermissions → KILLED
      return true;
132
    } else {
133
      if (course.getCourseStaff().stream()
134 3 1. baseHasManagePermissions : negated conditional → KILLED
2. lambda$baseHasManagePermissions$3 : replaced boolean return with false for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$baseHasManagePermissions$3 → KILLED
3. lambda$baseHasManagePermissions$3 : replaced boolean return with true for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$baseHasManagePermissions$3 → KILLED
          .anyMatch(staff -> staff.getEmail().equals(currentUser.getUser().getEmail()))) {
135 1 1. baseHasManagePermissions : replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::baseHasManagePermissions → KILLED
        return true;
136
      }
137 2 1. baseHasManagePermissions : replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::baseHasManagePermissions → KILLED
2. baseHasManagePermissions : replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::baseHasManagePermissions → KILLED
      return currentUser.getUser().getEmail().equals(course.getInstructorEmail());
138
    }
139
  }
140
141
  @PreAuthorize("hasRole('ROLE_USER')")
142
  public Boolean hasDownloadPermissions(
143
      MethodSecurityExpressionOperations operations, Long downloadId) {
144 2 1. hasDownloadPermissions : replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasDownloadPermissions → KILLED
2. hasDownloadPermissions : replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasDownloadPermissions → KILLED
    return downloadRequestRepository
145
        .findById(downloadId)
146 2 1. lambda$hasDownloadPermissions$4 : replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$hasDownloadPermissions$4 → KILLED
2. lambda$hasDownloadPermissions$4 : replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$hasDownloadPermissions$4 → KILLED
        .map(downloadRequest -> baseHasManagePermissions(operations, downloadRequest.getCourse()))
147
        .orElse(true);
148
  }
149
}

Mutations

67

1.1
Location : hasManagePermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:NotFound]/[method:null_on_null()]
negated conditional → KILLED

68

1.1
Location : hasManagePermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:NotFound]/[method:null_on_null()]
replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasManagePermissions → KILLED

70

1.1
Location : hasManagePermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:UnsuccessfulInstructor]/[method:instructor_cant_load_non_owned_course()]
replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasManagePermissions → KILLED

2.2
Location : hasManagePermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:SuccessfulAdmin]/[method:instructor_can_load_owned_course()]
replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasManagePermissions → KILLED

87

1.1
Location : hasInstructorPermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:SuccessfulAdminPerms]/[method:instructor_can_load_owned_course()]
negated conditional → KILLED

2.2
Location : lambda$hasInstructorPermissions$0
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:SuccessfulAdminPerms]/[method:instructor_can_load_owned_course()]
replaced boolean return with false for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$hasInstructorPermissions$0 → KILLED

3.3
Location : lambda$hasInstructorPermissions$0
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:UnsuccessfulInstructorPerms]/[method:instructor_cant_load_non_owned_course()]
replaced boolean return with true for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$hasInstructorPermissions$0 → KILLED

88

1.1
Location : hasInstructorPermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:SuccessfulAdminPerms]/[method:instructor_can_load_owned_course()]
replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasInstructorPermissions → KILLED

91

1.1
Location : hasInstructorPermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:NotFoundInstructor]/[method:null_on_null()]
negated conditional → KILLED

92

1.1
Location : hasInstructorPermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:NotFoundInstructor]/[method:null_on_null()]
replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasInstructorPermissions → KILLED

94

1.1
Location : hasInstructorPermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:UnsuccessfulInstructorPerms]/[method:instructor_cant_load_non_owned_course()]
replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasInstructorPermissions → KILLED

2.2
Location : hasInstructorPermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:SuccessfulInstructorPerms]/[method:instructor_can_load_owned_course()]
replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasInstructorPermissions → KILLED

111

1.1
Location : hasRosterStudentManagementPermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:CorrectPassRosterStudent]/[method:returns_properly()]
replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasRosterStudentManagementPermissions → KILLED

2.2
Location : hasRosterStudentManagementPermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:NotFoundRosterStudent]/[method:null_on_null()]
replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasRosterStudentManagementPermissions → KILLED

113

1.1
Location : lambda$hasRosterStudentManagementPermissions$1
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:CorrectPassRosterStudent]/[method:returns_properly()]
replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$hasRosterStudentManagementPermissions$1 → KILLED

2.2
Location : lambda$hasRosterStudentManagementPermissions$1
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:FalseButExistsRosterStudent]/[method:returns_properly()]
replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$hasRosterStudentManagementPermissions$1 → KILLED

130

1.1
Location : baseHasManagePermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:SuccessfulAdmin]/[method:instructor_can_load_owned_course()]
negated conditional → KILLED

2.2
Location : lambda$baseHasManagePermissions$2
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:CorrectPassDownloadRequest]/[method:returns_properly()]
replaced boolean return with true for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$baseHasManagePermissions$2 → KILLED

3.3
Location : lambda$baseHasManagePermissions$2
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:SuccessfulAdmin]/[method:instructor_can_load_owned_course()]
replaced boolean return with false for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$baseHasManagePermissions$2 → KILLED

131

1.1
Location : baseHasManagePermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:SuccessfulAdmin]/[method:instructor_can_load_owned_course()]
replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::baseHasManagePermissions → KILLED

134

1.1
Location : baseHasManagePermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:CorrectPassDownloadRequest]/[method:returns_properly()]
negated conditional → KILLED

2.2
Location : lambda$baseHasManagePermissions$3
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:SuccessfulCourseStaff]/[method:instructor_can_load_owned_course()]
replaced boolean return with false for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$baseHasManagePermissions$3 → KILLED

3.3
Location : lambda$baseHasManagePermissions$3
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:UnsuccessfulInstructor]/[method:instructor_cant_load_non_owned_course()]
replaced boolean return with true for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$baseHasManagePermissions$3 → KILLED

135

1.1
Location : baseHasManagePermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:SuccessfulCourseStaff]/[method:instructor_can_load_owned_course()]
replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::baseHasManagePermissions → KILLED

137

1.1
Location : baseHasManagePermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:SuccessfulInstructor]/[method:instructor_can_load_owned_course()]
replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::baseHasManagePermissions → KILLED

2.2
Location : baseHasManagePermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:CorrectPassDownloadRequest]/[method:returns_properly()]
replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::baseHasManagePermissions → KILLED

144

1.1
Location : hasDownloadPermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:NotFoundDownloadRequest]/[method:null_on_null()]
replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasDownloadPermissions → KILLED

2.2
Location : hasDownloadPermissions
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:CorrectPassDownloadRequest]/[method:returns_properly()]
replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::hasDownloadPermissions → KILLED

146

1.1
Location : lambda$hasDownloadPermissions$4
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:FalseButExistsDownloadRequest]/[method:returns_properly()]
replaced Boolean return with False for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$hasDownloadPermissions$4 → KILLED

2.2
Location : lambda$hasDownloadPermissions$4
Killed by : edu.ucsb.cs156.frontiers.security.CourseSecurityTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.security.CourseSecurityTests]/[nested-class:CorrectPassDownloadRequest]/[method:returns_properly()]
replaced Boolean return with True for edu/ucsb/cs156/frontiers/config/CourseSecurity::lambda$hasDownloadPermissions$4 → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0