From ab7072ac9048955e307ec0e98929746edd115b38 Mon Sep 17 00:00:00 2001 From: Jay Brown Date: Thu, 11 Dec 2025 22:18:42 +0000 Subject: [PATCH] Merged in feature/permit.api.keys (pull request #196) permit keys * permit keys use the environment from the api key if requested --- .../user.creation.tool/cognito-permit-sync.go | 173 ++++++++++++------ .../user.creation.tool/cognito.go | 22 ++- .../user.creation.tool/get.scope.from.key.sh | 84 +++++++++ cmd/cognito_test/user.creation.tool/permit.go | 52 ++++++ cmd/cognito_test/user.creation.tool/readme.md | 131 ++++++++++--- .../user.creation.tool/users.to.import.csv | 4 +- 6 files changed, 377 insertions(+), 89 deletions(-) create mode 100755 cmd/cognito_test/user.creation.tool/get.scope.from.key.sh diff --git a/cmd/cognito_test/user.creation.tool/cognito-permit-sync.go b/cmd/cognito_test/user.creation.tool/cognito-permit-sync.go index fcbe94f4..b9c775cc 100644 --- a/cmd/cognito_test/user.creation.tool/cognito-permit-sync.go +++ b/cmd/cognito_test/user.creation.tool/cognito-permit-sync.go @@ -109,6 +109,47 @@ func main() { } } +// 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() @@ -126,9 +167,7 @@ func run() error { fmt.Println(strings.Repeat("=", 60)) } - sanityCheckCredentials() - - // Check if we should list projects/environments + // 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") @@ -137,6 +176,16 @@ func run() error { 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) @@ -197,57 +246,8 @@ func run() error { } fmt.Println() - if config.DeleteUsers { - // Delete mode - 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)) - - // Process user deletions - result := deleteUsers(ctx, config, cognitoClient, users, auditLogger) - - // Print delete summary - printDeleteSummary(result) - } else if config.DisableUsers { - // Disable mode - fmt.Printf("Found %d users to DISABLE\n", len(users)) - fmt.Printf("Will disable users in Cognito User Pool: %s\n", config.CognitoUserPoolID) - fmt.Println(strings.Repeat("=", 60)) - - // Process user disabling - result := disableEnableUsers(ctx, config, cognitoClient, users, auditLogger, "disable") - - // Print disable/enable summary - printDisableEnableSummary(result) - } else if config.EnableUsers { - // Enable mode - fmt.Printf("Found %d users to ENABLE\n", len(users)) - fmt.Printf("Will enable users in Cognito User Pool: %s\n", config.CognitoUserPoolID) - fmt.Println(strings.Repeat("=", 60)) - - // Process user enabling - result := disableEnableUsers(ctx, config, cognitoClient, users, auditLogger, "enable") - - // Print disable/enable summary - printDisableEnableSummary(result) - } else { - // Create mode (existing functionality) - 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)) - - // Process users - result := processUsers(ctx, config, cognitoClient, users, auditLogger) - - // Print summary - printSummary(result) - } + // Execute the requested operation + executeOperation(ctx, config, cognitoClient, users, auditLogger) // Log dry run end if applicable if config.DryRun { @@ -260,12 +260,70 @@ func run() error { 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 or key (required)") - flag.StringVar(&config.PermitEnvID, "env", "", "Permit.io environment ID or key (required)") + 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 @@ -309,6 +367,9 @@ func parseFlags() AppConfig { 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() diff --git a/cmd/cognito_test/user.creation.tool/cognito.go b/cmd/cognito_test/user.creation.tool/cognito.go index 06af524e..ee18f9bd 100644 --- a/cmd/cognito_test/user.creation.tool/cognito.go +++ b/cmd/cognito_test/user.creation.tool/cognito.go @@ -40,7 +40,13 @@ type CognitoUserResponse struct { Enabled bool } -func sanityCheckCredentials() { +// sanityCheckCredentials validates that both AWS and Permit.io credentials are properly configured. +// It checks AWS credentials by calling STS GetCallerIdentity and Permit.io credentials by +// calling the API key scope endpoint. +// +// Parameters: +// - appConfig: Application configuration containing Permit.io API key and base URL +func sanityCheckCredentials(appConfig AppConfig) { awsProfile := os.Getenv("AWS_PROFILE") var cfgOpts []func(*config.LoadOptions) error @@ -62,18 +68,22 @@ func sanityCheckCredentials() { os.Exit(1) } - permitKey := os.Getenv("PERMIT_KEY") - if permitKey == "" { + if appConfig.PermitAPIKey == "" { fmt.Fprintf(os.Stderr, "Permit.io: PERMIT_KEY environment variable is not set\n") os.Exit(1) } - req, _ := http.NewRequest("GET", "https://api.permit.io/v2/projects", nil) - req.Header.Add("Authorization", "Bearer "+permitKey) + // Use the API key scope endpoint to validate the key - this works for all key types + req, _ := http.NewRequest("GET", appConfig.PermitBaseURL+"/v2/api-key/scope", nil) + req.Header.Add("Authorization", "Bearer "+appConfig.PermitAPIKey) client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil || resp.StatusCode < 200 || resp.StatusCode >= 300 { - fmt.Fprintf(os.Stderr, "Permit.io: Credential check failed: %v, status=%v\n", err, resp.StatusCode) + statusCode := 0 + if resp != nil { + statusCode = resp.StatusCode + } + fmt.Fprintf(os.Stderr, "Permit.io: Credential check failed: %v, status=%v\n", err, statusCode) os.Exit(1) } resp.Body.Close() diff --git a/cmd/cognito_test/user.creation.tool/get.scope.from.key.sh b/cmd/cognito_test/user.creation.tool/get.scope.from.key.sh new file mode 100755 index 00000000..0530916d --- /dev/null +++ b/cmd/cognito_test/user.creation.tool/get.scope.from.key.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# get.scope.from.key.sh - Retrieves the scope (project/environment) for a Permit.io API key +# +# Usage: ./get.scope.from.key.sh +# +# This script calls the Permit.io API to introspect the provided API key and +# returns the organization, project, and environment IDs and names associated with it. + +if [ -z "$1" ]; then + echo "Usage: $0 " >&2 + echo "Example: $0 permit_key_abc123..." >&2 + exit 1 +fi + +API_KEY="$1" +BASE_URL="https://api.permit.io" + +# Get the scope IDs +SCOPE=$(curl -s -H "Authorization: Bearer ${API_KEY}" "${BASE_URL}/v2/api-key/scope") + +if [ -z "$SCOPE" ] || echo "$SCOPE" | jq -e '.error' > /dev/null 2>&1; then + echo "Error fetching API key scope:" + echo "$SCOPE" | jq + exit 1 +fi + +ORG_ID=$(echo "$SCOPE" | jq -r '.organization_id // empty') +PROJECT_ID=$(echo "$SCOPE" | jq -r '.project_id // empty') +ENV_ID=$(echo "$SCOPE" | jq -r '.environment_id // empty') + +echo "API Key Scope" +echo "=============" +echo "" + +# Organization info +if [ -n "$ORG_ID" ]; then + echo "Organization:" + echo " ID: $ORG_ID" +fi + +# Project info - fetch name if we have a project ID +if [ -n "$PROJECT_ID" ]; then + PROJECT_INFO=$(curl -s -H "Authorization: Bearer ${API_KEY}" "${BASE_URL}/v2/projects/${PROJECT_ID}" 2>/dev/null) + PROJECT_NAME=$(echo "$PROJECT_INFO" | jq -r '.name // empty' 2>/dev/null) + PROJECT_KEY=$(echo "$PROJECT_INFO" | jq -r '.key // empty' 2>/dev/null) + + echo "" + echo "Project:" + echo " ID: $PROJECT_ID" + if [ -n "$PROJECT_NAME" ]; then + echo " Name: $PROJECT_NAME" + fi + if [ -n "$PROJECT_KEY" ]; then + echo " Key: $PROJECT_KEY" + fi +fi + +# Environment info - fetch name if we have an environment ID +if [ -n "$ENV_ID" ] && [ -n "$PROJECT_ID" ]; then + ENV_INFO=$(curl -s -H "Authorization: Bearer ${API_KEY}" "${BASE_URL}/v2/projects/${PROJECT_ID}/envs/${ENV_ID}" 2>/dev/null) + ENV_NAME=$(echo "$ENV_INFO" | jq -r '.name // empty' 2>/dev/null) + ENV_KEY=$(echo "$ENV_INFO" | jq -r '.key // empty' 2>/dev/null) + + echo "" + echo "Environment:" + echo " ID: $ENV_ID" + if [ -n "$ENV_NAME" ]; then + echo " Name: $ENV_NAME" + fi + if [ -n "$ENV_KEY" ]; then + echo " Key: $ENV_KEY" + fi +fi + +# Determine key type +echo "" +echo "---" +if [ -n "$ENV_ID" ]; then + echo "Key Type: Environment-scoped (can use: -project use-key)" +elif [ -n "$PROJECT_ID" ]; then + echo "Key Type: Project-scoped (can use: -project use-key -env )" +else + echo "Key Type: Organization-scoped (must use: -project -env )" +fi diff --git a/cmd/cognito_test/user.creation.tool/permit.go b/cmd/cognito_test/user.creation.tool/permit.go index ca6beba5..aed70b62 100644 --- a/cmd/cognito_test/user.creation.tool/permit.go +++ b/cmd/cognito_test/user.creation.tool/permit.go @@ -30,6 +30,58 @@ type RoleAssignment struct { Tenant string `json:"tenant"` } +// APIKeyScope represents the scope information returned by the Permit.io API key introspection endpoint. +// It contains the organization, project, and environment IDs that the API key has access to. +// Organization-level keys will have only OrganizationID set. +// Project-level keys will have OrganizationID and ProjectID set. +// Environment-level keys will have all three fields set. +type APIKeyScope struct { + OrganizationID string `json:"organization_id"` + ProjectID string `json:"project_id,omitempty"` + EnvironmentID string `json:"environment_id,omitempty"` +} + +// getAPIKeyScope retrieves the scope information for the provided Permit.io API key. +// This allows the tool to introspect a project-level or environment-level API key +// to automatically determine the project and environment IDs without requiring +// the user to specify them explicitly. +// +// Parameters: +// - client: HTTP client for making API requests +// - config: Application configuration containing the Permit.io API key and base URL +// +// Returns: +// - *APIKeyScope: The scope information containing organization, project, and environment IDs +// - error: Error if the API call fails or response cannot be parsed +func getAPIKeyScope(client *http.Client, config AppConfig) (*APIKeyScope, error) { + url := fmt.Sprintf("%s/v2/api-key/scope", config.PermitBaseURL) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+config.PermitAPIKey) + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) + } + + var scope APIKeyScope + if err := json.NewDecoder(resp.Body).Decode(&scope); err != nil { + return nil, fmt.Errorf("error decoding response: %w", err) + } + + return &scope, nil +} + func createPermitUser(client *http.Client, config AppConfig, cognitoUser *CognitoUserResponse) error { url := fmt.Sprintf("%s/v2/facts/%s/%s/users", config.PermitBaseURL, config.PermitProjectID, config.PermitEnvID) diff --git a/cmd/cognito_test/user.creation.tool/readme.md b/cmd/cognito_test/user.creation.tool/readme.md index e6f70815..f4831d8e 100644 --- a/cmd/cognito_test/user.creation.tool/readme.md +++ b/cmd/cognito_test/user.creation.tool/readme.md @@ -5,6 +5,7 @@ A CLI tool that creates, updates, deletes, disables, and enables users in AWS Co ## Overview This tool: + 1. Reads user data from a CSV file (supports comments with #) 2. **Idempotently** creates or updates users in AWS Cognito 3. **Idempotently** creates or updates corresponding users in Permit.io @@ -28,6 +29,7 @@ This tool: ### Delete Mode (`--delete-users`) Runs the inverse of the normal create operation, deleting any users listed in the CSV: + 1. Reads user data from a CSV file 2. Finds each user in Permit.io by email (skips if not found) 3. Deletes the user from Permit.io (if exists) @@ -45,6 +47,7 @@ email,first_name,last_name,admin,viewer ### Disable Mode (`--disable-users`) Disables users in AWS Cognito without deleting them. This prevents users from logging in while preserving their account and settings: + 1. Reads user data from a CSV file 2. Checks if each user exists in Cognito 3. Disables the user account in Cognito (if exists and enabled) @@ -52,6 +55,7 @@ Disables users in AWS Cognito without deleting them. This prevents users from lo 5. **Idempotent**: Reports "already disabled" for users who are already disabled **Important Notes**: + - Disabled users cannot authenticate or access applications - User data, roles, and settings are preserved - Users can be re-enabled later using `--enable-users` @@ -61,6 +65,7 @@ Disables users in AWS Cognito without deleting them. This prevents users from lo ### Enable Mode (`--enable-users`) Re-enables previously disabled users in AWS Cognito: + 1. Reads user data from a CSV file 2. Checks if each user exists in Cognito 3. Enables the user account in Cognito (if exists and disabled) @@ -68,6 +73,7 @@ Re-enables previously disabled users in AWS Cognito: 5. **Idempotent**: Reports "already enabled" for users who are already enabled **Important Notes**: + - Only works on existing Cognito users - Does not create new users - use default mode for user creation - Only affects Cognito enable/disable status - does not modify roles @@ -76,16 +82,32 @@ Re-enables previously disabled users in AWS Cognito: ## Prerequisites 1. **AWS Account** with: + - An existing Cognito User Pool - Appropriate IAM permissions for Cognito operations - AWS credentials configured + If you are running this from the devbox environment you can use the `task aws:login` to perform the login for you and authenticate your cli to be the same as your broswer aws credentials. + If you are running this in the aws cloud console it should just work. + 2. **Permit.io Account** with: - A project and environment set up - - API key with user creation permissions + - API key with user creation permissions (organization, project, or environment-level key) ## Configuration +### Permit.io API Key Types + +Permit.io supports three levels of API keys, and this tool works with all of them: + +| Key Type | Scope | Usage | +| ---------------- | ------------------------------------ | --------------------------------------------------------------- | +| **Organization** | All projects and environments | Use with explicit `-project` and `-env` flags | +| **Project** | Single project, all its environments | Use with `-project use-key` and explicit `-env` flag | +| **Environment** | Single environment only | Use with `-project use-key` (auto-detects both project and env) | + +**Recommendation**: Use environment-scoped keys for production deployments as they provide the principle of least privilege. + ### Environment Variables Create a `.env` file (see `.env.example`): @@ -100,7 +122,7 @@ COGNITO_USER_POOL_ID=us-east-1_xxxxxxxxx # AWS Configuration AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=your-access-key # Or use IAM role -AWS_SECRET_ACCESS_KEY=your-secret-key +AWS_SECRET_ACCESS_KEY=your-secret-key # or use profile AWS_PROFILE=name_of_sso_profile_that_is_already_logged_in ``` @@ -161,10 +183,36 @@ export $(cat .env | xargs) ./cognito-permit-sync -project list ``` +### Using Project/Environment-Scoped API Keys + +If you have a **project-scoped** or **environment-scoped** Permit.io API key (rather than an organization-level key), you can use `-project use-key` to automatically detect the project and environment from the API key: + +```bash +# With an environment-scoped key (auto-detects both project and environment) +./cognito-permit-sync -project use-key -csv users.csv + +# With a project-scoped key (auto-detects project, requires -env flag) +./cognito-permit-sync -project use-key -env production -csv users.csv +``` + +This is useful when: + +- You have a scoped API key and don't know the project/environment IDs +- You want to avoid hardcoding project IDs in scripts +- You're working with environment-specific deployments + +To check what project/environment your API key is scoped to: + +```bash +./get.scope.from.key.sh your_permit_api_key +``` + ### Command Line Options -- `-project`: Permit.io project ID or key (required, or "list" to show available projects) -- `-env`: Permit.io environment ID or key (required) +- `-project`: Permit.io project ID/key, or special values: + - `list` - Show available projects and environments + - `use-key` - Auto-detect project from API key scope +- `-env`: Permit.io environment ID or key (required unless using `use-key` with an environment-scoped API key) - `-csv`: Path to CSV file containing users (required) - `-api-url`: Permit.io API base URL (default: https://api.permit.io) - `--delete-users`: Delete users instead of creating/updating them @@ -181,10 +229,12 @@ export $(cat .env | xargs) For each user in the CSV: 1. **Checks if user exists in Cognito**: - - If exists: Retrieves existing user details + + - If exists: Retrieves existing user details (including Subject ID) - If not exists: Creates new user with email as username, sets email_verified to true 2. **Checks if user exists in Permit.io**: + - If exists: Uses existing user - If not exists: Creates user with Cognito Subject ID as key @@ -194,11 +244,26 @@ For each user in the CSV: - Adds new roles specified in CSV - Skips roles that are already assigned correctly +#### Syncing Existing Cognito Users to a New Permit.io Project + +The tool handles the scenario where users already exist in Cognito but need to be synced to a different Permit.io project: + +- **Cognito**: User is detected as existing, details are retrieved (no changes made) +- **Permit.io**: User is created in the target project using their existing Cognito Subject ID +- **Roles**: Assigned according to the CSV specification + +This is useful when: + +- Migrating users to a new Permit.io project +- Setting up a new environment with existing Cognito users +- Syncing users across multiple Permit.io projects + ### Delete Mode (`--delete-users`) For each user in the CSV: 1. **Finds user in Permit.io by email**: + - If found: Proceeds with deletion - If not found: Skips Permit.io deletion @@ -213,10 +278,12 @@ For each user in the CSV: For each user in the CSV: 1. **Checks if user exists in Cognito**: + - If not found: Reports error and skips - If found: Proceeds with disable check 2. **Checks current user status**: + - If already disabled: Reports "already disabled" and skips - If enabled: Proceeds with disable operation @@ -230,10 +297,12 @@ For each user in the CSV: For each user in the CSV: 1. **Checks if user exists in Cognito**: + - If not found: Reports error and skips - If found: Proceeds with enable check 2. **Checks current user status**: + - If already enabled: Reports "already enabled" and skips - If disabled: Proceeds with enable operation @@ -245,6 +314,7 @@ For each user in the CSV: ### Dry-Run Mode (`--dry-run`) Works with all operation modes: + - Simulates all operations without making actual API calls - Shows exactly what would be done - Logs all actions to `dry-run-audit.log` @@ -261,6 +331,7 @@ Works with all operation modes: ### Audit Log Format Each audit entry includes: + - Timestamp (YYYY-MM-DD HH:MM:SS) - Action type (CREATE, DELETE, DISABLE, ENABLE, ASSIGN_ROLE, UNASSIGN_ROLE, SKIP) - System (COGNITO, PERMIT) @@ -353,6 +424,7 @@ The tool handles various error scenarios gracefully: ### Role Management Examples If a user currently has roles `[admin, viewer, old_role]` and CSV specifies `[admin, editor]`: + - ✅ Keeps: `admin` (already assigned) - ➕ Adds: `editor` (new role) - ➖ Removes: `viewer`, `old_role` (no longer needed) @@ -363,24 +435,25 @@ Your AWS credentials need these Cognito permissions: ```json { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "cognito-idp:AdminCreateUser", - "cognito-idp:AdminGetUser", - "cognito-idp:AdminDeleteUser", - "cognito-idp:AdminDisableUser", - "cognito-idp:AdminEnableUser" - ], - "Resource": "arn:aws:cognito-idp:REGION:ACCOUNT:userpool/USER_POOL_ID" - } - ] + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "cognito-idp:AdminCreateUser", + "cognito-idp:AdminGetUser", + "cognito-idp:AdminDeleteUser", + "cognito-idp:AdminDisableUser", + "cognito-idp:AdminEnableUser" + ], + "Resource": "arn:aws:cognito-idp:REGION:ACCOUNT:userpool/USER_POOL_ID" + } + ] } ``` **New Permissions Required**: + - `cognito-idp:AdminDisableUser`: Required for `--disable-users` functionality - `cognito-idp:AdminEnableUser`: Required for `--enable-users` functionality @@ -392,7 +465,7 @@ Your AWS credentials need these Cognito permissions: 4. **Backup Strategy**: Keep CSV files versioned for rollback capability 5. **Monitor Performance**: Use built-in timing information for optimization 6. **Test with Small Batches**: Validate with a few users before bulk operations -7. **Disable vs Delete**: +7. **Disable vs Delete**: - Use `--disable-users` to temporarily prevent access while preserving user data - Use `--delete-users` only when permanently removing users - Disabled users can be re-enabled with `--enable-users` @@ -401,10 +474,12 @@ Your AWS credentials need these Cognito permissions: ## Troubleshooting ### "User already exists" (No Longer an Error) + - ✅ **Fixed**: Tool now handles existing users gracefully - ✅ **Action**: Continues with role management for existing users ### Audit Log Analysis + ```bash # Check recent audit activity tail -f audit.log @@ -423,17 +498,20 @@ cat dry-run-audit.log ``` ### Disable/Enable Issues + - **Permission Errors**: Ensure your AWS credentials have `AdminDisableUser` and `AdminEnableUser` permissions - **User Not Found**: Disable/enable operations only work on existing Cognito users - **Already Disabled/Enabled**: The tool reports current state - this is normal idempotent behavior - **Verification**: Check audit logs to confirm operations completed successfully ### Role Synchronization Issues + - **Check audit log** for role assignment/removal details - **Verify CSV format** for role columns - **Use dry-run mode** to preview role changes ### API Rate Limiting + - Built-in 100ms delays between operations - Monitor API response times in console output - Consider breaking large CSV files into smaller batches @@ -441,6 +519,7 @@ cat dry-run-audit.log ## Development ### Running Tests + Set your .env with the required variables or make them part of your environment: ```bash @@ -476,10 +555,12 @@ GOOS=windows GOARCH=amd64 go build -o cognito-permit-sync.exe - **✅ CSV Comments**: Support for comment lines with # - **✅ Better Error Handling**: Graceful handling of missing/existing users - **✅ Performance Insights**: Detailed timing and success metrics -- **🆕 User Disable/Enable**: Temporarily disable users without deletion -- **🆕 Enhanced Access Control**: Fine-grained user access management -- **🆕 Extended Audit Trail**: Track all disable/enable operations -- **🆕 Idempotent Disable/Enable**: Smart state checking prevents unnecessary operations +- **✅ User Disable/Enable**: Temporarily disable users without deletion +- **✅ Enhanced Access Control**: Fine-grained user access management +- **✅ Extended Audit Trail**: Track all disable/enable operations +- **✅ Idempotent Disable/Enable**: Smart state checking prevents unnecessary operations +- **🆕 Scoped API Key Support**: Use `-project use-key` to auto-detect project/environment from API key +- **🆕 Cross-Project User Sync**: Sync existing Cognito users to new Permit.io projects ## Future Enhancements @@ -493,4 +574,4 @@ GOOS=windows GOARCH=amd64 go build -o cognito-permit-sync.exe ## License -[Your License Here] \ No newline at end of file +[Your License Here] diff --git a/cmd/cognito_test/user.creation.tool/users.to.import.csv b/cmd/cognito_test/user.creation.tool/users.to.import.csv index 441a358f..33796d5d 100644 --- a/cmd/cognito_test/user.creation.tool/users.to.import.csv +++ b/cmd/cognito_test/user.creation.tool/users.to.import.csv @@ -1,3 +1,3 @@ email,first_name,last_name,admin,viewer -"foo1@gmail.com",john, smith,editor,viewer -"foo2@gmail.com",jane, smith,editor,viewer, admin +"foo1@gmail.com",john, smith,auditor,user_admin +"foo2@gmail.com",jane, smith,client_user,super_admin