Files
query-orchestration/cmd/auth_related/user.creation.tool/cognito-permit-sync.go
T
Jay Brown 6dccf494f8 Merged in feature/permit-policy-cleanup (pull request #210)
cleanup permit policies and correct documentation

* docs

* policy cleanup

* refactor

* Merge remote-tracking branch 'origin/feature/permit-policy-cleanup' into feature/permit-policy-cleanup

* docs and edits
2026-02-19 20:22:59 +00:00

1283 lines
44 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// cognito-permit-sync.go is the main entry point and orchestration logic for the user creation tool.
// This file coordinates the synchronization of users between AWS Cognito and Permit.io systems,
// handling CSV parsing, user creation/deletion/disable/enable operations, role validation,
// configuration management, and command-line interface. It serves as the primary workflow controller.
package main
import (
"context"
"encoding/csv"
"errors"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
"github.com/joho/godotenv"
)
type ErrorResponse struct {
Error struct {
Message string `json:"message"`
Details interface{} `json:"details,omitempty"`
} `json:"error"`
}
// Import tracking types
type ImportResult struct {
TotalUsers int
SuccessfulUsers int
FailedUsers []FailedUser
StartTime time.Time
EndTime time.Time
}
// Delete tracking types
type DeleteResult struct {
TotalUsers int
SuccessfulUsers int
FailedUsers []FailedUser
StartTime time.Time
EndTime time.Time
}
// Disable/Enable tracking types
type DisableEnableResult struct {
TotalUsers int
SuccessfulUsers int
FailedUsers []FailedUser
StartTime time.Time
EndTime time.Time
OperationType string // "disable" or "enable"
}
type FailedUser struct {
Email string
FirstName string
LastName string
Error string
FailureStep string // "cognito" or "permit"
}
// Combined config
type AppConfig struct {
// Permit.io config
PermitAPIKey string
PermitProjectID string
PermitEnvID string
PermitBaseURL string
// Cognito config
CognitoUserPoolID string
CognitoRegion string
// Other
CSVPath string
DeleteUsers bool
DisableUsers bool
EnableUsers bool
DryRun bool
}
// UserProcessingContext holds the context for processing a single user
type UserProcessingContext struct {
Config AppConfig
CognitoClient *cognitoidentityprovider.Client
CognitoConfig *CognitoConfig
HTTPClient *http.Client
AuditLogger *AuditLogger
User map[string]string
Index int
Total int
}
func main() {
// Load .env file if it exists
if err := godotenv.Load(); err != nil {
// Don't exit on error - .env file might not exist and that's OK
// Environment variables can still be set other ways
fmt.Println("Error loading .env file - ignoring.")
}
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
// resolveAPIKeyScope resolves the project and environment IDs from the API key scope
// when the user specifies -project use-key. It updates the config with the resolved values.
//
// Parameters:
// - config: Pointer to AppConfig that will be updated with resolved project/environment IDs
//
// Returns:
// - error: Error if scope resolution fails or required values cannot be determined
func resolveAPIKeyScope(config *AppConfig) error {
if config.PermitAPIKey == "" {
return fmt.Errorf("PERMIT_KEY environment variable is required when using -project use-key")
}
fmt.Println("Detecting project and environment from API key scope...")
httpClient := &http.Client{Timeout: 30 * time.Second}
scope, err := getAPIKeyScope(httpClient, *config)
if err != nil {
return fmt.Errorf("failed to get API key scope: %w", err)
}
if scope.ProjectID == "" {
return fmt.Errorf("API key does not have a project scope - use an org-level key with explicit -project flag, or use a project/environment-scoped key")
}
config.PermitProjectID = scope.ProjectID
fmt.Printf(" Detected project: %s\n", scope.ProjectID)
if scope.EnvironmentID != "" {
config.PermitEnvID = scope.EnvironmentID
fmt.Printf(" Detected environment: %s\n", scope.EnvironmentID)
} else if config.PermitEnvID == "" {
return fmt.Errorf("API key is project-scoped but -env flag not provided; specify environment or use an environment-scoped key")
} else {
fmt.Printf(" Using provided environment: %s\n", config.PermitEnvID)
}
fmt.Println()
return nil
}
func run() error {
config := parseFlags()
// Initialize audit logger
auditLogger, err := NewAuditLogger(config.DryRun)
if err != nil {
return fmt.Errorf("failed to initialize audit logger: %w", err)
}
defer auditLogger.Close()
// Log dry run start if applicable
if config.DryRun {
auditLogger.LogDryRunStart()
fmt.Println("🔍 DRY RUN MODE - No actual changes will be made")
fmt.Println(strings.Repeat("=", 60))
}
// Check if we should list projects/environments (Permit-only operation)
if config.PermitProjectID == "list" {
if config.PermitAPIKey == "" {
return fmt.Errorf("PERMIT_KEY environment variable is not set")
}
listProjectsAndEnvironments(config)
return nil
}
// Resolve API key scope if using project-level or environment-level key
if config.PermitProjectID == "use-key" {
if err := resolveAPIKeyScope(&config); err != nil {
return err
}
}
// Run sanity check for credentials (AWS + Permit)
sanityCheckCredentials(config)
// Validate configuration
if err := validateConfig(config); err != nil {
return fmt.Errorf("configuration error: %w", err)
}
// Initialize AWS Cognito client
ctx := context.Background()
cognitoClient, err := initializeCognitoClient()
if err != nil {
return fmt.Errorf("error initializing Cognito client: %w", err)
}
// Read and parse CSV file
users, err := readCSV(config.CSVPath, config)
if err != nil {
return fmt.Errorf("error reading CSV file: %w", err)
}
if len(users) == 0 {
fmt.Println("No users found in CSV file")
return nil
}
// Add this code AFTER this existing line:
// if len(users) == 0 {
// fmt.Println("No users found in CSV file")
// return nil
// }
// AND BEFORE this existing line:
// if config.DeleteUsers {
// Validate that all roles specified in CSV exist in Permit.io
fmt.Println("Validating roles specified in CSV...")
httpClient := &http.Client{Timeout: 30 * time.Second}
requestedRoles, err := ValidateCSVRoles(httpClient, config, users)
if err != nil {
fmt.Printf("\n❌ Role Validation Failed!\n")
fmt.Println(strings.Repeat("=", 60))
fmt.Printf("Error: %v\n\n", err)
fmt.Printf("The following roles are missing in Permit.io:\n")
for _, role := range requestedRoles {
fmt.Printf(" - %s\n", role)
}
fmt.Printf("\nPlease create these roles in Permit.io before running this tool.\n")
fmt.Printf("You can create roles in the Permit.io dashboard under:\n")
fmt.Printf("Project: %s → Environment: %s → Roles\n", config.PermitProjectID, config.PermitEnvID)
fmt.Println(strings.Repeat("=", 60))
return fmt.Errorf("role validation failed")
}
if len(requestedRoles) > 0 {
fmt.Printf("✅ All %d roles validated successfully: %s\n",
len(requestedRoles), strings.Join(requestedRoles, ", "))
} else {
fmt.Println("️ No roles specified in CSV - skipping role validation")
}
fmt.Println()
// Execute the requested operation
executeOperation(ctx, config, cognitoClient, users, auditLogger)
// Log dry run end if applicable
if config.DryRun {
auditLogger.LogDryRunEnd()
fmt.Println("\n🔍 DRY RUN COMPLETED - Check dry-run-audit.log for details")
} else {
fmt.Println("\n📋 Check audit.log for detailed audit trail")
}
return nil
}
// executeOperation runs the appropriate user operation based on the config flags.
// It handles delete, disable, enable, and create/update modes.
//
// Parameters:
// - ctx: Context for the operation
// - config: Application configuration specifying the operation mode
// - cognitoClient: AWS Cognito client for user operations
// - users: Slice of user data from the CSV file
// - auditLogger: Logger for audit trail
func executeOperation(ctx context.Context, config AppConfig, cognitoClient *cognitoidentityprovider.Client, users []map[string]string, auditLogger *AuditLogger) {
if config.DeleteUsers {
executeDeleteOperation(ctx, config, cognitoClient, users, auditLogger)
} else if config.DisableUsers {
executeDisableEnableOperation(ctx, config, cognitoClient, users, auditLogger, "disable")
} else if config.EnableUsers {
executeDisableEnableOperation(ctx, config, cognitoClient, users, auditLogger, "enable")
} else {
executeCreateOperation(ctx, config, cognitoClient, users, auditLogger)
}
}
// executeDeleteOperation handles the delete users workflow
func executeDeleteOperation(ctx context.Context, config AppConfig, cognitoClient *cognitoidentityprovider.Client, users []map[string]string, auditLogger *AuditLogger) {
fmt.Printf("Found %d users to DELETE\n", len(users))
fmt.Printf("Will delete users from Permit.io project: %s\n", config.PermitProjectID)
fmt.Printf("Then delete from Cognito User Pool: %s\n", config.CognitoUserPoolID)
fmt.Println(strings.Repeat("=", 60))
if !config.DryRun {
fmt.Println("⚠️ WARNING: This will permanently delete users!")
}
fmt.Println(strings.Repeat("=", 60))
result := deleteUsers(ctx, config, cognitoClient, users, auditLogger)
printDeleteSummary(result)
}
// executeDisableEnableOperation handles the disable/enable users workflow
func executeDisableEnableOperation(ctx context.Context, config AppConfig, cognitoClient *cognitoidentityprovider.Client, users []map[string]string, auditLogger *AuditLogger, operation string) {
opUpper := strings.ToUpper(operation)
fmt.Printf("Found %d users to %s\n", len(users), opUpper)
fmt.Printf("Will %s users in Cognito User Pool: %s\n", operation, config.CognitoUserPoolID)
fmt.Println(strings.Repeat("=", 60))
result := disableEnableUsers(ctx, config, cognitoClient, users, auditLogger, operation)
printDisableEnableSummary(result)
}
// executeCreateOperation handles the create/update users workflow
func executeCreateOperation(ctx context.Context, config AppConfig, cognitoClient *cognitoidentityprovider.Client, users []map[string]string, auditLogger *AuditLogger) {
fmt.Printf("Found %d users to import\n", len(users))
fmt.Printf("Will create users in Cognito User Pool: %s\n", config.CognitoUserPoolID)
fmt.Printf("Then sync to Permit.io project: %s\n", config.PermitProjectID)
fmt.Println(strings.Repeat("=", 60))
result := processUsers(ctx, config, cognitoClient, users, auditLogger)
printSummary(result)
}
func parseFlags() AppConfig {
var config AppConfig
// Permit.io flags
flag.StringVar(&config.PermitProjectID, "project", "", "Permit.io project ID/key, 'list' to show available, or 'use-key' to auto-detect from API key scope")
flag.StringVar(&config.PermitEnvID, "env", "", "Permit.io environment ID or key (required unless using 'use-key' with env-scoped API key)")
flag.StringVar(&config.PermitBaseURL, "api-url", "https://api.permit.io", "Permit.io API base URL")
// File flag
flag.StringVar(&config.CSVPath, "csv", "", "Path to CSV file containing users (required)")
// Delete flag
flag.BoolVar(&config.DeleteUsers, "delete-users", false, "Delete users instead of creating them")
// Disable/Enable flags
flag.BoolVar(&config.DisableUsers, "disable-users", false, "Disable users in Cognito instead of creating them")
flag.BoolVar(&config.EnableUsers, "enable-users", false, "Enable users in Cognito instead of creating them")
// Dry run flag
flag.BoolVar(&config.DryRun, "dry-run", false, "Perform a dry run without making actual changes")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "This tool creates users in AWS Cognito and syncs them to Permit.io\n\n")
fmt.Fprintf(os.Stderr, "Options:\n")
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nEnvironment Variables:\n")
fmt.Fprintf(os.Stderr, " PERMIT_KEY Permit.io API key (required)\n")
fmt.Fprintf(os.Stderr, " COGNITO_USER_POOL_ID AWS Cognito User Pool ID (required)\n")
fmt.Fprintf(os.Stderr, " COGNITO_CLIENT_ID AWS Cognito Client ID (required)\n")
fmt.Fprintf(os.Stderr, " COGNITO_CLIENT_SECRET AWS Cognito Client Secret (optional)\n")
fmt.Fprintf(os.Stderr, " AWS_REGION AWS Region (defaults to us-east-1)\n")
fmt.Fprintf(os.Stderr, "\nCSV Format:\n")
fmt.Fprintf(os.Stderr, " email,first_name,last_name,role1,role2,...,roleN\n")
fmt.Fprintf(os.Stderr, " - No user_id needed (generated by Cognito)\n")
fmt.Fprintf(os.Stderr, " - Up to 99 roles can be specified per user\n")
fmt.Fprintf(os.Stderr, " - Empty role columns are ignored\n")
fmt.Fprintf(os.Stderr, "\nExample:\n")
fmt.Fprintf(os.Stderr, " %s -project myproject -env production -csv users.csv\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\nTo delete users:\n")
fmt.Fprintf(os.Stderr, " %s -project myproject -env production -csv users.csv --delete-users\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\nTo disable users:\n")
fmt.Fprintf(os.Stderr, " %s -project myproject -env production -csv users.csv --disable-users\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\nTo enable users:\n")
fmt.Fprintf(os.Stderr, " %s -project myproject -env production -csv users.csv --enable-users\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\nTo perform a dry run:\n")
fmt.Fprintf(os.Stderr, " %s -project myproject -env production -csv users.csv --dry-run\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\nTo list available projects and environments:\n")
fmt.Fprintf(os.Stderr, " %s -project list\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\nTo use a project/environment-scoped API key (auto-detect project/env):\n")
fmt.Fprintf(os.Stderr, " %s -project use-key -csv users.csv\n", os.Args[0])
fmt.Fprintf(os.Stderr, " (For project-scoped keys, also specify -env)\n")
}
flag.Parse()
// Get environment variables
config.PermitAPIKey = os.Getenv("PERMIT_KEY")
config.CognitoUserPoolID = os.Getenv("COGNITO_USER_POOL_ID")
config.CognitoRegion = os.Getenv("AWS_REGION")
if config.CognitoRegion == "" {
config.CognitoRegion = "us-east-1"
}
return config
}
func validateConfig(config AppConfig) error {
// Validate Permit.io config
if config.PermitAPIKey == "" {
return fmt.Errorf("PERMIT_KEY environment variable is not set")
}
if config.PermitProjectID == "" {
return fmt.Errorf("project ID is required")
}
if config.PermitEnvID == "" {
return fmt.Errorf("environment ID is required")
}
// Validate Cognito config
if config.CognitoUserPoolID == "" {
return fmt.Errorf("COGNITO_USER_POOL_ID environment variable is not set")
}
// Validate CSV
if config.CSVPath == "" {
return fmt.Errorf("CSV file path is required")
}
if _, err := os.Stat(config.CSVPath); os.IsNotExist(err) {
return fmt.Errorf("CSV file does not exist: %s", config.CSVPath)
}
// Validate that only one operation type is specified
operationCount := 0
if config.DeleteUsers {
operationCount++
}
if config.DisableUsers {
operationCount++
}
if config.EnableUsers {
operationCount++
}
if operationCount > 1 {
return fmt.Errorf("only one operation can be specified: --delete-users, --disable-users, or --enable-users")
}
return nil
}
func readCSV(path string, config AppConfig) ([]map[string]string, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
reader := csv.NewReader(file)
reader.TrimLeadingSpace = true
reader.FieldsPerRecord = -1 // Allow variable number of fields per record
// Read and validate header
header, err := reader.Read()
if err != nil {
return nil, fmt.Errorf("error reading CSV header: %w", err)
}
if err := validateCSVHeader(header, config); err != nil {
return nil, err
}
// Read all user records
return readUserRecords(reader, config)
}
func validateCSVHeader(header []string, config AppConfig) error {
// For create operations, we need at least 3 columns (email, first_name, last_name)
// For other operations, we only need email (first column)
isCreateOperation := !config.DeleteUsers && !config.DisableUsers && !config.EnableUsers
if isCreateOperation {
expectedHeaders := []string{"email", "first_name", "last_name"}
if len(header) < len(expectedHeaders) {
return fmt.Errorf("CSV must have at least %d columns for user creation: %v", len(expectedHeaders), expectedHeaders)
}
for i, expected := range expectedHeaders {
if strings.ToLower(strings.TrimSpace(header[i])) != expected {
return fmt.Errorf("column %d should be '%s' but got '%s'", i+1, expected, header[i])
}
}
// Check maximum number of roles (99 max = 102 total columns max now without user_id)
if len(header) > 102 {
return fmt.Errorf("CSV can have at most 99 role columns (found %d)", len(header)-3)
}
} else {
// For delete/disable/enable operations, we only need email
if len(header) < 1 {
return fmt.Errorf("CSV must have at least email column")
}
if strings.ToLower(strings.TrimSpace(header[0])) != "email" {
return fmt.Errorf("first column should be 'email' but got '%s'", header[0])
}
// Check maximum number of columns (still enforce reasonable limits)
if len(header) > 102 {
return fmt.Errorf("CSV can have at most 102 columns")
}
}
return nil
}
func readUserRecords(reader *csv.Reader, config AppConfig) ([]map[string]string, error) {
var users []map[string]string
lineNum := 1
for {
record, err := reader.Read()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, fmt.Errorf("error reading line %d: %w", lineNum+1, err)
}
lineNum++
// Skip empty lines
if isEmptyRecord(record) {
continue
}
if isCommentRecord(record) {
continue
}
// Ensure we have at least the required fields
// For create operations, we need at least 3 fields (email, first_name, last_name)
// For other operations, we only need email (first field)
isCreateOperation := !config.DeleteUsers && !config.DisableUsers && !config.EnableUsers
if isCreateOperation && len(record) < 3 {
return nil, fmt.Errorf("line %d: must have at least 3 fields (email, first_name, last_name) for user creation", lineNum)
}
if !isCreateOperation && len(record) < 1 {
return nil, fmt.Errorf("line %d: must have at least email field", lineNum)
}
user, err := parseUserRecord(record, lineNum, config)
if err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
func isEmptyRecord(record []string) bool {
return len(record) == 0 || (len(record) == 1 && record[0] == "")
}
func isCommentRecord(record []string) bool {
return len(record) > 0 && strings.HasPrefix(strings.TrimSpace(record[0]), "#")
}
func parseUserRecord(record []string, lineNum int, config AppConfig) (map[string]string, error) {
user := make(map[string]string)
user["email"] = strings.TrimSpace(record[0])
// Handle optional fields for non-create operations
if len(record) > 1 {
user["first_name"] = strings.TrimSpace(record[1])
} else {
user["first_name"] = ""
}
if len(record) > 2 {
user["last_name"] = strings.TrimSpace(record[2])
} else {
user["last_name"] = ""
}
// Collect roles
user["roles"] = collectRoles(record)
// Validate required fields
if user["email"] == "" {
return nil, fmt.Errorf("line %d: email cannot be empty", lineNum)
}
// Validate first_name and last_name are required for create operations only
isCreateOperation := !config.DeleteUsers && !config.DisableUsers && !config.EnableUsers
if isCreateOperation {
if user["first_name"] == "" {
return nil, fmt.Errorf("line %d: first_name cannot be empty for user creation", lineNum)
}
if user["last_name"] == "" {
return nil, fmt.Errorf("line %d: last_name cannot be empty for user creation", lineNum)
}
}
return user, nil
}
func collectRoles(record []string) string {
var roles []string
for i := 3; i < len(record) && i < 102; i++ { // Max 99 roles, starting from index 3
role := strings.TrimSpace(record[i])
if role != "" {
roles = append(roles, role)
}
}
return strings.Join(roles, ",")
}
/*
processUsers handles the processing of many users.
Because of its complexity and for maintainability, it breaks the work down into the following hierarchy:
processUsers()
└── processSingleUser()
├── ensureCognitoUser()
├── ensurePermitUser()
└── manageUserRoles()
├── parseDesiredRoles()
├── getCurrentRoles()
├── removeUnwantedRoles()
└── addMissingRoles()
└── containsRole()
*/
func processUsers(ctx context.Context, config AppConfig, cognitoClient *cognitoidentityprovider.Client, users []map[string]string, auditLogger *AuditLogger) ImportResult {
result := ImportResult{
TotalUsers: len(users),
StartTime: time.Now(),
}
httpClient := &http.Client{
Timeout: 30 * time.Second,
}
cognitoConfig := &CognitoConfig{
UserPoolID: config.CognitoUserPoolID,
Region: config.CognitoRegion,
}
for i, user := range users {
fmt.Printf("\nProcessing user %d/%d: %s\n", i+1, len(users), user["email"])
// Create processing context
procCtx := &UserProcessingContext{
Config: config,
CognitoClient: cognitoClient,
CognitoConfig: cognitoConfig,
HTTPClient: httpClient,
AuditLogger: auditLogger,
User: user,
Index: i,
Total: len(users),
}
// Process this user
success := processSingleUser(ctx, procCtx, &result)
if success {
result.SuccessfulUsers++
if config.DryRun {
fmt.Printf(" ✅ [DRY RUN] User would be successfully processed\n")
} else {
fmt.Printf(" ✅ User successfully processed in both systems\n")
}
}
// Add a small delay to avoid rate limiting
if !config.DryRun {
time.Sleep(100 * time.Millisecond)
}
}
result.EndTime = time.Now()
return result
}
// processSingleUser handles the complete processing of a single user
func processSingleUser(ctx context.Context, procCtx *UserProcessingContext, result *ImportResult) bool {
// Step 1: Ensure user exists in Cognito
cognitoUser, err := ensureCognitoUser(ctx, procCtx)
if err != nil {
addFailedUser(result, procCtx.User, err.Error(), "cognito")
return false
}
// Step 2: Ensure user exists in Permit.io
err = ensurePermitUser(procCtx, cognitoUser)
if err != nil {
addFailedUser(result, procCtx.User, err.Error(), "permit")
return false
}
// Step 3: Manage user roles
err = manageUserRoles(procCtx, cognitoUser)
if err != nil {
addFailedUser(result, procCtx.User, err.Error(), "permit")
// Don't return false here - user was created, just role assignment may have failed partially
}
return true
}
// ensureCognitoUser ensures the user exists in Cognito (creates if needed)
func ensureCognitoUser(ctx context.Context, procCtx *UserProcessingContext) (*CognitoUserResponse, error) {
user := procCtx.User
config := procCtx.Config
cognitoClient := procCtx.CognitoClient
cognitoConfig := procCtx.CognitoConfig
auditLogger := procCtx.AuditLogger
// Check if user exists in Cognito
if userExistsInCognito(ctx, cognitoClient, cognitoConfig.UserPoolID, user["email"]) {
fmt.Printf(" ️ User already exists in Cognito\n")
auditLogger.LogAction(ActionSkip, SystemCognito, user["email"], ResultSkipped, "User already exists")
// Get existing user details
existingUser, err := getCognitoUser(ctx, cognitoClient, cognitoConfig.UserPoolID, user["email"])
if err != nil {
auditLogger.LogAction(ActionCreate, SystemCognito, user["email"], ResultFailure, fmt.Sprintf("Failed to retrieve existing user: %v", err))
fmt.Printf(" ❌ Failed to retrieve existing Cognito user: %v\n", err)
return nil, fmt.Errorf("failed to retrieve existing Cognito user: %w", err)
}
return existingUser, nil
}
// Create new user in Cognito
if config.DryRun {
fmt.Printf(" 🔍 [DRY RUN] Would create user in Cognito\n")
auditLogger.LogAction(ActionCreate, SystemCognito, user["email"], ResultSuccess, "DRY RUN - Would create user")
// Create a mock user for dry run
return &CognitoUserResponse{
SubjectID: "mock-subject-id-" + user["email"],
Username: user["email"],
Email: user["email"],
FirstName: user["first_name"],
LastName: user["last_name"],
Status: "CONFIRMED",
}, nil
}
cognitoUser, err := createCognitoUser(ctx, cognitoClient, cognitoConfig, user)
if err != nil {
auditLogger.LogAction(ActionCreate, SystemCognito, user["email"], ResultFailure, fmt.Sprintf("Failed to create: %v", err))
fmt.Printf(" ❌ Cognito creation failed: %v\n", err)
return nil, fmt.Errorf("failed to create in Cognito: %w", err)
}
auditLogger.LogActionWithSubjectID(ActionCreate, SystemCognito, user["email"], cognitoUser.SubjectID, ResultSuccess, "User created successfully")
fmt.Printf(" ✓ Created in Cognito with Subject ID: %s\n", cognitoUser.SubjectID)
return cognitoUser, nil
}
// ensurePermitUser ensures the user exists in Permit.io (creates if needed)
func ensurePermitUser(procCtx *UserProcessingContext, cognitoUser *CognitoUserResponse) error {
user := procCtx.User
config := procCtx.Config
httpClient := procCtx.HTTPClient
auditLogger := procCtx.AuditLogger
// Check if user exists in Permit.io
permitExists, permitUserKey, err := userExistsInPermit(httpClient, config, user["email"])
if err != nil {
auditLogger.LogAction(ActionCreate, SystemPermit, user["email"], ResultFailure, fmt.Sprintf("Failed to check user existence: %v", err))
fmt.Printf(" ❌ Failed to check Permit.io user existence: %v\n", err)
return fmt.Errorf("failed to check Permit.io user existence: %w", err)
}
if permitExists {
fmt.Printf(" ️ User already exists in Permit.io with key: %s\n", permitUserKey)
auditLogger.LogActionWithSubjectID(ActionSkip, SystemPermit, user["email"], permitUserKey, ResultSkipped, "User already exists")
return nil
}
// Create user in Permit.io
if config.DryRun {
fmt.Printf(" 🔍 [DRY RUN] Would create user in Permit.io\n")
auditLogger.LogActionWithSubjectID(ActionCreate, SystemPermit, user["email"], cognitoUser.SubjectID, ResultSuccess, "DRY RUN - Would create user")
return nil
}
err = createPermitUser(httpClient, config, cognitoUser)
if err != nil {
auditLogger.LogActionWithSubjectID(ActionCreate, SystemPermit, user["email"], cognitoUser.SubjectID, ResultFailure, fmt.Sprintf("Failed to create: %v", err))
fmt.Printf(" ❌ Permit.io creation failed: %v\n", err)
return fmt.Errorf("failed to create in Permit.io: %w", err)
}
auditLogger.LogActionWithSubjectID(ActionCreate, SystemPermit, user["email"], cognitoUser.SubjectID, ResultSuccess, "User created successfully")
fmt.Printf(" ✓ Created in Permit.io\n")
return nil
}
// manageUserRoles ensures user has exactly the roles specified in CSV
func manageUserRoles(procCtx *UserProcessingContext, cognitoUser *CognitoUserResponse) error {
user := procCtx.User
config := procCtx.Config
httpClient := procCtx.HTTPClient
auditLogger := procCtx.AuditLogger
if user["roles"] == "" {
return nil // No roles to manage
}
// Parse desired roles from CSV
desiredRoles := parseDesiredRoles(user["roles"])
if len(desiredRoles) == 0 {
return nil
}
// Get current roles
currentRoles, err := getCurrentRoles(httpClient, config, cognitoUser.SubjectID, auditLogger, user["email"])
if err != nil {
return err
}
// Remove roles that are no longer needed
err = removeUnwantedRoles(procCtx, cognitoUser, currentRoles, desiredRoles)
if err != nil {
return err
}
// Add roles that are missing
return addMissingRoles(procCtx, cognitoUser, currentRoles, desiredRoles)
}
// parseDesiredRoles parses the roles string from CSV into a slice
func parseDesiredRoles(rolesStr string) []string {
var desiredRoles []string
for _, role := range strings.Split(rolesStr, ",") {
role = strings.TrimSpace(role)
if role != "" {
desiredRoles = append(desiredRoles, role)
}
}
return desiredRoles
}
// getCurrentRoles retrieves the current roles for a user
func getCurrentRoles(httpClient *http.Client, config AppConfig, subjectID string, auditLogger *AuditLogger, email string) ([]string, error) {
if config.DryRun {
return []string{}, nil // Assume no roles for dry run
}
currentRoles, err := getPermitUserRoles(httpClient, config, subjectID)
if err != nil {
fmt.Printf(" ⚠️ Failed to get current roles: %v\n", err)
auditLogger.LogAction(ActionUpdate, SystemPermit, email, ResultFailure, fmt.Sprintf("Failed to get current roles: %v", err))
return []string{}, nil // Continue with empty current roles
}
return currentRoles, nil
}
// removeUnwantedRoles removes roles that are in current but not in desired
func removeUnwantedRoles(procCtx *UserProcessingContext, cognitoUser *CognitoUserResponse, currentRoles, desiredRoles []string) error {
config := procCtx.Config
httpClient := procCtx.HTTPClient
auditLogger := procCtx.AuditLogger
user := procCtx.User
for _, currentRole := range currentRoles {
if !containsRole(desiredRoles, currentRole) {
// Remove this role
if config.DryRun {
fmt.Printf(" 🔍 [DRY RUN] Would remove role: %s\n", currentRole)
auditLogger.LogRoleAction(ActionUnassignRole, user["email"], currentRole, ResultSuccess, "DRY RUN - Would remove role")
} else {
err := unassignRole(httpClient, config, cognitoUser.SubjectID, currentRole)
if err != nil {
fmt.Printf(" ⚠️ Failed to remove role '%s': %v\n", currentRole, err)
auditLogger.LogRoleAction(ActionUnassignRole, user["email"], currentRole, ResultFailure, fmt.Sprintf("Failed to remove: %v", err))
} else {
fmt.Printf(" ✓ Removed role: %s\n", currentRole)
auditLogger.LogRoleAction(ActionUnassignRole, user["email"], currentRole, ResultSuccess, "Role removed successfully")
}
}
}
}
return nil
}
// addMissingRoles adds roles that are in desired but not in current
func addMissingRoles(procCtx *UserProcessingContext, cognitoUser *CognitoUserResponse, currentRoles, desiredRoles []string) error {
config := procCtx.Config
httpClient := procCtx.HTTPClient
auditLogger := procCtx.AuditLogger
user := procCtx.User
var roleErrors []string
for _, desiredRole := range desiredRoles {
if !containsRole(currentRoles, desiredRole) {
// Add this role
if config.DryRun {
fmt.Printf(" 🔍 [DRY RUN] Would assign role: %s\n", desiredRole)
auditLogger.LogRoleAction(ActionAssignRole, user["email"], desiredRole, ResultSuccess, "DRY RUN - Would assign role")
} else {
err := assignRole(httpClient, config, cognitoUser.SubjectID, desiredRole)
if err != nil {
roleErrors = append(roleErrors, fmt.Sprintf("role '%s': %v", desiredRole, err))
auditLogger.LogRoleAction(ActionAssignRole, user["email"], desiredRole, ResultFailure, fmt.Sprintf("Failed to assign: %v", err))
} else {
fmt.Printf(" ✓ Assigned role: %s\n", desiredRole)
auditLogger.LogRoleAction(ActionAssignRole, user["email"], desiredRole, ResultSuccess, "Role assigned successfully")
}
}
} else {
// Role already assigned
fmt.Printf(" ️ Role already assigned: %s\n", desiredRole)
auditLogger.LogRoleAction(ActionSkip, user["email"], desiredRole, ResultSkipped, "Role already assigned")
}
}
if len(roleErrors) > 0 {
return fmt.Errorf("failed to assign roles: %s", strings.Join(roleErrors, "; "))
}
return nil
}
// containsRole checks if a role exists in a slice of roles
func containsRole(roles []string, role string) bool {
for _, r := range roles {
if r == role {
return true
}
}
return false
}
// addFailedUser adds a user to the failed users list
func addFailedUser(result *ImportResult, user map[string]string, errorMsg, failureStep string) {
result.FailedUsers = append(result.FailedUsers, FailedUser{
Email: user["email"],
FirstName: user["first_name"],
LastName: user["last_name"],
Error: errorMsg,
FailureStep: failureStep,
})
}
func deleteUsers(ctx context.Context, config AppConfig, cognitoClient *cognitoidentityprovider.Client, users []map[string]string, auditLogger *AuditLogger) DeleteResult {
result := DeleteResult{
TotalUsers: len(users),
StartTime: time.Now(),
}
httpClient := &http.Client{
Timeout: 30 * time.Second,
}
for i, user := range users {
fmt.Printf("\nDeleting user %d/%d: %s\n", i+1, len(users), user["email"])
// Step 1: Find user in Permit.io by email to get their key (Cognito Subject ID)
permitUserKey, err := findPermitUserByEmail(httpClient, config, user["email"])
if err != nil {
result.FailedUsers = append(result.FailedUsers, FailedUser{
Email: user["email"],
FirstName: user["first_name"],
LastName: user["last_name"],
Error: fmt.Sprintf("Failed to find user in Permit.io: %v", err),
FailureStep: "permit",
})
auditLogger.LogAction(ActionDelete, SystemPermit, user["email"], ResultFailure, fmt.Sprintf("Failed to find user: %v", err))
fmt.Printf(" ❌ Failed to find user in Permit.io: %v\n", err)
continue
}
if permitUserKey == "" {
fmt.Printf(" ️ User not found in Permit.io, skipping Permit.io deletion\n")
auditLogger.LogAction(ActionSkip, SystemPermit, user["email"], ResultSkipped, "User not found in Permit.io")
} else {
fmt.Printf(" ✓ Found user in Permit.io with key: %s\n", permitUserKey)
// Step 2: Delete user from Permit.io
if config.DryRun {
fmt.Printf(" 🔍 [DRY RUN] Would delete from Permit.io\n")
auditLogger.LogActionWithSubjectID(ActionDelete, SystemPermit, user["email"], permitUserKey, ResultSuccess, "DRY RUN - Would delete user")
} else {
err = deletePermitUser(httpClient, config, permitUserKey)
if err != nil {
result.FailedUsers = append(result.FailedUsers, FailedUser{
Email: user["email"],
FirstName: user["first_name"],
LastName: user["last_name"],
Error: fmt.Sprintf("Failed to delete from Permit.io: %v", err),
FailureStep: "permit",
})
auditLogger.LogActionWithSubjectID(ActionDelete, SystemPermit, user["email"], permitUserKey, ResultFailure, fmt.Sprintf("Failed to delete: %v", err))
fmt.Printf(" ❌ Permit.io deletion failed: %v\n", err)
continue
}
auditLogger.LogActionWithSubjectID(ActionDelete, SystemPermit, user["email"], permitUserKey, ResultSuccess, "User deleted successfully")
fmt.Printf(" ✓ Deleted from Permit.io\n")
}
}
// Step 3: Check if user exists in Cognito and delete if needed
if !userExistsInCognito(ctx, cognitoClient, config.CognitoUserPoolID, user["email"]) {
fmt.Printf(" ️ User not found in Cognito, skipping Cognito deletion\n")
auditLogger.LogAction(ActionSkip, SystemCognito, user["email"], ResultSkipped, "User not found in Cognito")
} else {
// Delete user from Cognito (only if they exist)
if config.DryRun {
fmt.Printf(" 🔍 [DRY RUN] Would delete from Cognito\n")
auditLogger.LogAction(ActionDelete, SystemCognito, user["email"], ResultSuccess, "DRY RUN - Would delete user")
} else {
err = deleteCognitoUser(ctx, cognitoClient, config.CognitoUserPoolID, user["email"])
if err != nil {
result.FailedUsers = append(result.FailedUsers, FailedUser{
Email: user["email"],
FirstName: user["first_name"],
LastName: user["last_name"],
Error: fmt.Sprintf("Failed to delete from Cognito: %v", err),
FailureStep: "cognito",
})
auditLogger.LogAction(ActionDelete, SystemCognito, user["email"], ResultFailure, fmt.Sprintf("Failed to delete: %v", err))
fmt.Printf(" ❌ Cognito deletion failed: %v\n", err)
if permitUserKey != "" {
fmt.Printf(" ⚠️ WARNING: User deleted from Permit.io but remains in Cognito!\n")
}
continue
}
auditLogger.LogAction(ActionDelete, SystemCognito, user["email"], ResultSuccess, "User deleted successfully")
fmt.Printf(" ✓ Deleted from Cognito\n")
}
}
// Mark as successful - user was processed (deleted where they existed)
result.SuccessfulUsers++
if config.DryRun {
fmt.Printf(" ✅ [DRY RUN] User would be successfully deleted\n")
} else {
fmt.Printf(" ✅ User successfully deleted from both systems\n")
}
// Add a small delay to avoid rate limiting
if !config.DryRun {
time.Sleep(100 * time.Millisecond)
}
}
result.EndTime = time.Now()
return result
}
func printSummary(result ImportResult) {
duration := result.EndTime.Sub(result.StartTime)
fmt.Println("\n" + strings.Repeat("=", 60))
fmt.Println("IMPORT SUMMARY")
fmt.Println(strings.Repeat("=", 60))
fmt.Printf("Total users processed: %d\n", result.TotalUsers)
fmt.Printf("Successfully created: %d\n", result.SuccessfulUsers)
fmt.Printf("Failed: %d\n", len(result.FailedUsers))
fmt.Printf("Time taken: %s\n", duration.Round(time.Millisecond))
fmt.Printf("Average time per user: %s\n", (duration / time.Duration(result.TotalUsers)).Round(time.Millisecond))
// Group failures by type
cognitoFailures := []FailedUser{}
permitFailures := []FailedUser{}
for _, failed := range result.FailedUsers {
if failed.FailureStep == "cognito" {
cognitoFailures = append(cognitoFailures, failed)
} else {
permitFailures = append(permitFailures, failed)
}
}
if len(cognitoFailures) > 0 {
fmt.Printf("\nCognito Failures (%d):\n", len(cognitoFailures))
fmt.Println(strings.Repeat("-", 60))
for _, failed := range cognitoFailures {
fmt.Printf(" Email: %s\n", failed.Email)
fmt.Printf(" Name: %s %s\n", failed.FirstName, failed.LastName)
fmt.Printf(" Error: %s\n", failed.Error)
fmt.Println()
}
}
if len(permitFailures) > 0 {
fmt.Printf("\nPermit.io Failures (%d):\n", len(permitFailures))
fmt.Println(strings.Repeat("-", 60))
for _, failed := range permitFailures {
fmt.Printf(" Email: %s\n", failed.Email)
fmt.Printf(" Name: %s %s\n", failed.FirstName, failed.LastName)
fmt.Printf(" Error: %s\n", failed.Error)
fmt.Println()
}
}
if result.SuccessfulUsers > 0 {
fmt.Printf("\n✅ Successfully created %d users in both Cognito and Permit.io\n", result.SuccessfulUsers)
}
fmt.Println(strings.Repeat("=", 60))
}
func printDeleteSummary(result DeleteResult) {
duration := result.EndTime.Sub(result.StartTime)
fmt.Println("\n" + strings.Repeat("=", 60))
fmt.Println("DELETE SUMMARY")
fmt.Println(strings.Repeat("=", 60))
fmt.Printf("Total users processed: %d\n", result.TotalUsers)
fmt.Printf("Successfully deleted: %d\n", result.SuccessfulUsers)
fmt.Printf("Failed: %d\n", len(result.FailedUsers))
fmt.Printf("Time taken: %s\n", duration.Round(time.Millisecond))
fmt.Printf("Average time per user: %s\n", (duration / time.Duration(result.TotalUsers)).Round(time.Millisecond))
// Group failures by type
permitFailures := []FailedUser{}
cognitoFailures := []FailedUser{}
for _, failed := range result.FailedUsers {
if failed.FailureStep == "permit" {
permitFailures = append(permitFailures, failed)
} else if failed.FailureStep == "cognito" {
cognitoFailures = append(cognitoFailures, failed)
}
}
if len(permitFailures) > 0 {
fmt.Printf("\nPermit.io Failures (%d):\n", len(permitFailures))
fmt.Println(strings.Repeat("-", 60))
for _, failed := range permitFailures {
fmt.Printf(" Email: %s\n", failed.Email)
fmt.Printf(" Name: %s %s\n", failed.FirstName, failed.LastName)
fmt.Printf(" Error: %s\n", failed.Error)
fmt.Println()
}
}
if len(cognitoFailures) > 0 {
fmt.Printf("\nCognito Failures (%d):\n", len(cognitoFailures))
fmt.Println(strings.Repeat("-", 60))
fmt.Printf("⚠️ WARNING: These users were deleted from Permit.io but remain in Cognito!\n\n")
for _, failed := range cognitoFailures {
fmt.Printf(" Email: %s\n", failed.Email)
fmt.Printf(" Name: %s %s\n", failed.FirstName, failed.LastName)
fmt.Printf(" Error: %s\n", failed.Error)
fmt.Println()
}
}
if result.SuccessfulUsers > 0 {
fmt.Printf("\n✅ Successfully deleted %d users from both Permit.io and Cognito\n", result.SuccessfulUsers)
}
fmt.Println(strings.Repeat("=", 60))
}
func disableEnableUsers(ctx context.Context, config AppConfig, cognitoClient *cognitoidentityprovider.Client, users []map[string]string, auditLogger *AuditLogger, operation string) DisableEnableResult {
result := DisableEnableResult{
TotalUsers: len(users),
StartTime: time.Now(),
OperationType: operation,
}
for i, user := range users {
fmt.Printf("\n%s user %d/%d: %s\n", strings.ToUpper(operation[:1])+operation[1:], i+1, len(users), user["email"])
// Check if user exists in Cognito
if !userExistsInCognito(ctx, cognitoClient, config.CognitoUserPoolID, user["email"]) {
fmt.Printf(" ❌ User not found in Cognito\n")
auditLogger.LogAction(getActionType(operation), SystemCognito, user["email"], ResultFailure, "User not found")
result.FailedUsers = append(result.FailedUsers, FailedUser{
Email: user["email"],
FirstName: user["first_name"],
LastName: user["last_name"],
Error: "User not found in Cognito",
FailureStep: "cognito",
})
continue
}
// Check if user is already in desired state
isEnabled, err := isCognitoUserEnabled(ctx, cognitoClient, config.CognitoUserPoolID, user["email"])
if err != nil {
fmt.Printf(" ❌ Failed to check if user is enabled: %v\n", err)
auditLogger.LogAction(getActionType(operation), SystemCognito, user["email"], ResultFailure, fmt.Sprintf("Failed to check enabled state: %v", err))
result.FailedUsers = append(result.FailedUsers, FailedUser{
Email: user["email"],
FirstName: user["first_name"],
LastName: user["last_name"],
Error: fmt.Sprintf("Failed to check enabled state: %v", err),
FailureStep: "cognito",
})
continue
}
if isUserInDesiredEnabledState(isEnabled, operation) {
fmt.Printf(" ️ User is already %sd\n", operation)
auditLogger.LogAction(ActionSkip, SystemCognito, user["email"], ResultSkipped, fmt.Sprintf("User already %sd", operation))
result.SuccessfulUsers++
continue
}
// Perform the operation
if config.DryRun {
fmt.Printf(" 🔍 [DRY RUN] Would %s user\n", operation)
auditLogger.LogAction(getActionType(operation), SystemCognito, user["email"], ResultSuccess, fmt.Sprintf("DRY RUN - Would %s user", operation))
result.SuccessfulUsers++
} else {
err := performUserOperation(ctx, cognitoClient, config.CognitoUserPoolID, user["email"], operation)
if err != nil {
fmt.Printf(" ❌ Failed to %s user: %v\n", operation, err)
auditLogger.LogAction(getActionType(operation), SystemCognito, user["email"], ResultFailure, fmt.Sprintf("Failed to %s: %v", operation, err))
result.FailedUsers = append(result.FailedUsers, FailedUser{
Email: user["email"],
FirstName: user["first_name"],
LastName: user["last_name"],
Error: fmt.Sprintf("Failed to %s user: %v", operation, err),
FailureStep: "cognito",
})
continue
}
fmt.Printf(" ✅ Successfully %sd user\n", operation)
auditLogger.LogAction(getActionType(operation), SystemCognito, user["email"], ResultSuccess, fmt.Sprintf("User %sd successfully", operation))
result.SuccessfulUsers++
}
// Add a small delay to avoid rate limiting
if !config.DryRun {
time.Sleep(100 * time.Millisecond)
}
}
result.EndTime = time.Now()
return result
}
func getActionType(operation string) ActionType {
if operation == "disable" {
return ActionDisable
}
return ActionEnable
}
func isUserInDesiredEnabledState(isEnabled bool, operation string) bool {
if operation == "disable" {
// User should be disabled (not enabled)
return !isEnabled
} else { // enable
// User should be enabled
return isEnabled
}
}
func performUserOperation(ctx context.Context, client *cognitoidentityprovider.Client, userPoolID, username, operation string) error {
if operation == "disable" {
return disableCognitoUser(ctx, client, userPoolID, username)
}
return enableCognitoUser(ctx, client, userPoolID, username)
}
func printDisableEnableSummary(result DisableEnableResult) {
duration := result.EndTime.Sub(result.StartTime)
fmt.Println("\n" + strings.Repeat("=", 60))
fmt.Printf("%s SUMMARY\n", strings.ToUpper(result.OperationType))
fmt.Println(strings.Repeat("=", 60))
fmt.Printf("Total users processed: %d\n", result.TotalUsers)
fmt.Printf("Successfully %sd: %d\n", result.OperationType, result.SuccessfulUsers)
fmt.Printf("Failed: %d\n", len(result.FailedUsers))
fmt.Printf("Time taken: %s\n", duration.Round(time.Millisecond))
fmt.Printf("Average time per user: %s\n", (duration / time.Duration(result.TotalUsers)).Round(time.Millisecond))
if len(result.FailedUsers) > 0 {
fmt.Printf("\nCognito Failures (%d):\n", len(result.FailedUsers))
fmt.Println(strings.Repeat("-", 60))
for _, failed := range result.FailedUsers {
fmt.Printf(" Email: %s\n", failed.Email)
fmt.Printf(" Name: %s %s\n", failed.FirstName, failed.LastName)
fmt.Printf(" Error: %s\n", failed.Error)
fmt.Println()
}
}
if result.SuccessfulUsers > 0 {
fmt.Printf("\n✅ Successfully %sd %d users in Cognito\n", result.OperationType, result.SuccessfulUsers)
}
fmt.Println(strings.Repeat("=", 60))
}