6dccf494f8
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
1283 lines
44 KiB
Go
1283 lines
44 KiB
Go
// 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))
|
||
}
|