Skip to content

Commit a2a95bb

Browse files
fix: drop stale pending reviews and scope assignment toggle to super admins (#161)
Co-authored-by: Caleb Bae <baecal000@gmail.com> (cherry picked from commit 5575463)
1 parent 61e1d02 commit a2a95bb

5 files changed

Lines changed: 133 additions & 16 deletions

File tree

‎cmd/api/reviews.go‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,19 +167,31 @@ func (app *application) batchAssignReviews(w http.ResponseWriter, r *http.Reques
167167
// getNextReview assigns and returns the next application needing review
168168
//
169169
// @Summary Get next review assignment (Admin)
170-
// @Description Automatically assigns the next submitted application needing review to the current admin and returns it
170+
// @Description Automatically assigns the next submitted application needing review to the current admin and returns it. Super admins who have disabled their review assignment toggle are refused.
171171
// @Tags admin/reviews
172172
// @Produce json
173173
// @Success 200 {object} ReviewResponse
174174
// @Failure 401 {object} object{error=string}
175-
// @Failure 403 {object} object{error=string}
175+
// @Failure 403 {object} object{error=string} "Review assignment disabled for this super admin"
176176
// @Failure 404 {object} object{error=string} "No applications need review"
177177
// @Failure 500 {object} object{error=string}
178178
// @Security CookieAuth
179179
// @Router /admin/reviews/next [get]
180180
func (app *application) getNextReview(w http.ResponseWriter, r *http.Request) {
181181
user := getUserFromContext(r.Context())
182182

183+
if user.Role == store.RoleSuperAdmin {
184+
enabled, err := app.store.Settings.GetReviewAssignmentToggle(r.Context(), user.ID)
185+
if err != nil {
186+
app.internalServerError(w, r, err)
187+
return
188+
}
189+
if !enabled {
190+
app.forbiddenResponse(w, r, errors.New("review assignment is disabled for this account"))
191+
return
192+
}
193+
}
194+
183195
reviewsPerApp, err := app.store.Settings.GetReviewsPerApplication(r.Context())
184196
if err != nil {
185197
app.internalServerError(w, r, err)

‎cmd/api/reviews_test.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,22 @@ func TestGetNextReview(t *testing.T) {
341341
mockReviews.AssertExpectations(t)
342342
mockSettings.AssertExpectations(t)
343343
})
344+
345+
t.Run("should return 403 for a super admin with assignment disabled", func(t *testing.T) {
346+
superAdmin := newSuperAdminUser()
347+
348+
mockSettings.On("GetReviewAssignmentToggle", superAdmin.ID).Return(false, nil).Once()
349+
350+
req, err := http.NewRequest(http.MethodGet, "/", nil)
351+
require.NoError(t, err)
352+
req = setUserContext(req, superAdmin)
353+
354+
rr := executeRequest(req, http.HandlerFunc(app.getNextReview))
355+
checkResponseCode(t, http.StatusForbidden, rr.Code)
356+
357+
mockReviews.AssertNotCalled(t, "AssignNextForAdmin", superAdmin.ID, 3)
358+
mockSettings.AssertExpectations(t)
359+
})
344360
}
345361

346362
func TestBatchAssignReviews(t *testing.T) {

‎docs/docs.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,7 +1101,7 @@ const docTemplate = `{
11011101
"CookieAuth": []
11021102
}
11031103
],
1104-
"description": "Automatically assigns the next submitted application needing review to the current admin and returns it",
1104+
"description": "Automatically assigns the next submitted application needing review to the current admin and returns it. Super admins who have disabled their review assignment toggle are refused.",
11051105
"produces": [
11061106
"application/json"
11071107
],
@@ -1128,7 +1128,7 @@ const docTemplate = `{
11281128
}
11291129
},
11301130
"403": {
1131-
"description": "Forbidden",
1131+
"description": "Review assignment disabled for this super admin",
11321132
"schema": {
11331133
"type": "object",
11341134
"properties": {

‎internal/store/reviews.go‎

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ func (s *ApplicationReviewsStore) GetTravelStatusByReviewID(ctx context.Context,
131131
}
132132

133133
// GetPendingByAdminID returns all reviews assigned to an admin that haven't been voted on yet,
134-
// including application details for display
134+
// including application details for display. Reviews on applications that have
135+
// already been decided are omitted; the next BatchAssign removes them.
135136
func (s *ApplicationReviewsStore) GetPendingByAdminID(ctx context.Context, adminID string) ([]ApplicationReviewWithDetails, error) {
136137
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration)
137138
defer cancel()
@@ -155,7 +156,7 @@ func (s *ApplicationReviewsStore) GetPendingByAdminID(ctx context.Context, admin
155156
FROM application_reviews ar
156157
JOIN applications a ON ar.application_id = a.id
157158
JOIN users u ON a.user_id = u.id
158-
WHERE ar.admin_id = $1 AND ar.vote IS NULL
159+
WHERE ar.admin_id = $1 AND ar.vote IS NULL AND a.status = 'submitted'
159160
ORDER BY ar.assigned_at ASC
160161
`
161162

@@ -295,8 +296,11 @@ type BatchAssignmentResult struct {
295296
ReviewsUnfilled int `json:"reviews_unfilled"`
296297
}
297298

298-
// BatchAssign recovers inaccessible pending reviews and fills submitted
299+
// BatchAssign recovers pending reviews that can no longer be acted on (reviewer
300+
// disabled or demoted, application already decided) and fills submitted
299301
// applications' assignment targets with distinct, currently eligible reviewers.
302+
// The assignment toggle only applies to super admins; entries for users who
303+
// no longer hold that role are dropped so a demoted user is a regular admin.
300304
func (s *ApplicationReviewsStore) BatchAssign(ctx context.Context, reviewsPerApp int) (*BatchAssignmentResult, error) {
301305
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration*2)
302306
defer cancel()
@@ -336,10 +340,14 @@ func (s *ApplicationReviewsStore) BatchAssign(ctx context.Context, reviewsPerApp
336340
result := &BatchAssignmentResult{ReviewsPerApplication: reviewsPerApp}
337341
removed, err := tx.ExecContext(ctx, `
338342
DELETE FROM application_reviews ar
339-
WHERE ar.vote IS NULL AND (
340-
ar.admin_id::text = ANY($1::text[]) OR NOT EXISTS (
343+
USING applications a
344+
WHERE a.id = ar.application_id AND ar.vote IS NULL AND (
345+
a.status <> 'submitted' OR NOT EXISTS (
341346
SELECT 1 FROM users u
342-
WHERE u.id = ar.admin_id AND u.role IN ('admin', 'super_admin')
347+
WHERE u.id = ar.admin_id AND (
348+
u.role = 'admin' OR
349+
(u.role = 'super_admin' AND NOT (u.id::text = ANY($1::text[])))
350+
)
343351
)
344352
)
345353
`, disabledIDs)
@@ -354,7 +362,8 @@ func (s *ApplicationReviewsStore) BatchAssign(ctx context.Context, reviewsPerApp
354362

355363
// Read workloads after cleanup. Creation time and ID provide stable ties.
356364
adminRows, err := tx.QueryContext(ctx, `
357-
SELECT u.id, u.role, COUNT(ar.id), NOT (u.id::text = ANY($1::text[]))
365+
SELECT u.id, u.role, COUNT(ar.id),
366+
NOT (u.role = 'super_admin' AND u.id::text = ANY($1::text[]))
358367
FROM users u
359368
LEFT JOIN application_reviews ar ON ar.admin_id = u.id AND ar.vote IS NULL
360369
WHERE u.role IN ('admin', 'super_admin')
@@ -370,15 +379,19 @@ func (s *ApplicationReviewsStore) BatchAssign(ctx context.Context, reviewsPerApp
370379
Pending int
371380
}
372381
var admins []reviewer
382+
superAdmins := make(map[string]bool)
373383
for adminRows.Next() {
374384
var admin reviewer
375385
var role UserRole
376386
var enabled bool
377387
if err := adminRows.Scan(&admin.ID, &role, &admin.Pending, &enabled); err != nil {
378388
return nil, err
379389
}
380-
if role == RoleSuperAdmin && !listed[admin.ID] {
381-
entries = append(entries, ReviewAssignmentEntry{ID: admin.ID, Enabled: true})
390+
if role == RoleSuperAdmin {
391+
superAdmins[admin.ID] = true
392+
if !listed[admin.ID] {
393+
entries = append(entries, ReviewAssignmentEntry{ID: admin.ID, Enabled: true})
394+
}
382395
}
383396
if enabled {
384397
admins = append(admins, admin)
@@ -389,7 +402,15 @@ func (s *ApplicationReviewsStore) BatchAssign(ctx context.Context, reviewsPerApp
389402
}
390403
adminRows.Close()
391404

392-
// Normalize legacy settings and retain the super-admin backfill.
405+
// Normalize legacy settings, retain the super-admin backfill, and drop
406+
// entries for users who are no longer super admins.
407+
current := entries[:0]
408+
for _, entry := range entries {
409+
if superAdmins[entry.ID] {
410+
current = append(current, entry)
411+
}
412+
}
413+
entries = current
393414
encoded, err := json.Marshal(entries)
394415
if err != nil {
395416
return nil, err

‎internal/store/reviews_assignment_integration_test.go‎

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,11 @@ func TestIntegrationBatchAssign(t *testing.T) {
292292
if _, err := (&UsersStore{db: db}).UpdateRole(ctx, admins[0], RoleHacker); err != nil {
293293
t.Fatal(err)
294294
}
295-
} else if err := (&SettingsStore{db: db}).SetReviewAssignmentToggle(ctx, admins[0], false); err != nil {
296-
t.Fatal(err)
295+
} else {
296+
batchTestExec(t, db, "UPDATE users SET role='super_admin' WHERE id=$1", admins[0])
297+
if err := (&SettingsStore{db: db}).SetReviewAssignmentToggle(ctx, admins[0], false); err != nil {
298+
t.Fatal(err)
299+
}
297300
}
298301
r, err := s.BatchAssign(ctx, 2)
299302
if err != nil {
@@ -352,6 +355,71 @@ func TestIntegrationBatchAssign(t *testing.T) {
352355
}
353356
}
354357
})
358+
t.Run("decided_application_pending_reviews_removed", func(t *testing.T) {
359+
db, s, admins, apps := batchTestSeed(t, 2, 2)
360+
batchTestBatch(t, s, 2)
361+
pending, err := s.GetPendingByAdminID(ctx, admins[0])
362+
if err != nil || len(pending) != 2 {
363+
t.Fatalf("pending=%d err=%v", len(pending), err)
364+
}
365+
var decidedReview string
366+
for _, p := range pending {
367+
if p.ApplicationID == apps[0] {
368+
decidedReview = p.ID
369+
}
370+
}
371+
if _, err := s.SubmitVote(ctx, decidedReview, admins[0], ReviewVoteAccept, nil, nil); err != nil {
372+
t.Fatal(err)
373+
}
374+
if _, err := (&ApplicationsStore{db: db}).SetStatus(ctx, apps[0], StatusAccepted); err != nil {
375+
t.Fatal(err)
376+
}
377+
// The queue hides the decided application before any batch runs.
378+
for _, admin := range admins {
379+
pending, err := s.GetPendingByAdminID(ctx, admin)
380+
if err != nil || len(pending) != 1 || pending[0].ApplicationID != apps[1] {
381+
t.Fatalf("admin queue after decision=%+v err=%v", pending, err)
382+
}
383+
}
384+
r, err := s.BatchAssign(ctx, 2)
385+
if err != nil {
386+
t.Fatal(err)
387+
}
388+
want := BatchAssignmentResult{ReviewsRemoved: 1, ReviewsPerApplication: 2}
389+
if *r != want {
390+
t.Errorf("result=%+v, want %+v", *r, want)
391+
}
392+
if n := batchTestCount(t, db, "SELECT count(*) FROM application_reviews WHERE application_id=$1 AND vote IS NOT NULL", apps[0]); n != 1 {
393+
t.Errorf("completed reviews on decided application=%d, want 1", n)
394+
}
395+
if n := batchTestCount(t, db, "SELECT count(*) FROM application_reviews WHERE application_id=$1 AND vote IS NULL", apps[0]); n != 0 {
396+
t.Errorf("pending reviews on decided application=%d, want 0", n)
397+
}
398+
})
399+
t.Run("demoted_disabled_super_admin_becomes_eligible_admin", func(t *testing.T) {
400+
db, s, admins, _ := batchTestSeed(t, 2, 2)
401+
batchTestExec(t, db, "UPDATE users SET role='super_admin' WHERE id=$1", admins[0])
402+
settings := &SettingsStore{db: db}
403+
if err := settings.SetReviewAssignmentToggle(ctx, admins[0], false); err != nil {
404+
t.Fatal(err)
405+
}
406+
if n := batchTestBatch(t, s, 2); n != 2 {
407+
t.Fatalf("created=%d, want 2 (only the enabled admin)", n)
408+
}
409+
if _, err := (&UsersStore{db: db}).UpdateRole(ctx, admins[0], RoleAdmin); err != nil {
410+
t.Fatal(err)
411+
}
412+
if n := batchTestBatch(t, s, 2); n != 2 {
413+
t.Errorf("created=%d, want 2 for the demoted reviewer", n)
414+
}
415+
if n := batchTestCount(t, db, "SELECT count(*) FROM application_reviews WHERE admin_id=$1", admins[0]); n != 2 {
416+
t.Errorf("demoted reviewer assignments=%d, want 2", n)
417+
}
418+
entries, err := settings.GetAllReviewAssignmentToggles(ctx)
419+
if err != nil || len(entries) != 0 {
420+
t.Errorf("stale toggle entries=%+v err=%v", entries, err)
421+
}
422+
})
355423
t.Run("simultaneous_batches", func(t *testing.T) {
356424
for _, absent := range []bool{false, true} {
357425
t.Run(fmt.Sprint(absent), func(t *testing.T) {

0 commit comments

Comments
 (0)