Files
Jacob Mathison e3d9047143 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
2026-05-12 16:49:12 +00:00

1269 lines
41 KiB
Go

// adminHandlers.go implements the admin API handlers for user management operations.
// These handlers provide REST API access to AWS Cognito and Permit.io user management functions.
package queryapi
import (
"context"
"fmt"
"net/http"
"os"
"time"
"queryorchestration/internal/cognitoauth"
"queryorchestration/internal/serviceconfig/aws"
"queryorchestration/internal/usermanagement"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
"github.com/labstack/echo/v4"
"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.
//
// Parameters:
// - ctx: The Echo context containing the HTTP request
//
// Returns:
// - cognitoauth.UserInfo: The admin user information
// - error: Error if user info cannot be retrieved and auth is not disabled
func getAdminUserForAudit(ctx echo.Context) (cognitoauth.UserInfo, error) {
// Try to get user info from JWT token
adminUser, ok := cognitoauth.GetUserInfo(ctx)
if ok {
return adminUser, nil
}
// If auth is disabled, use a default system user for audit logging
if os.Getenv("DISABLE_AUTH") == "true" {
return cognitoauth.UserInfo{
Email: "system@test.local",
Username: "system",
Groups: []string{"admin"},
}, nil
}
// Auth is enabled but user info not found - return error
return cognitoauth.UserInfo{}, fmt.Errorf("could not identify admin user")
}
// getPermitConfig creates a PermitConfig by reading environment variables.
// This centralizes Permit.io configuration to avoid code duplication.
// Note: ProjectID and EnvID are automatically derived from the API key scope
// when first needed, so they do not need to be set here.
//
// Parameters:
// - ctrl: The Controllers instance containing the API key from config
//
// Returns:
// - *usermanagement.PermitConfig: The Permit.io configuration
func (ctrl *Controllers) getPermitConfig() *usermanagement.PermitConfig {
tenant := os.Getenv("PERMIT_IO_TENANT")
if tenant == "" {
tenant = "default"
}
baseURL := os.Getenv("PERMIT_IO_BASE_URL")
if baseURL == "" {
baseURL = "https://api.permit.io"
}
return &usermanagement.PermitConfig{
APIKey: ctrl.cfg.GetPermitIOAPIKey(),
BaseURL: baseURL,
DefaultTenant: tenant,
}
}
// CreateAdminUser creates a new user in both AWS Cognito and Permit.io.
// POST /admin/users
func (ctrl *Controllers) CreateAdminUser(ctx echo.Context) error {
// Parse request body
var req AdminUserCreate
if err := ctx.Bind(&req); err != nil {
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
Message: fmt.Sprintf("Invalid request body: %v", err),
})
}
// Get admin user email from JWT for audit logging
adminUser, err := getAdminUserForAudit(ctx)
if err != nil {
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
Message: "Could not identify admin user",
})
}
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 {
ctrl.cfg.GetAuthLogger().Error("Failed to load AWS config", "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
Message: "Failed to initialize AWS configuration",
})
}
cognitoClient := cognitoidentityprovider.NewFromConfig(awsCfg)
// Prepare Cognito configuration
cognitoConfig := &usermanagement.CognitoConfig{
UserPoolID: ctrl.cfg.GetAuthUserPoolID(),
Region: ctrl.cfg.GetAuthRegion(),
}
// Prepare user attributes
userAttrs := usermanagement.UserAttributes{
Email: string(req.Email),
FirstName: req.FirstName,
LastName: req.LastName,
EnvironmentID: ctrl.getCognitoEnvironmentID(),
}
// Check if user already exists
existingUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, userAttrs.Email)
if err == nil {
// User exists - check if attributes match (idempotent)
if existingUser.FirstName == userAttrs.FirstName && existingUser.LastName == userAttrs.LastName {
// Return 200 OK for idempotent creation
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionCreate,
usermanagement.SystemCognito,
adminUser.Email,
userAttrs.Email,
existingUser.SubjectID,
usermanagement.ResultSuccess,
"User already exists with matching attributes (idempotent)",
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
createdAt := time.Now()
return ctx.JSON(http.StatusOK, AdminUserCreateResponse{
CognitoSubjectId: existingUser.SubjectID,
Email: types.Email(existingUser.Email),
FirstName: &existingUser.FirstName,
LastName: &existingUser.LastName,
Status: existingUser.Status,
Enabled: &existingUser.Enabled,
PermitSynced: true,
CreatedAt: &createdAt,
})
}
// User exists but attributes differ - conflict
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionCreate,
usermanagement.SystemCognito,
adminUser.Email,
userAttrs.Email,
existingUser.SubjectID,
usermanagement.ResultFailure,
"User already exists with different attributes",
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
return ctx.JSON(http.StatusConflict, ErrorMessage{
Message: "User already exists with different attributes",
})
}
// Create user in Cognito
cognitoUser, err := usermanagement.CreateCognitoUser(context.Background(), cognitoClient, cognitoConfig, userAttrs)
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to create user in Cognito", "error", err, "email", userAttrs.Email)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionCreate,
usermanagement.SystemCognito,
adminUser.Email,
userAttrs.Email,
"",
usermanagement.ResultFailure,
fmt.Sprintf("Cognito creation failed: %v", err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
return ctx.JSON(http.StatusBadGateway, ErrorMessage{
Message: fmt.Sprintf("Failed to create user in Cognito: %v", err),
})
}
// Log successful Cognito creation
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionCreate,
usermanagement.SystemCognito,
adminUser.Email,
userAttrs.Email,
cognitoUser.SubjectID,
usermanagement.ResultSuccess,
"User created successfully in Cognito",
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
// Create user in Permit.io
permitSynced := false
err = usermanagement.CreatePermitUser(httpClient, permitConfig, cognitoUser)
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to create user in Permit.io", "error", err, "email", userAttrs.Email)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionCreate,
usermanagement.SystemPermit,
adminUser.Email,
userAttrs.Email,
cognitoUser.SubjectID,
usermanagement.ResultFailure,
fmt.Sprintf("Permit.io creation failed: %v", err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
} else {
permitSynced = true
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionCreate,
usermanagement.SystemPermit,
adminUser.Email,
userAttrs.Email,
cognitoUser.SubjectID,
usermanagement.ResultSuccess,
"User created successfully in Permit.io",
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
}
// Assign roles in Permit.io
var rolesAssigned []string
if permitSynced {
for _, role := range req.Roles {
err = usermanagement.AssignRole(httpClient, permitConfig, cognitoUser.SubjectID, role)
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to assign role", "error", err, "role", role, "user", userAttrs.Email)
usermanagement.LogRoleAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionAssignRole,
adminUser.Email,
userAttrs.Email,
role,
usermanagement.ResultFailure,
fmt.Sprintf("Role assignment failed: %v", err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
} else {
rolesAssigned = append(rolesAssigned, role)
usermanagement.LogRoleAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionAssignRole,
adminUser.Email,
userAttrs.Email,
role,
usermanagement.ResultSuccess,
"Role assigned successfully",
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
}
}
}
// Build response
createdAt := time.Now()
response := AdminUserCreateResponse{
CognitoSubjectId: cognitoUser.SubjectID,
Email: types.Email(cognitoUser.Email),
FirstName: &cognitoUser.FirstName,
LastName: &cognitoUser.LastName,
Status: cognitoUser.Status,
Enabled: &cognitoUser.Enabled,
PermitSynced: permitSynced,
CreatedAt: &createdAt,
}
if len(rolesAssigned) > 0 {
response.RolesAssigned = &rolesAssigned
}
return ctx.JSON(http.StatusCreated, response)
}
// GetAdminUser retrieves user details from both AWS Cognito and Permit.io.
// GET /admin/users/{email}
func (ctrl *Controllers) GetAdminUser(ctx echo.Context, email UserEmail) error {
// Get admin user email from JWT for audit logging
adminUser, err := getAdminUserForAudit(ctx)
if err != nil {
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
Message: "Could not identify admin user",
})
}
// Create AWS config and Cognito client
awsCfg, err := aws.GetAWSConfig(context.Background())
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to load AWS config", "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
Message: "Failed to initialize AWS configuration",
})
}
cognitoClient := cognitoidentityprovider.NewFromConfig(awsCfg)
// Prepare Cognito configuration
cognitoConfig := &usermanagement.CognitoConfig{
UserPoolID: ctrl.cfg.GetAuthUserPoolID(),
Region: ctrl.cfg.GetAuthRegion(),
}
// Get user from Cognito
cognitoUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
errInfo := usermanagement.ClassifyCognitoError(err, string(email))
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito",
"error", err,
"email", email,
"error_type", errInfo.ErrorType,
"http_status", errInfo.HTTPStatus)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionUpdate,
usermanagement.SystemCognito,
adminUser.Email,
string(email),
"",
usermanagement.ResultFailure,
fmt.Sprintf("Cognito error (%s): %v", errInfo.ErrorType, err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
errorType := string(errInfo.ErrorType)
return ctx.JSON(errInfo.HTTPStatus, ErrorMessage{
Message: errInfo.Message,
ErrorType: &errorType,
Details: &errInfo.Details,
})
}
// Check environment ownership - return 404 for users in other environments
if err := checkUserEnvironmentOwnership(cognitoUser, ctrl.getCognitoEnvironmentID()); err != nil {
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", 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(
ctrl.cfg.GetAuthLogger(),
adminUser.Email,
fmt.Sprintf("email=%s", email),
1,
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
// Build response
response := AdminUserDetails{
CognitoSubjectId: cognitoUser.SubjectID,
Email: types.Email(cognitoUser.Email),
FirstName: &cognitoUser.FirstName,
LastName: &cognitoUser.LastName,
Status: cognitoUser.Status,
Enabled: cognitoUser.Enabled,
}
if len(roles) > 0 {
response.Roles = &roles
}
return ctx.JSON(http.StatusOK, response)
}
// CheckAdminUserExists checks if a user exists in Cognito and/or Permit.io.
// HEAD /admin/users/{email}
func (ctrl *Controllers) CheckAdminUserExists(ctx echo.Context, email UserEmail) error {
// Create AWS config and Cognito client
awsCfg, err := aws.GetAWSConfig(context.Background())
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to load AWS config", "error", err)
return ctx.NoContent(http.StatusInternalServerError)
}
cognitoClient := cognitoidentityprovider.NewFromConfig(awsCfg)
// Prepare Cognito configuration
cognitoConfig := &usermanagement.CognitoConfig{
UserPoolID: ctrl.cfg.GetAuthUserPoolID(),
Region: ctrl.cfg.GetAuthRegion(),
}
// Check if user exists in Cognito (use GetCognitoUser to get detailed error info)
cognitoUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
errInfo := usermanagement.ClassifyCognitoError(err, string(email))
// For HEAD requests, we can only return status code and headers
ctx.Response().Header().Set("X-Error-Type", string(errInfo.ErrorType))
ctx.Response().Header().Set("X-Error-Message", errInfo.Message)
return ctx.NoContent(errInfo.HTTPStatus)
}
// Check environment ownership - return 404 for users in other environments
if err := checkUserEnvironmentOwnership(cognitoUser, ctrl.getCognitoEnvironmentID()); err != nil {
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)
}
// ListAdminUsers lists users from AWS Cognito with pagination, filtering, and sorting.
// GET /admin/users
func (ctrl *Controllers) ListAdminUsers(ctx echo.Context, params ListAdminUsersParams) error {
// Get admin user email from JWT for audit logging
adminUser, err := getAdminUserForAudit(ctx)
if err != nil {
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
Message: "Could not identify admin user",
})
}
// Create AWS config and Cognito client
awsCfg, err := aws.GetAWSConfig(context.Background())
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to load AWS config", "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
Message: "Failed to initialize AWS configuration",
})
}
cognitoClient := cognitoidentityprovider.NewFromConfig(awsCfg)
// Prepare Cognito configuration
cognitoConfig := &usermanagement.CognitoConfig{
UserPoolID: ctrl.cfg.GetAuthUserPoolID(),
Region: ctrl.cfg.GetAuthRegion(),
}
page, pageSize := adminUserListPagination(params)
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())
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(),
adminUser.Email,
fmt.Sprintf("page=%d,page_size=%d", page, pageSize),
len(users),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
// Build response
total := int32(len(visibleUsers)) // #nosec G115 -- admin user lists are bounded by Cognito user pool size
response := AdminUserList{
Users: users,
Total: total,
Page: page,
PageSize: pageSize,
HasMore: &hasMore,
}
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 {
return nil
}
firstName := ""
if req.FirstName != nil {
firstName = *req.FirstName
}
lastName := ""
if req.LastName != nil {
lastName = *req.LastName
}
userAttrs := usermanagement.UserAttributes{
Email: string(email),
FirstName: firstName,
LastName: lastName,
}
err := usermanagement.UpdateCognitoUserAttributes(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email), userAttrs)
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to update user in Cognito", "error", err, "email", email)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionUpdate,
usermanagement.SystemCognito,
adminUser.Email,
string(email),
cognitoUser.SubjectID,
usermanagement.ResultFailure,
fmt.Sprintf("Cognito update failed: %v", err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
return err
}
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionUpdate,
usermanagement.SystemCognito,
adminUser.Email,
string(email),
cognitoUser.SubjectID,
usermanagement.ResultSuccess,
"User attributes updated successfully in Cognito",
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
if req.FirstName != nil {
cognitoUser.FirstName = *req.FirstName
}
if req.LastName != nil {
cognitoUser.LastName = *req.LastName
}
return nil
}
// 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 {
return
}
currentRoles, err := usermanagement.GetUserRoles(httpClient, permitConfig, subjectID)
if err != nil {
ctrl.cfg.GetAuthLogger().Warn("Failed to get current user roles from Permit.io", "error", err, "email", email)
currentRoles = []string{}
}
requestedRoles := *req.Roles
currentRolesMap := make(map[string]bool)
for _, role := range currentRoles {
currentRolesMap[role] = true
}
requestedRolesMap := make(map[string]bool)
for _, role := range requestedRoles {
requestedRolesMap[role] = true
}
// Add new roles
for _, role := range requestedRoles {
if !currentRolesMap[role] {
err = usermanagement.AssignRole(httpClient, permitConfig, subjectID, role)
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to assign role", "error", err, "role", role, "user", email)
usermanagement.LogRoleAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionAssignRole,
adminUser.Email,
string(email),
role,
usermanagement.ResultFailure,
fmt.Sprintf("Role assignment failed: %v", err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
} else {
usermanagement.LogRoleAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionAssignRole,
adminUser.Email,
string(email),
role,
usermanagement.ResultSuccess,
"Role assigned successfully",
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
}
}
}
// Remove old roles
for _, role := range currentRoles {
if !requestedRolesMap[role] {
err = usermanagement.UnassignRole(httpClient, permitConfig, subjectID, role)
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to unassign role", "error", err, "role", role, "user", email)
usermanagement.LogRoleAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionUnassignRole,
adminUser.Email,
string(email),
role,
usermanagement.ResultFailure,
fmt.Sprintf("Role unassignment failed: %v", err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
} else {
usermanagement.LogRoleAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionUnassignRole,
adminUser.Email,
string(email),
role,
usermanagement.ResultSuccess,
"Role unassigned successfully",
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
}
}
}
}
// UpdateAdminUser updates user attributes in Cognito (first_name, last_name) and manages roles in Permit.io.
// PATCH /admin/users/{email}
func (ctrl *Controllers) UpdateAdminUser(ctx echo.Context, email UserEmail) error {
// Parse request body
var req AdminUserUpdate
if err := ctx.Bind(&req); err != nil {
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
Message: fmt.Sprintf("Invalid request body: %v", err),
})
}
// Get admin user email from JWT for audit logging
adminUser, err := getAdminUserForAudit(ctx)
if err != nil {
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
Message: "Could not identify admin user",
})
}
// Create AWS config and Cognito client
awsCfg, err := aws.GetAWSConfig(context.Background())
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to load AWS config", "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
Message: "Failed to initialize AWS configuration",
})
}
cognitoClient := cognitoidentityprovider.NewFromConfig(awsCfg)
// Prepare Cognito configuration
cognitoConfig := &usermanagement.CognitoConfig{
UserPoolID: ctrl.cfg.GetAuthUserPoolID(),
Region: ctrl.cfg.GetAuthRegion(),
}
// Get user from Cognito to verify existence and get Subject ID
cognitoUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
errInfo := usermanagement.ClassifyCognitoError(err, string(email))
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito",
"error", err,
"email", email,
"error_type", errInfo.ErrorType,
"http_status", errInfo.HTTPStatus)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionUpdate,
usermanagement.SystemCognito,
adminUser.Email,
string(email),
"",
usermanagement.ResultFailure,
fmt.Sprintf("Cognito error (%s): %v", errInfo.ErrorType, err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
errorType := string(errInfo.ErrorType)
return ctx.JSON(errInfo.HTTPStatus, ErrorMessage{
Message: errInfo.Message,
ErrorType: &errorType,
Details: &errInfo.Details,
})
}
// Check environment ownership - return 404 for users in other environments
if err := checkUserEnvironmentOwnership(cognitoUser, ctrl.getCognitoEnvironmentID()); err != nil {
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
})
}
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 {
return ctx.JSON(http.StatusBadGateway, ErrorMessage{
Message: fmt.Sprintf("Failed to update user in Cognito: %v", err),
})
}
// Handle role updates in Permit.io if provided
ctrl.updateUserRolesInPermit(ctx, email, req, adminUser, cognitoUser.SubjectID, httpClient, permitConfig)
// Get updated user from Cognito
updatedUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
ctrl.cfg.GetAuthLogger().Warn("Failed to get updated user from Cognito", "error", err, "email", email)
updatedUser = cognitoUser
}
// Get updated roles
updatedRoles, err := usermanagement.GetUserRoles(httpClient, permitConfig, cognitoUser.SubjectID)
if err != nil {
ctrl.cfg.GetAuthLogger().Warn("Failed to get updated user roles from Permit.io", "error", err, "email", email)
}
// Build response
response := AdminUserDetails{
CognitoSubjectId: updatedUser.SubjectID,
Email: types.Email(updatedUser.Email),
FirstName: &updatedUser.FirstName,
LastName: &updatedUser.LastName,
Status: updatedUser.Status,
Enabled: updatedUser.Enabled,
}
if len(updatedRoles) > 0 {
response.Roles = &updatedRoles
}
return ctx.JSON(http.StatusOK, response)
}
// DeleteAdminUser permanently deletes a user from both AWS Cognito and Permit.io.
// DELETE /admin/users/{email}
func (ctrl *Controllers) DeleteAdminUser(ctx echo.Context, email UserEmail, params DeleteAdminUserParams) error {
if !params.Confirm {
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
Message: "confirm=true query parameter is required to delete a user",
})
}
// Get admin user email from JWT for audit logging
adminUser, err := getAdminUserForAudit(ctx)
if err != nil {
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
Message: "Could not identify admin user",
})
}
// Create AWS config and Cognito client
awsCfg, err := aws.GetAWSConfig(context.Background())
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to load AWS config", "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
Message: "Failed to initialize AWS configuration",
})
}
cognitoClient := cognitoidentityprovider.NewFromConfig(awsCfg)
// Prepare Cognito configuration
cognitoConfig := &usermanagement.CognitoConfig{
UserPoolID: ctrl.cfg.GetAuthUserPoolID(),
Region: ctrl.cfg.GetAuthRegion(),
}
// Get user from Cognito to verify existence and get Subject ID
cognitoUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
errInfo := usermanagement.ClassifyCognitoError(err, string(email))
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito",
"error", err,
"email", email,
"error_type", errInfo.ErrorType,
"http_status", errInfo.HTTPStatus)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionDelete,
usermanagement.SystemCognito,
adminUser.Email,
string(email),
"",
usermanagement.ResultFailure,
fmt.Sprintf("Cognito error (%s): %v", errInfo.ErrorType, err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
errorType := string(errInfo.ErrorType)
return ctx.JSON(errInfo.HTTPStatus, ErrorMessage{
Message: errInfo.Message,
ErrorType: &errorType,
Details: &errInfo.Details,
})
}
// Check environment ownership - return 404 for users in other environments
if err := checkUserEnvironmentOwnership(cognitoUser, ctrl.getCognitoEnvironmentID()); err != nil {
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
})
}
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))
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to delete user from Cognito", "error", err, "email", email)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionDelete,
usermanagement.SystemCognito,
adminUser.Email,
string(email),
cognitoUser.SubjectID,
usermanagement.ResultFailure,
fmt.Sprintf("Cognito deletion failed: %v", err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
} else {
cognitoDeleted = true
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionDelete,
usermanagement.SystemCognito,
adminUser.Email,
string(email),
cognitoUser.SubjectID,
usermanagement.ResultSuccess,
"User deleted successfully from Cognito",
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
}
// Delete user from Permit.io
permitDeleted := false
err = usermanagement.DeletePermitUser(httpClient, permitConfig, cognitoUser.SubjectID)
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to delete user from Permit.io", "error", err, "email", email)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionDelete,
usermanagement.SystemPermit,
adminUser.Email,
string(email),
cognitoUser.SubjectID,
usermanagement.ResultFailure,
fmt.Sprintf("Permit.io deletion failed: %v", err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
} else {
permitDeleted = true
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionDelete,
usermanagement.SystemPermit,
adminUser.Email,
string(email),
cognitoUser.SubjectID,
usermanagement.ResultSuccess,
"User deleted successfully from Permit.io",
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
}
// Build response
response := AdminUserDeleteResponse{
Success: cognitoDeleted, // Success based on Cognito deletion (primary auth system)
CognitoSubjectId: &cognitoUser.SubjectID,
Email: types.Email(string(email)),
}
response.DeletedFrom.Cognito = cognitoDeleted
response.DeletedFrom.Permit = permitDeleted
return ctx.JSON(http.StatusOK, response)
}
// DisableAdminUser disables a user in AWS Cognito, preventing authentication.
// POST /admin/users/{email}/disable
func (ctrl *Controllers) DisableAdminUser(ctx echo.Context, email UserEmail) error {
// Get admin user email from JWT for audit logging
adminUser, err := getAdminUserForAudit(ctx)
if err != nil {
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
Message: "Could not identify admin user",
})
}
// Create AWS config and Cognito client
awsCfg, err := aws.GetAWSConfig(context.Background())
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to load AWS config", "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
Message: "Failed to initialize AWS configuration",
})
}
cognitoClient := cognitoidentityprovider.NewFromConfig(awsCfg)
// Prepare Cognito configuration
cognitoConfig := &usermanagement.CognitoConfig{
UserPoolID: ctrl.cfg.GetAuthUserPoolID(),
Region: ctrl.cfg.GetAuthRegion(),
}
// Get user from Cognito to verify existence and get Subject ID
cognitoUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
errInfo := usermanagement.ClassifyCognitoError(err, string(email))
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito",
"error", err,
"email", email,
"error_type", errInfo.ErrorType,
"http_status", errInfo.HTTPStatus)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionDisable,
usermanagement.SystemCognito,
adminUser.Email,
string(email),
"",
usermanagement.ResultFailure,
fmt.Sprintf("Cognito error (%s): %v", errInfo.ErrorType, err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
errorType := string(errInfo.ErrorType)
return ctx.JSON(errInfo.HTTPStatus, ErrorMessage{
Message: errInfo.Message,
ErrorType: &errorType,
Details: &errInfo.Details,
})
}
// Check environment ownership - return 404 for users in other environments
if err := checkUserEnvironmentOwnership(cognitoUser, ctrl.getCognitoEnvironmentID()); err != nil {
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
})
}
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 {
ctrl.cfg.GetAuthLogger().Error("Failed to disable user in Cognito", "error", err, "email", email)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionDisable,
usermanagement.SystemCognito,
adminUser.Email,
string(email),
cognitoUser.SubjectID,
usermanagement.ResultFailure,
fmt.Sprintf("Cognito disable failed: %v", err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
return ctx.JSON(http.StatusBadGateway, ErrorMessage{
Message: fmt.Sprintf("Failed to disable user in Cognito: %v", err),
})
}
// Log successful disable
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionDisable,
usermanagement.SystemCognito,
adminUser.Email,
string(email),
cognitoUser.SubjectID,
usermanagement.ResultSuccess,
"User disabled successfully in Cognito",
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
// Build response
timestamp := time.Now()
response := AdminUserActionResponse{
Action: Disable,
CognitoSubjectId: &cognitoUser.SubjectID,
Email: types.Email(string(email)),
Success: true,
Timestamp: &timestamp,
}
return ctx.JSON(http.StatusOK, response)
}
// EnableAdminUser re-enables a previously disabled user in AWS Cognito.
// POST /admin/users/{email}/enable
func (ctrl *Controllers) EnableAdminUser(ctx echo.Context, email UserEmail) error {
// Get admin user email from JWT for audit logging
adminUser, err := getAdminUserForAudit(ctx)
if err != nil {
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
Message: "Could not identify admin user",
})
}
// Create AWS config and Cognito client
awsCfg, err := aws.GetAWSConfig(context.Background())
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to load AWS config", "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
Message: "Failed to initialize AWS configuration",
})
}
cognitoClient := cognitoidentityprovider.NewFromConfig(awsCfg)
// Prepare Cognito configuration
cognitoConfig := &usermanagement.CognitoConfig{
UserPoolID: ctrl.cfg.GetAuthUserPoolID(),
Region: ctrl.cfg.GetAuthRegion(),
}
// Get user from Cognito to verify existence and get Subject ID
cognitoUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
errInfo := usermanagement.ClassifyCognitoError(err, string(email))
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito",
"error", err,
"email", email,
"error_type", errInfo.ErrorType,
"http_status", errInfo.HTTPStatus)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionEnable,
usermanagement.SystemCognito,
adminUser.Email,
string(email),
"",
usermanagement.ResultFailure,
fmt.Sprintf("Cognito error (%s): %v", errInfo.ErrorType, err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
errorType := string(errInfo.ErrorType)
return ctx.JSON(errInfo.HTTPStatus, ErrorMessage{
Message: errInfo.Message,
ErrorType: &errorType,
Details: &errInfo.Details,
})
}
// Check environment ownership - return 404 for users in other environments
if err := checkUserEnvironmentOwnership(cognitoUser, ctrl.getCognitoEnvironmentID()); err != nil {
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
})
}
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 {
ctrl.cfg.GetAuthLogger().Error("Failed to enable user in Cognito", "error", err, "email", email)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionEnable,
usermanagement.SystemCognito,
adminUser.Email,
string(email),
cognitoUser.SubjectID,
usermanagement.ResultFailure,
fmt.Sprintf("Cognito enable failed: %v", err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
return ctx.JSON(http.StatusBadGateway, ErrorMessage{
Message: fmt.Sprintf("Failed to enable user in Cognito: %v", err),
})
}
// Log successful enable
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionEnable,
usermanagement.SystemCognito,
adminUser.Email,
string(email),
cognitoUser.SubjectID,
usermanagement.ResultSuccess,
"User enabled successfully in Cognito",
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
// Build response
timestamp := time.Now()
response := AdminUserActionResponse{
Action: Enable,
CognitoSubjectId: &cognitoUser.SubjectID,
Email: types.Email(string(email)),
Success: true,
Timestamp: &timestamp,
}
return ctx.JSON(http.StatusOK, response)
}