Merged in feature/permit.api.keys (pull request #196)
permit keys * permit keys use the environment from the api key if requested
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 <PERMIT_API_KEY>
|
||||
#
|
||||
# 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 <PERMIT_API_KEY>" >&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 <env_key>)"
|
||||
else
|
||||
echo "Key Type: Organization-scoped (must use: -project <project_key> -env <env_key>)"
|
||||
fi
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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`):
|
||||
@@ -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)
|
||||
@@ -381,6 +453,7 @@ Your AWS credentials need these Cognito permissions:
|
||||
```
|
||||
|
||||
**New Permissions Required**:
|
||||
|
||||
- `cognito-idp:AdminDisableUser`: Required for `--disable-users` functionality
|
||||
- `cognito-idp:AdminEnableUser`: Required for `--enable-users` functionality
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user