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:
+168
-47
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user