From 2f73b601220bdfb82f077242946f79b62ffdb718 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 19 May 2026 01:09:42 -0700 Subject: [PATCH] Allow ADMIN users to bulk deactivate and reactivate by role. Platform ADMIN callers no longer hit 403 on these endpoints; bulk changes to platform users.role ADMIN remain restricted to SUPER_ADMIN, while team_members.team_role ADMIN is still eligible under path ADMIN. Co-authored-by: Cursor --- docs/docs.go | 8 ++++---- docs/swagger.json | 8 ++++---- docs/swagger.yaml | 13 +++++++++---- internal/web_server/handlers/admin.go | 27 ++++++++++++++++++--------- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 9022e6d..4f0bc0a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -512,7 +512,7 @@ const docTemplate = `{ "Bearer": [] } ], - "description": "Sets all platform users with the given users.role to DEACTIVATED (except the caller) and all team_members with the given team_role to inactive. The path role must match a valid learner role or team role string (e.g. STUDENT, INSTRUCTOR, ADMIN, CONTENT_MANAGER). SUPER_ADMIN cannot be bulk-deactivated. Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself).", + "description": "Sets all platform users with the given users.role to DEACTIVATED (except the caller) and all team_members with the given team_role to inactive. The path role must match a valid learner role or team role string (e.g. STUDENT, INSTRUCTOR, ADMIN, CONTENT_MANAGER). SUPER_ADMIN cannot be bulk-deactivated. ADMIN platform users must use SUPER_ADMIN to bulk change other platform ADMIN users (team_members with team_role ADMIN under path ADMIN remain allowed). Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself).", "consumes": [ "application/json" ], @@ -522,7 +522,7 @@ const docTemplate = `{ "tags": [ "admin" ], - "summary": "Bulk deactivate accounts by role (SUPER_ADMIN only)", + "summary": "Bulk deactivate accounts by role (SUPER_ADMIN or ADMIN platform users only)", "parameters": [ { "type": "string", @@ -575,7 +575,7 @@ const docTemplate = `{ "Bearer": [] } ], - "description": "Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path role must be a valid platform or team role. SUPER_ADMIN cannot be bulk changed. Matches only users currently DEACTIVATED and team rows currently inactive.", + "description": "Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path role must be a valid platform or team role. SUPER_ADMIN cannot be bulk changed. ADMIN callers cannot bulk change other platform ADMIN users (team_members ADMIN under path ADMIN is allowed). Matches only users currently DEACTIVATED and team rows currently inactive.", "consumes": [ "application/json" ], @@ -585,7 +585,7 @@ const docTemplate = `{ "tags": [ "admin" ], - "summary": "Bulk reactivate accounts by role (SUPER_ADMIN only)", + "summary": "Bulk reactivate accounts by role (SUPER_ADMIN or ADMIN platform users only)", "parameters": [ { "type": "string", diff --git a/docs/swagger.json b/docs/swagger.json index 50cdbdc..f3c3d48 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -504,7 +504,7 @@ "Bearer": [] } ], - "description": "Sets all platform users with the given users.role to DEACTIVATED (except the caller) and all team_members with the given team_role to inactive. The path role must match a valid learner role or team role string (e.g. STUDENT, INSTRUCTOR, ADMIN, CONTENT_MANAGER). SUPER_ADMIN cannot be bulk-deactivated. Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself).", + "description": "Sets all platform users with the given users.role to DEACTIVATED (except the caller) and all team_members with the given team_role to inactive. The path role must match a valid learner role or team role string (e.g. STUDENT, INSTRUCTOR, ADMIN, CONTENT_MANAGER). SUPER_ADMIN cannot be bulk-deactivated. ADMIN platform users must use SUPER_ADMIN to bulk change other platform ADMIN users (team_members with team_role ADMIN under path ADMIN remain allowed). Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself).", "consumes": [ "application/json" ], @@ -514,7 +514,7 @@ "tags": [ "admin" ], - "summary": "Bulk deactivate accounts by role (SUPER_ADMIN only)", + "summary": "Bulk deactivate accounts by role (SUPER_ADMIN or ADMIN platform users only)", "parameters": [ { "type": "string", @@ -567,7 +567,7 @@ "Bearer": [] } ], - "description": "Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path role must be a valid platform or team role. SUPER_ADMIN cannot be bulk changed. Matches only users currently DEACTIVATED and team rows currently inactive.", + "description": "Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path role must be a valid platform or team role. SUPER_ADMIN cannot be bulk changed. ADMIN callers cannot bulk change other platform ADMIN users (team_members ADMIN under path ADMIN is allowed). Matches only users currently DEACTIVATED and team rows currently inactive.", "consumes": [ "application/json" ], @@ -577,7 +577,7 @@ "tags": [ "admin" ], - "summary": "Bulk reactivate accounts by role (SUPER_ADMIN only)", + "summary": "Bulk reactivate accounts by role (SUPER_ADMIN or ADMIN platform users only)", "parameters": [ { "type": "string", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f497b0a..366afbd 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2881,6 +2881,8 @@ paths: (except the caller) and all team_members with the given team_role to inactive. The path role must match a valid learner role or team role string (e.g. STUDENT, INSTRUCTOR, ADMIN, CONTENT_MANAGER). SUPER_ADMIN cannot be bulk-deactivated. + ADMIN platform users must use SUPER_ADMIN to bulk change other platform ADMIN + users (team_members with team_role ADMIN under path ADMIN remain allowed). Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself). parameters: @@ -2915,7 +2917,8 @@ paths: $ref: '#/definitions/domain.ErrorResponse' security: - Bearer: [] - summary: Bulk deactivate accounts by role (SUPER_ADMIN only) + summary: Bulk deactivate accounts by role (SUPER_ADMIN or ADMIN platform users + only) tags: - admin /api/v1/admin/roles/{role}/bulk-reactivate: @@ -2925,8 +2928,9 @@ paths: description: Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path role must be a valid platform or team role. SUPER_ADMIN - cannot be bulk changed. Matches only users currently DEACTIVATED and team - rows currently inactive. + cannot be bulk changed. ADMIN callers cannot bulk change other platform ADMIN + users (team_members ADMIN under path ADMIN is allowed). Matches only users + currently DEACTIVATED and team rows currently inactive. parameters: - description: Role key (matches users.role and/or team_members.team_role) in: path @@ -2959,7 +2963,8 @@ paths: $ref: '#/definitions/domain.ErrorResponse' security: - Bearer: [] - summary: Bulk reactivate accounts by role (SUPER_ADMIN only) + summary: Bulk reactivate accounts by role (SUPER_ADMIN or ADMIN platform users + only) tags: - admin /api/v1/admin/users/{user_id}/lms-learning-activity: diff --git a/internal/web_server/handlers/admin.go b/internal/web_server/handlers/admin.go index afff6a1..ed7cc99 100644 --- a/internal/web_server/handlers/admin.go +++ b/internal/web_server/handlers/admin.go @@ -382,8 +382,8 @@ func (h *Handler) UpdateAdmin(c *fiber.Ctx) error { } // BulkDeactivateAccountsByRole godoc -// @Summary Bulk deactivate accounts by role (SUPER_ADMIN only) -// @Description Sets all platform users with the given users.role to DEACTIVATED (except the caller) and all team_members with the given team_role to inactive. The path role must match a valid learner role or team role string (e.g. STUDENT, INSTRUCTOR, ADMIN, CONTENT_MANAGER). SUPER_ADMIN cannot be bulk-deactivated. Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself). +// @Summary Bulk deactivate accounts by role (SUPER_ADMIN or ADMIN platform users only) +// @Description Sets all platform users with the given users.role to DEACTIVATED (except the caller) and all team_members with the given team_role to inactive. The path role must match a valid learner role or team role string (e.g. STUDENT, INSTRUCTOR, ADMIN, CONTENT_MANAGER). SUPER_ADMIN cannot be bulk-deactivated. ADMIN platform users must use SUPER_ADMIN to bulk change other platform ADMIN users (team_members with team_role ADMIN under path ADMIN remain allowed). Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself). // @Tags admin // @Accept json // @Produce json @@ -403,10 +403,10 @@ func (h *Handler) BulkDeactivateAccountsByRole(c *fiber.Ctx) error { Error: "role not found in context", }) } - if callerRole != domain.RoleSuperAdmin { + if callerRole != domain.RoleSuperAdmin && callerRole != domain.RoleAdmin { return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ Message: "Forbidden", - Error: "only SUPER_ADMIN platform users may bulk deactivate by role", + Error: "only SUPER_ADMIN or ADMIN platform users may bulk deactivate by role", }) } @@ -431,7 +431,6 @@ func (h *Handler) BulkDeactivateAccountsByRole(c *fiber.Ctx) error { Error: "SUPER_ADMIN cannot be bulk deactivated", }) } - validUserRole := domain.Role(roleKey).IsValid() validTeamRole := domain.TeamRole(roleKey).IsValid() if !validUserRole && !validTeamRole { @@ -441,6 +440,11 @@ func (h *Handler) BulkDeactivateAccountsByRole(c *fiber.Ctx) error { }) } + // Non-super-admins cannot bulk change other platform ADMIN users (same role); team_members ADMIN is still allowed. + if callerRole != domain.RoleSuperAdmin && roleKey == string(domain.RoleAdmin) { + validUserRole = false + } + var req domain.BulkAccountsByRoleRequest if len(c.Body()) > 0 { if err := c.BodyParser(&req); err != nil { @@ -504,8 +508,8 @@ func (h *Handler) BulkDeactivateAccountsByRole(c *fiber.Ctx) error { } // BulkReactivateAccountsByRole godoc -// @Summary Bulk reactivate accounts by role (SUPER_ADMIN only) -// @Description Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path role must be a valid platform or team role. SUPER_ADMIN cannot be bulk changed. Matches only users currently DEACTIVATED and team rows currently inactive. +// @Summary Bulk reactivate accounts by role (SUPER_ADMIN or ADMIN platform users only) +// @Description Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path role must be a valid platform or team role. SUPER_ADMIN cannot be bulk changed. ADMIN callers cannot bulk change other platform ADMIN users (team_members ADMIN under path ADMIN is allowed). Matches only users currently DEACTIVATED and team rows currently inactive. // @Tags admin // @Accept json // @Produce json @@ -525,10 +529,10 @@ func (h *Handler) BulkReactivateAccountsByRole(c *fiber.Ctx) error { Error: "role not found in context", }) } - if callerRole != domain.RoleSuperAdmin { + if callerRole != domain.RoleSuperAdmin && callerRole != domain.RoleAdmin { return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ Message: "Forbidden", - Error: "only SUPER_ADMIN platform users may bulk reactivate by role", + Error: "only SUPER_ADMIN or ADMIN platform users may bulk reactivate by role", }) } @@ -563,6 +567,11 @@ func (h *Handler) BulkReactivateAccountsByRole(c *fiber.Ctx) error { }) } + // Non-super-admins cannot bulk change other platform ADMIN users; team_members ADMIN is still allowed. + if callerRole != domain.RoleSuperAdmin && roleKey == string(domain.RoleAdmin) { + validUserRole = false + } + var req domain.BulkAccountsByRoleRequest if len(c.Body()) > 0 { if err := c.BodyParser(&req); err != nil {