github.com/quickfeed/quickfeed@v0.0.0-20240507093252-ed8ca812a09c/web/interceptor/access_control_test.go (about) 1 package interceptor_test 2 3 import ( 4 "context" 5 "testing" 6 7 "connectrpc.com/connect" 8 "github.com/quickfeed/quickfeed/internal/qtest" 9 "github.com/quickfeed/quickfeed/qf" 10 "github.com/quickfeed/quickfeed/web" 11 "github.com/quickfeed/quickfeed/web/auth" 12 "github.com/quickfeed/quickfeed/web/interceptor" 13 ) 14 15 type accessTest struct { 16 cookie string 17 userID uint64 18 courseID uint64 19 groupID uint64 20 wantAccess bool 21 wantCode connect.Code 22 } 23 24 func TestAccessControl(t *testing.T) { 25 db, cleanup := qtest.TestDB(t) 26 defer cleanup() 27 logger := qtest.Logger(t) 28 29 tm, err := auth.NewTokenManager(db) 30 if err != nil { 31 t.Fatal(err) 32 } 33 client := web.MockClient(t, db, connect.WithInterceptors( 34 interceptor.NewUserInterceptor(logger, tm), 35 interceptor.NewAccessControlInterceptor(tm), 36 )) 37 ctx := context.Background() 38 39 courseAdmin := qtest.CreateFakeUser(t, db) 40 groupStudent := qtest.CreateFakeCustomUser(t, db, &qf.User{Name: "group student", Login: "group student"}) 41 student := qtest.CreateFakeCustomUser(t, db, &qf.User{Name: "student", Login: "student"}) 42 user := qtest.CreateFakeCustomUser(t, db, &qf.User{Name: "user", Login: "user"}) 43 admin := qtest.CreateFakeUser(t, db) 44 admin.IsAdmin = true 45 if err := db.UpdateUser(admin); err != nil { 46 t.Fatal(err) 47 } 48 49 course := &qf.Course{ 50 Code: "test101", 51 Year: 2022, 52 ScmOrganizationID: 1, 53 ScmOrganizationName: "test", 54 CourseCreatorID: courseAdmin.ID, 55 } 56 if err := db.CreateCourse(courseAdmin.ID, course); err != nil { 57 t.Fatal(err) 58 } 59 qtest.EnrollStudent(t, db, groupStudent, course) 60 qtest.EnrollStudent(t, db, student, course) 61 group := &qf.Group{ 62 CourseID: course.ID, 63 Name: "Test", 64 Users: []*qf.User{groupStudent}, 65 } 66 if err := db.CreateGroup(group); err != nil { 67 t.Fatal(err) 68 } 69 70 assignment := &qf.Assignment{ 71 CourseID: course.ID, 72 Name: "Test Assignment", 73 Order: 1, 74 } 75 if err := db.CreateAssignment(assignment); err != nil { 76 t.Fatal(err) 77 } 78 if err := db.CreateSubmission(&qf.Submission{ 79 AssignmentID: assignment.ID, 80 UserID: groupStudent.ID, 81 }); err != nil { 82 t.Fatal(err) 83 } 84 85 f := func(t *testing.T, id uint64) string { 86 cookie, err := tm.NewAuthCookie(id) 87 if err != nil { 88 t.Fatal(err) 89 } 90 return cookie.String() 91 } 92 courseAdminCookie := f(t, courseAdmin.ID) 93 groupStudentCookie := f(t, groupStudent.ID) 94 studentCookie := f(t, student.ID) 95 userCookie := f(t, user.ID) 96 adminCookie := f(t, admin.ID) 97 98 freeAccessTest := map[string]accessTest{ 99 "admin": {cookie: courseAdminCookie, courseID: course.ID, wantAccess: true, wantCode: connect.CodePermissionDenied}, 100 "student": {cookie: studentCookie, courseID: course.ID, wantAccess: true, wantCode: connect.CodePermissionDenied}, 101 "group student": {cookie: groupStudentCookie, courseID: course.ID, wantAccess: true, wantCode: connect.CodePermissionDenied}, 102 "user": {cookie: userCookie, courseID: course.ID, wantAccess: true, wantCode: connect.CodePermissionDenied}, 103 "non-teacher admin": {cookie: adminCookie, courseID: course.ID, wantAccess: true, wantCode: connect.CodePermissionDenied}, 104 "empty context": {wantAccess: false, wantCode: connect.CodeUnauthenticated}, 105 } 106 for name, tt := range freeAccessTest { 107 t.Run("UnrestrictedAccess/"+name, func(t *testing.T) { 108 _, err := client.GetUser(ctx, qtest.RequestWithCookie(&qf.Void{}, tt.cookie)) 109 checkAccess(t, "GetUser", err, tt.wantCode, tt.wantAccess) 110 _, err = client.GetCourse(ctx, qtest.RequestWithCookie(&qf.CourseRequest{CourseID: tt.courseID}, tt.cookie)) 111 checkAccess(t, "GetCourse", err, tt.wantCode, tt.wantAccess) 112 _, err = client.GetCourses(ctx, qtest.RequestWithCookie(&qf.Void{}, tt.cookie)) 113 checkAccess(t, "GetCourses", err, tt.wantCode, tt.wantAccess) 114 }) 115 } 116 117 userAccessTests := map[string]accessTest{ 118 "correct user ID": {cookie: userCookie, userID: user.ID, courseID: course.ID, wantAccess: true, wantCode: connect.CodePermissionDenied}, 119 "incorrect user ID": {cookie: userCookie, groupID: groupStudent.ID, courseID: course.ID, wantAccess: false, wantCode: connect.CodePermissionDenied}, 120 } 121 for name, tt := range userAccessTests { 122 t.Run("UserAccess/"+name, func(t *testing.T) { 123 enrol := &qf.Enrollment{ 124 CourseID: tt.courseID, 125 UserID: tt.userID, 126 } 127 enrolRequest := &qf.EnrollmentRequest{ 128 FetchMode: &qf.EnrollmentRequest_UserID{ 129 UserID: tt.userID, 130 }, 131 } 132 _, err := client.CreateEnrollment(ctx, qtest.RequestWithCookie(enrol, tt.cookie)) 133 checkAccess(t, "CreateEnrollment", err, tt.wantCode, tt.wantAccess) 134 _, err = client.UpdateCourseVisibility(ctx, qtest.RequestWithCookie(enrol, tt.cookie)) 135 checkAccess(t, "UpdateCourseVisibility", err, tt.wantCode, tt.wantAccess) 136 _, err = client.UpdateUser(ctx, qtest.RequestWithCookie(&qf.User{ID: tt.userID}, tt.cookie)) 137 checkAccess(t, "UpdateUser", err, tt.wantCode, tt.wantAccess) 138 _, err = client.GetEnrollments(ctx, qtest.RequestWithCookie(enrolRequest, tt.cookie)) 139 checkAccess(t, "GetEnrollments", err, tt.wantCode, tt.wantAccess) 140 _, err = client.UpdateUser(ctx, qtest.RequestWithCookie(&qf.User{ID: tt.userID}, tt.cookie)) 141 checkAccess(t, "UpdateUser", err, tt.wantCode, tt.wantAccess) 142 }) 143 } 144 145 studentAccessTests := map[string]accessTest{ 146 "course admin": {cookie: courseAdminCookie, userID: courseAdmin.ID, courseID: course.ID, wantAccess: true, wantCode: connect.CodePermissionDenied}, 147 "admin, not enrolled in a course": {cookie: adminCookie, userID: admin.ID, courseID: course.ID, wantAccess: false, wantCode: connect.CodePermissionDenied}, 148 "user, not enrolled in the course": {cookie: userCookie, userID: user.ID, courseID: course.ID, wantAccess: false, wantCode: connect.CodePermissionDenied}, 149 "student": {cookie: studentCookie, userID: student.ID, courseID: course.ID, wantAccess: true, wantCode: connect.CodePermissionDenied}, 150 "student of another course": {cookie: studentCookie, userID: student.ID, courseID: 123, wantAccess: false, wantCode: connect.CodePermissionDenied}, 151 } 152 for name, tt := range studentAccessTests { 153 t.Run("StudentAccess/"+name, func(t *testing.T) { 154 _, err := client.GetSubmissions(ctx, qtest.RequestWithCookie(&qf.SubmissionRequest{ 155 CourseID: tt.courseID, 156 FetchMode: &qf.SubmissionRequest_UserID{ 157 UserID: tt.userID, 158 }, 159 }, tt.cookie)) 160 checkAccess(t, "GetSubmissions", err, tt.wantCode, tt.wantAccess) 161 _, err = client.GetAssignments(ctx, qtest.RequestWithCookie(&qf.CourseRequest{CourseID: tt.courseID}, tt.cookie)) 162 checkAccess(t, "GetAssignments", err, tt.wantCode, tt.wantAccess) 163 _, err = client.GetEnrollments(ctx, qtest.RequestWithCookie(&qf.EnrollmentRequest{ 164 FetchMode: &qf.EnrollmentRequest_UserID{ 165 UserID: tt.userID, 166 }, 167 }, tt.cookie)) 168 checkAccess(t, "GetEnrollments", err, tt.wantCode, tt.wantAccess) 169 _, err = client.GetRepositories(ctx, qtest.RequestWithCookie(&qf.CourseRequest{CourseID: tt.courseID}, tt.cookie)) 170 checkAccess(t, "GetRepositories", err, tt.wantCode, tt.wantAccess) 171 }) 172 } 173 174 groupAccessTests := map[string]accessTest{ 175 "student in a group": {cookie: groupStudentCookie, userID: groupStudent.ID, courseID: course.ID, groupID: group.ID, wantAccess: true, wantCode: connect.CodePermissionDenied}, 176 "student, not in a group": {cookie: studentCookie, userID: student.ID, courseID: course.ID, groupID: group.ID, wantAccess: false, wantCode: connect.CodePermissionDenied}, 177 "student in a group, wrong group ID in request": {cookie: studentCookie, userID: student.ID, courseID: course.ID, groupID: 123, wantAccess: false, wantCode: connect.CodePermissionDenied}, 178 } 179 for name, tt := range groupAccessTests { 180 t.Run("GroupAccess/"+name, func(t *testing.T) { 181 _, err := client.GetGroup(ctx, qtest.RequestWithCookie(&qf.GroupRequest{ 182 CourseID: tt.courseID, 183 GroupID: tt.groupID, 184 }, tt.cookie)) 185 checkAccess(t, "GetGroup", err, tt.wantCode, tt.wantAccess) 186 }) 187 } 188 189 teacherAccessTests := map[string]accessTest{ 190 "course teacher": {cookie: courseAdminCookie, userID: groupStudent.ID, courseID: course.ID, groupID: group.ID, wantAccess: true, wantCode: connect.CodePermissionDenied}, 191 "student": {cookie: studentCookie, userID: student.ID, courseID: course.ID, groupID: group.ID, wantAccess: false, wantCode: connect.CodePermissionDenied}, 192 "admin, not enrolled in the course": {cookie: adminCookie, userID: admin.ID, courseID: course.ID, groupID: group.ID, wantAccess: false, wantCode: connect.CodePermissionDenied}, 193 } 194 for name, tt := range teacherAccessTests { 195 t.Run("TeacherAccess/"+name, func(t *testing.T) { 196 _, err := client.GetGroup(ctx, qtest.RequestWithCookie(&qf.GroupRequest{ 197 CourseID: tt.courseID, 198 GroupID: tt.groupID, 199 }, tt.cookie)) 200 checkAccess(t, "GetGroup", err, tt.wantCode, tt.wantAccess) 201 _, err = client.GetGroup(ctx, qtest.RequestWithCookie(&qf.GroupRequest{ 202 CourseID: tt.courseID, 203 UserID: tt.userID, 204 }, tt.cookie)) 205 checkAccess(t, "GetGroup", err, tt.wantCode, tt.wantAccess) 206 _, err = client.DeleteGroup(ctx, qtest.RequestWithCookie(&qf.GroupRequest{ 207 GroupID: tt.groupID, 208 CourseID: tt.courseID, 209 UserID: tt.userID, 210 }, tt.cookie)) 211 checkAccess(t, "DeleteGroup", err, tt.wantCode, tt.wantAccess) 212 _, err = client.UpdateGroup(ctx, qtest.RequestWithCookie(&qf.Group{CourseID: tt.courseID}, tt.cookie)) 213 checkAccess(t, "UpdateGroup", err, tt.wantCode, tt.wantAccess) 214 _, err = client.UpdateCourse(ctx, qtest.RequestWithCookie(course, tt.cookie)) 215 checkAccess(t, "UpdateCourse", err, tt.wantCode, tt.wantAccess) 216 _, err = client.UpdateEnrollments(ctx, qtest.RequestWithCookie(&qf.Enrollments{ 217 Enrollments: []*qf.Enrollment{{ID: 1, CourseID: tt.courseID}}, 218 }, tt.cookie)) 219 checkAccess(t, "UpdateEnrollments", err, tt.wantCode, tt.wantAccess) 220 _, err = client.UpdateAssignments(ctx, qtest.RequestWithCookie(&qf.CourseRequest{CourseID: tt.courseID}, tt.cookie)) 221 checkAccess(t, "UpdateAssignments", err, tt.wantCode, tt.wantAccess) 222 _, err = client.UpdateSubmission(ctx, qtest.RequestWithCookie(&qf.UpdateSubmissionRequest{SubmissionID: 1, CourseID: tt.courseID}, tt.cookie)) 223 checkAccess(t, "UpdateSubmission", err, tt.wantCode, tt.wantAccess) 224 _, err = client.UpdateSubmissions(ctx, qtest.RequestWithCookie(&qf.UpdateSubmissionsRequest{AssignmentID: 1, CourseID: tt.courseID}, tt.cookie)) 225 checkAccess(t, "UpdateSubmissions", err, tt.wantCode, tt.wantAccess) 226 _, err = client.RebuildSubmissions(ctx, qtest.RequestWithCookie(&qf.RebuildRequest{ 227 AssignmentID: 1, 228 CourseID: tt.courseID, 229 }, tt.cookie)) 230 checkAccess(t, "RebuildSubmissions", err, tt.wantCode, tt.wantAccess) 231 _, err = client.CreateBenchmark(ctx, qtest.RequestWithCookie(&qf.GradingBenchmark{CourseID: tt.courseID, AssignmentID: 1}, tt.cookie)) 232 checkAccess(t, "CreateBenchmark", err, tt.wantCode, tt.wantAccess) 233 _, err = client.UpdateBenchmark(ctx, qtest.RequestWithCookie(&qf.GradingBenchmark{CourseID: tt.courseID, AssignmentID: 1}, tt.cookie)) 234 checkAccess(t, "UpdateBenchmark", err, tt.wantCode, tt.wantAccess) 235 _, err = client.DeleteBenchmark(ctx, qtest.RequestWithCookie(&qf.GradingBenchmark{CourseID: tt.courseID, AssignmentID: 1}, tt.cookie)) 236 checkAccess(t, "DeleteBenchmark", err, tt.wantCode, tt.wantAccess) 237 _, err = client.CreateCriterion(ctx, qtest.RequestWithCookie(&qf.GradingCriterion{CourseID: tt.courseID, BenchmarkID: 1}, tt.cookie)) 238 checkAccess(t, "CreateCriterion", err, tt.wantCode, tt.wantAccess) 239 _, err = client.UpdateCriterion(ctx, qtest.RequestWithCookie(&qf.GradingCriterion{CourseID: tt.courseID, BenchmarkID: 1}, tt.cookie)) 240 checkAccess(t, "UpdateCriterion", err, tt.wantCode, tt.wantAccess) 241 _, err = client.DeleteCriterion(ctx, qtest.RequestWithCookie(&qf.GradingCriterion{CourseID: tt.courseID, BenchmarkID: 1}, tt.cookie)) 242 checkAccess(t, "DeleteCriterion", err, tt.wantCode, tt.wantAccess) 243 _, err = client.CreateReview(ctx, qtest.RequestWithCookie(&qf.ReviewRequest{ 244 CourseID: tt.courseID, 245 Review: &qf.Review{ 246 SubmissionID: 1, 247 ReviewerID: 1, 248 }, 249 }, tt.cookie)) 250 checkAccess(t, "CreateReview", err, tt.wantCode, tt.wantAccess) 251 _, err = client.UpdateReview(ctx, qtest.RequestWithCookie(&qf.ReviewRequest{ 252 CourseID: tt.courseID, 253 Review: &qf.Review{ 254 SubmissionID: 1, 255 ReviewerID: 1, 256 }, 257 }, tt.cookie)) 258 checkAccess(t, "UpdateReview", err, tt.wantCode, tt.wantAccess) 259 _, err = client.IsEmptyRepo(ctx, qtest.RequestWithCookie(&qf.RepositoryRequest{CourseID: tt.courseID}, tt.cookie)) 260 checkAccess(t, "IsEmptyRepo", err, tt.wantCode, tt.wantAccess) 261 }) 262 } 263 264 courseAdminTests := map[string]accessTest{ 265 "admin, not enrolled": {cookie: adminCookie, courseID: course.ID, wantAccess: false, wantCode: connect.CodePermissionDenied}, 266 } 267 for name, tt := range courseAdminTests { 268 t.Run("CourseAdminAccess/"+name, func(t *testing.T) { 269 _, err = client.GetSubmissionsByCourse(ctx, qtest.RequestWithCookie(&qf.SubmissionRequest{ 270 CourseID: tt.courseID, 271 }, tt.cookie)) 272 checkAccess(t, "GetSubmissionsByCourse", err, tt.wantCode, tt.wantAccess) 273 }) 274 } 275 276 adminAccessTests := map[string]accessTest{ 277 "admin (accessing own info)": {cookie: courseAdminCookie, userID: courseAdmin.ID, courseID: course.ID, groupID: group.ID, wantAccess: true, wantCode: connect.CodePermissionDenied}, 278 "admin (accessing other user's info)": {cookie: courseAdminCookie, userID: user.ID, courseID: course.ID, groupID: group.ID, wantAccess: true, wantCode: connect.CodePermissionDenied}, 279 "non admin (accessing admin's info)": {cookie: studentCookie, userID: courseAdmin.ID, courseID: course.ID, groupID: group.ID, wantAccess: false, wantCode: connect.CodePermissionDenied}, 280 "non admin (accessing other user's info)": {cookie: studentCookie, userID: user.ID, courseID: course.ID, groupID: group.ID, wantAccess: false, wantCode: connect.CodePermissionDenied}, 281 } 282 for name, tt := range adminAccessTests { 283 t.Run("AdminAccess/"+name, func(t *testing.T) { 284 _, err := client.UpdateUser(ctx, qtest.RequestWithCookie(&qf.User{ID: tt.userID}, tt.cookie)) 285 checkAccess(t, "UpdateUser", err, tt.wantCode, tt.wantAccess) 286 _, err = client.GetUsers(ctx, qtest.RequestWithCookie(&qf.Void{}, tt.cookie)) 287 checkAccess(t, "GetUsers", err, tt.wantCode, tt.wantAccess) 288 _, err = client.GetOrganization(ctx, qtest.RequestWithCookie(&qf.Organization{ScmOrganizationName: "test"}, tt.cookie)) 289 checkAccess(t, "GetOrganization", err, tt.wantCode, tt.wantAccess) 290 _, err = client.CreateCourse(ctx, qtest.RequestWithCookie(course, tt.cookie)) 291 checkAccess(t, "CreateCourse", err, tt.wantCode, tt.wantAccess) 292 }) 293 } 294 295 createGroupTests := map[string]struct { 296 cookie string 297 group *qf.Group 298 wantAccess bool 299 wantCode connect.Code 300 }{ 301 "valid student, not in the request group": {cookie: studentCookie, group: &qf.Group{ 302 CourseID: course.ID, 303 }, wantAccess: false, wantCode: connect.CodePermissionDenied}, 304 "valid student": {cookie: studentCookie, group: &qf.Group{ 305 Name: "test", 306 CourseID: course.ID, 307 Users: []*qf.User{student}, 308 }, wantAccess: true, wantCode: connect.CodePermissionDenied}, 309 "course teacher": {cookie: courseAdminCookie, group: &qf.Group{ 310 CourseID: course.ID, 311 Users: []*qf.User{courseAdmin}, 312 }, wantAccess: true, wantCode: connect.CodePermissionDenied}, 313 "admin, not a teacher": {cookie: adminCookie, group: &qf.Group{ 314 CourseID: course.ID, 315 }, wantAccess: false, wantCode: connect.CodePermissionDenied}, 316 } 317 318 for name, tt := range createGroupTests { 319 t.Run("CreateGroupAccess/"+name, func(t *testing.T) { 320 _, err := client.CreateGroup(ctx, qtest.RequestWithCookie(tt.group, tt.cookie)) 321 checkAccess(t, "CreateGroup", err, tt.wantCode, tt.wantAccess) 322 }) 323 } 324 325 adminStatusChangeTests := map[string]struct { 326 cookie string 327 user *qf.User 328 wantAccess bool 329 wantCode connect.Code 330 }{ 331 "admin demoting a user": {cookie: courseAdminCookie, user: &qf.User{ 332 ID: admin.ID, 333 IsAdmin: false, 334 }, wantAccess: true, wantCode: connect.CodePermissionDenied}, 335 "admin promoting a user": {cookie: courseAdminCookie, user: &qf.User{ 336 ID: admin.ID, 337 IsAdmin: true, 338 }, wantAccess: true, wantCode: connect.CodePermissionDenied}, 339 "admin demoting self": {cookie: courseAdminCookie, user: &qf.User{ 340 ID: courseAdmin.ID, 341 IsAdmin: false, 342 }, wantAccess: true, wantCode: connect.CodePermissionDenied}, 343 "user promoting another user": {cookie: userCookie, user: &qf.User{ 344 ID: groupStudent.ID, 345 IsAdmin: true, 346 }, wantAccess: false, wantCode: connect.CodePermissionDenied}, 347 "user promoting self": {cookie: userCookie, user: &qf.User{ 348 ID: user.ID, 349 IsAdmin: true, 350 }, wantAccess: false, wantCode: connect.CodePermissionDenied}, 351 } 352 353 for name, tt := range adminStatusChangeTests { 354 t.Run("AdminStatusChange/"+name, func(t *testing.T) { 355 _, err := client.UpdateUser(ctx, qtest.RequestWithCookie(tt.user, tt.cookie)) 356 checkAccess(t, "UpdateUser", err, tt.wantCode, tt.wantAccess) 357 }) 358 } 359 360 adminGetEnrollmentsTests := map[string]accessTest{ 361 "admin, not enrolled in the course": {cookie: adminCookie, courseID: course.ID, userID: student.ID, wantAccess: true, wantCode: connect.CodePermissionDenied}, 362 } 363 364 for name, tt := range adminGetEnrollmentsTests { 365 t.Run("AdminGetEnrollments/"+name, func(t *testing.T) { 366 _, err := client.GetEnrollments(ctx, qtest.RequestWithCookie(&qf.EnrollmentRequest{ 367 FetchMode: &qf.EnrollmentRequest_CourseID{ 368 CourseID: tt.courseID, 369 }, 370 }, tt.cookie)) 371 checkAccess(t, "GetEnrollments", err, tt.wantCode, tt.wantAccess) 372 _, err = client.GetEnrollments(ctx, qtest.RequestWithCookie(&qf.EnrollmentRequest{ 373 FetchMode: &qf.EnrollmentRequest_UserID{ 374 UserID: tt.userID, 375 }, 376 }, tt.cookie)) 377 checkAccess(t, "GetEnrollments", err, tt.wantCode, tt.wantAccess) 378 }) 379 } 380 } 381 382 func checkAccess(t *testing.T, method string, err error, wantCode connect.Code, wantAccess bool) { 383 t.Helper() 384 if connErr, ok := err.(*connect.Error); ok { 385 gotCode := connErr.Code() 386 gotAccess := gotCode == wantCode 387 if gotAccess == wantAccess { 388 t.Errorf("%23s: (%v == %v) = %t, want %t", method, gotCode, wantCode, gotAccess, !wantAccess) 389 t.Log(err) 390 } 391 } else if err != nil && wantAccess { 392 // got error and want access; expected non-error or not access 393 t.Errorf("%23s: got %v (%t), want <nil> (%t)", method, err, wantAccess, !wantAccess) 394 } 395 }