Merged in jmathison/DEVOPS-722 (pull request #226)

DEVOPS-722 enforce team management visibility

* DEVOPS-722 enforce team management visibility

* DEVOPS-722 reduce admin user list complexity

* DEVOPS-722 cover admin access helpers

* DEVOPS-722 address team management PR feedback

* DEVOPS-722 fix admin access tests in CI

* DEVOPS-722 fix remaining admin PR feedback
This commit is contained in:
Jacob Mathison
2026-05-12 16:49:12 +00:00
parent a080ca59d8
commit e3d9047143
10 changed files with 1667 additions and 528 deletions
+168 -47
View File
@@ -18,6 +18,12 @@ import (
"github.com/oapi-codegen/runtime/types"
)
const (
adminCognitoListPageSize = 60
maxAdminCognitoListPages = 100
maxAdminCognitoListUsers = 6000
)
// getAdminUserForAudit retrieves the admin user information for audit logging.
// When DISABLE_AUTH is set to "true", it returns a default system user for audit purposes.
// Otherwise, it attempts to extract user info from the JWT token in the request.
@@ -95,6 +101,12 @@ func (ctrl *Controllers) CreateAdminUser(ctx echo.Context) error {
})
}
httpClient := &http.Client{Timeout: 10 * time.Second}
permitConfig := ctrl.getPermitConfig()
if !canAdminModeCreateUser(ctrl.getAdminTeamMode(ctx, httpClient, permitConfig), string(req.Email), req.Roles) {
return adminUserForbidden(ctx)
}
// Create AWS config and Cognito client
awsCfg, err := aws.GetAWSConfig(context.Background())
if err != nil {
@@ -204,9 +216,6 @@ func (ctrl *Controllers) CreateAdminUser(ctx echo.Context) error {
)
// Create user in Permit.io
httpClient := &http.Client{Timeout: 10 * time.Second}
permitConfig := ctrl.getPermitConfig()
permitSynced := false
err = usermanagement.CreatePermitUser(httpClient, permitConfig, cognitoUser)
if err != nil {
@@ -354,14 +363,11 @@ func (ctrl *Controllers) GetAdminUser(ctx echo.Context, email UserEmail) error {
})
}
// Get user roles from Permit.io
httpClient := &http.Client{Timeout: 10 * time.Second}
permitConfig := ctrl.getPermitConfig()
roles, err := usermanagement.GetUserRoles(httpClient, permitConfig, cognitoUser.SubjectID)
if err != nil {
ctrl.cfg.GetAuthLogger().Warn("Failed to get user roles from Permit.io", "error", err, "email", email)
targetAccess := ctrl.getAdminTargetAccess(ctx, email, cognitoUser)
if !targetAccess.canRead(cognitoUser.Email) {
return adminUserNotFound(ctx, string(email))
}
roles := targetAccess.roles
// Log successful retrieval
usermanagement.LogUserListAction(
@@ -422,6 +428,11 @@ func (ctrl *Controllers) CheckAdminUserExists(ctx echo.Context, email UserEmail)
return ctx.NoContent(http.StatusNotFound)
}
targetAccess := ctrl.getAdminTargetAccess(ctx, email, cognitoUser)
if !targetAccess.canRead(cognitoUser.Email) {
return ctx.NoContent(http.StatusNotFound)
}
return ctx.NoContent(http.StatusOK)
}
@@ -453,43 +464,34 @@ func (ctrl *Controllers) ListAdminUsers(ctx echo.Context, params ListAdminUsersP
Region: ctrl.cfg.GetAuthRegion(),
}
// Set default pagination parameters
page := int32(1)
pageSize := int32(50)
if params.Page != nil && *params.Page > 0 {
page = *params.Page
}
if params.PageSize != nil && *params.PageSize > 0 && *params.PageSize <= 60 {
pageSize = *params.PageSize
}
page, pageSize := adminUserListPagination(params)
// List users from Cognito (note: Cognito doesn't support traditional page numbers, only pagination tokens)
// For simplicity, we'll just return the first page
result, err := usermanagement.ListCognitoUsers(context.Background(), cognitoClient, cognitoConfig.UserPoolID, int(pageSize), nil)
allUsers, err := listAllAdminCognitoUsers(cognitoClient, cognitoConfig.UserPoolID)
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to list users from Cognito", "error", err)
return ctx.JSON(http.StatusBadGateway, ErrorMessage{
Message: fmt.Sprintf("Failed to list users from Cognito: %v", err),
})
}
filteredUsers := usermanagement.FilterUsersByEnvironmentID(allUsers, ctrl.getCognitoEnvironmentID())
// Apply environment filtering when COGNITO_ENVIRONMENT_ID is configured
filteredUsers := usermanagement.FilterUsersByEnvironmentID(result.Users, ctrl.getCognitoEnvironmentID())
// Convert to AdminUserDetails format
users := make([]AdminUserDetails, 0, len(filteredUsers))
for _, cognitoUser := range filteredUsers {
userDetail := AdminUserDetails{
CognitoSubjectId: cognitoUser.SubjectID,
Email: types.Email(cognitoUser.Email),
FirstName: &cognitoUser.FirstName,
LastName: &cognitoUser.LastName,
Status: cognitoUser.Status,
Enabled: cognitoUser.Enabled,
}
users = append(users, userDetail)
httpClient := &http.Client{Timeout: 10 * time.Second}
permitConfig := ctrl.getPermitConfig()
adminMode := ctrl.getAdminTeamMode(ctx, httpClient, permitConfig)
if adminMode == adminTeamModeNone {
return adminUserForbidden(ctx)
}
roleAssignments, err := usermanagement.ListAllPermitRoleAssignments(httpClient, permitConfig)
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to list role assignments from Permit.io", "error", err)
return ctx.JSON(http.StatusBadGateway, ErrorMessage{
Message: fmt.Sprintf("Failed to list role assignments from Permit.io: %v", err),
})
}
visibleUsers := ctrl.visibleAdminUsers(filteredUsers, adminMode, adminRoleAssignmentsByUser(roleAssignments))
users, hasMore := paginateAdminUsers(visibleUsers, page, pageSize)
// Log list action
usermanagement.LogUserListAction(
ctrl.cfg.GetAuthLogger(),
@@ -500,9 +502,7 @@ func (ctrl *Controllers) ListAdminUsers(ctx echo.Context, params ListAdminUsersP
)
// Build response
hasMore := result.PaginationKey != nil && *result.PaginationKey != ""
// len(users) is bounded by pageSize (max 60), so this conversion is safe
total := int32(len(users)) // #nosec G115 -- len(users) is bounded by pageSize which is max 60
total := int32(len(visibleUsers)) // #nosec G115 -- admin user lists are bounded by Cognito user pool size
response := AdminUserList{
Users: users,
Total: total,
@@ -514,6 +514,106 @@ func (ctrl *Controllers) ListAdminUsers(ctx echo.Context, params ListAdminUsersP
return ctx.JSON(http.StatusOK, response)
}
func adminUserListPagination(params ListAdminUsersParams) (int32, int32) {
page := int32(1)
pageSize := int32(50)
if params.Page != nil && *params.Page > 0 {
page = *params.Page
}
if params.PageSize != nil && *params.PageSize > 0 && *params.PageSize <= 60 {
pageSize = *params.PageSize
}
return page, pageSize
}
func listAllAdminCognitoUsers(cognitoClient *cognitoidentityprovider.Client, userPoolID string) ([]usermanagement.CognitoUserResponse, error) {
return collectAdminCognitoUsers(func(pageSize int, paginationToken *string) (*usermanagement.ListCognitoUsersResult, error) {
return usermanagement.ListCognitoUsers(context.Background(), cognitoClient, userPoolID, pageSize, paginationToken)
})
}
func collectAdminCognitoUsers(fetchPage func(pageSize int, paginationToken *string) (*usermanagement.ListCognitoUsersResult, error)) ([]usermanagement.CognitoUserResponse, error) {
var users []usermanagement.CognitoUserResponse
var paginationToken *string
for page := 1; page <= maxAdminCognitoListPages; page++ {
result, err := fetchPage(adminCognitoListPageSize, paginationToken)
if err != nil {
return nil, err
}
users = append(users, result.Users...)
if len(users) > maxAdminCognitoListUsers {
return nil, fmt.Errorf("admin Cognito user listing exceeded %d users", maxAdminCognitoListUsers)
}
if result.PaginationKey == nil || *result.PaginationKey == "" {
return users, nil
}
paginationToken = result.PaginationKey
}
return nil, fmt.Errorf("admin Cognito user listing exceeded %d pages", maxAdminCognitoListPages)
}
func (ctrl *Controllers) visibleAdminUsers(users []usermanagement.CognitoUserResponse, mode adminTeamMode, rolesBySubject map[string][]string) []AdminUserDetails {
visibleUsers := make([]AdminUserDetails, 0, len(users))
for _, cognitoUser := range users {
roles, ok := rolesForAdminListUser(cognitoUser, mode, rolesBySubject)
if !ok || !canAdminModeReadTarget(mode, cognitoUser.Email, roles) {
continue
}
visibleUsers = append(visibleUsers, adminUserDetailsFromCognito(cognitoUser, roles))
}
return visibleUsers
}
func rolesForAdminListUser(cognitoUser usermanagement.CognitoUserResponse, mode adminTeamMode, rolesBySubject map[string][]string) ([]string, bool) {
roles, ok := rolesBySubject[cognitoUser.SubjectID]
if !ok && mode == adminTeamModeUserAdmin {
return nil, false
}
if roles == nil {
roles = []string{}
}
return roles, true
}
func adminRoleAssignmentsByUser(assignments []usermanagement.RoleAssignment) map[string][]string {
rolesBySubject := make(map[string][]string, len(assignments))
for _, assignment := range assignments {
if assignment.User == "" || assignment.Role == "" {
continue
}
rolesBySubject[assignment.User] = append(rolesBySubject[assignment.User], assignment.Role)
}
return rolesBySubject
}
func adminUserDetailsFromCognito(cognitoUser usermanagement.CognitoUserResponse, roles []string) AdminUserDetails {
if roles == nil {
roles = []string{}
}
userDetail := AdminUserDetails{
CognitoSubjectId: cognitoUser.SubjectID,
Email: types.Email(cognitoUser.Email),
FirstName: &cognitoUser.FirstName,
LastName: &cognitoUser.LastName,
Status: cognitoUser.Status,
Enabled: cognitoUser.Enabled,
}
userDetail.Roles = &roles
return userDetail
}
func paginateAdminUsers(visibleUsers []AdminUserDetails, page int32, pageSize int32) ([]AdminUserDetails, bool) {
start := int((page - 1) * pageSize) // #nosec G115 -- page and pageSize are positive int32 request parameters
end := start + int(pageSize)
if start > len(visibleUsers) {
start = len(visibleUsers)
}
if end > len(visibleUsers) {
end = len(visibleUsers)
}
return visibleUsers[start:end], end < len(visibleUsers)
}
// updateUserAttributesInCognito updates user attributes in Cognito and logs the operation.
func (ctrl *Controllers) updateUserAttributesInCognito(ctx echo.Context, cognitoClient *cognitoidentityprovider.Client, cognitoConfig *usermanagement.CognitoConfig, email UserEmail, req AdminUserUpdate, adminUser cognitoauth.UserInfo, cognitoUser *usermanagement.CognitoUserResponse) error {
if req.FirstName == nil && req.LastName == nil {
@@ -576,7 +676,7 @@ func (ctrl *Controllers) updateUserAttributesInCognito(ctx echo.Context, cognito
// updateUserRolesInPermit updates user roles in Permit.io and logs each operation.
func (ctrl *Controllers) updateUserRolesInPermit(ctx echo.Context, email UserEmail, req AdminUserUpdate, adminUser cognitoauth.UserInfo, subjectID string, httpClient *http.Client, permitConfig *usermanagement.PermitConfig) {
if req.Roles == nil || len(*req.Roles) == 0 {
if req.Roles == nil {
return
}
@@ -733,6 +833,16 @@ func (ctrl *Controllers) UpdateAdminUser(ctx echo.Context, email UserEmail) erro
})
}
targetAccess := ctrl.getAdminTargetAccess(ctx, email, cognitoUser)
if !targetAccess.canMutate(cognitoUser.Email) {
return adminUserForbidden(ctx)
}
if !canAdminModeAssignRequestedRoles(targetAccess.mode, req.Roles) {
return adminUserForbidden(ctx)
}
httpClient := targetAccess.httpClient
permitConfig := targetAccess.permitConfig
// Update user attributes in Cognito if provided
err = ctrl.updateUserAttributesInCognito(ctx, cognitoClient, cognitoConfig, email, req, adminUser, cognitoUser)
if err != nil {
@@ -742,9 +852,6 @@ func (ctrl *Controllers) UpdateAdminUser(ctx echo.Context, email UserEmail) erro
}
// Handle role updates in Permit.io if provided
httpClient := &http.Client{Timeout: 10 * time.Second}
permitConfig := ctrl.getPermitConfig()
ctrl.updateUserRolesInPermit(ctx, email, req, adminUser, cognitoUser.SubjectID, httpClient, permitConfig)
// Get updated user from Cognito
@@ -847,6 +954,13 @@ func (ctrl *Controllers) DeleteAdminUser(ctx echo.Context, email UserEmail, para
})
}
targetAccess := ctrl.getAdminTargetAccess(ctx, email, cognitoUser)
if !targetAccess.canMutate(cognitoUser.Email) {
return adminUserForbidden(ctx)
}
httpClient := targetAccess.httpClient
permitConfig := targetAccess.permitConfig
// Delete user from Cognito
cognitoDeleted := false
err = usermanagement.DeleteCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
@@ -879,9 +993,6 @@ func (ctrl *Controllers) DeleteAdminUser(ctx echo.Context, email UserEmail, para
}
// Delete user from Permit.io
httpClient := &http.Client{Timeout: 10 * time.Second}
permitConfig := ctrl.getPermitConfig()
permitDeleted := false
err = usermanagement.DeletePermitUser(httpClient, permitConfig, cognitoUser.SubjectID)
if err != nil {
@@ -988,6 +1099,11 @@ func (ctrl *Controllers) DisableAdminUser(ctx echo.Context, email UserEmail) err
})
}
targetAccess := ctrl.getAdminTargetAccess(ctx, email, cognitoUser)
if !targetAccess.canMutate(cognitoUser.Email) {
return adminUserForbidden(ctx)
}
// Disable user in Cognito
err = usermanagement.DisableCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
@@ -1099,6 +1215,11 @@ func (ctrl *Controllers) EnableAdminUser(ctx echo.Context, email UserEmail) erro
})
}
targetAccess := ctrl.getAdminTargetAccess(ctx, email, cognitoUser)
if !targetAccess.canMutate(cognitoUser.Email) {
return adminUserForbidden(ctx)
}
// Enable user in Cognito
err = usermanagement.EnableCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {