Merged in feature/cognito-shared-environments (pull request #211)

cognito and permit shared prod environment support

* code complete

* user creattion tool

test harness
This commit is contained in:
Jay Brown
2026-02-26 12:33:35 +00:00
parent 6dccf494f8
commit c668485e6f
55 changed files with 3721 additions and 381 deletions
@@ -231,7 +231,7 @@ func main() {
config.SetAuthRoutePermissions(routePermissions)
// Register authentication routes
cognitoauth.RegisterRoutes(e, config)
cognitoauth.RegisterRoutes(e, config, nil)
// Register your application routes
// These will be protected based on the permissions set above
@@ -18,6 +18,8 @@ import (
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
"github.com/joho/godotenv"
"queryorchestration/internal/usermanagement"
)
type ErrorResponse struct {
@@ -72,8 +74,9 @@ type AppConfig struct {
PermitBaseURL string
// Cognito config
CognitoUserPoolID string
CognitoRegion string
CognitoUserPoolID string
CognitoRegion string
CognitoEnvironmentID string
// Other
CSVPath string
@@ -326,6 +329,9 @@ func parseFlags() AppConfig {
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")
// Cognito environment isolation flag
flag.StringVar(&config.CognitoEnvironmentID, "cognito-env-id", "", "Cognito environment ID to tag users with (falls back to COGNITO_ENVIRONMENT_ID env var)")
// File flag
flag.StringVar(&config.CSVPath, "csv", "", "Path to CSV file containing users (required)")
@@ -349,6 +355,7 @@ func parseFlags() AppConfig {
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, " COGNITO_ENVIRONMENT_ID Cognito environment ID for user tagging (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")
@@ -383,6 +390,11 @@ func parseFlags() AppConfig {
config.CognitoRegion = "us-east-1"
}
// Fall back to env var for cognito environment ID if flag not set
if config.CognitoEnvironmentID == "" {
config.CognitoEnvironmentID = os.Getenv(usermanagement.CognitoEnvIDEnvVar)
}
return config
}
@@ -405,6 +417,11 @@ func validateConfig(config AppConfig) error {
return fmt.Errorf("COGNITO_USER_POOL_ID environment variable is not set")
}
// Validate Cognito environment ID if set
if err := usermanagement.ValidateCognitoEnvironmentID(config.CognitoEnvironmentID); err != nil {
return fmt.Errorf("invalid cognito environment ID: %w", err)
}
// Validate CSV
if config.CSVPath == "" {
return fmt.Errorf("CSV file path is required")
@@ -725,16 +742,17 @@ func ensureCognitoUser(ctx context.Context, procCtx *UserProcessingContext) (*Co
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",
SubjectID: "mock-subject-id-" + user["email"],
Username: user["email"],
Email: user["email"],
FirstName: user["first_name"],
LastName: user["last_name"],
Status: "CONFIRMED",
EnvironmentID: config.CognitoEnvironmentID,
}, nil
}
cognitoUser, err := createCognitoUser(ctx, cognitoClient, cognitoConfig, user)
cognitoUser, err := createCognitoUser(ctx, cognitoClient, cognitoConfig, user, config.CognitoEnvironmentID)
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)
+38 -23
View File
@@ -16,6 +16,8 @@ import (
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"queryorchestration/internal/usermanagement"
)
// Cognito types
@@ -31,13 +33,14 @@ type UserInput struct {
}
type CognitoUserResponse struct {
SubjectID string
Username string
Email string
FirstName string
LastName string
Status string
Enabled bool
SubjectID string
Username string
Email string
FirstName string
LastName string
Status string
Enabled bool
EnvironmentID string
}
// sanityCheckCredentials validates that both AWS and Permit.io credentials are properly configured.
@@ -107,7 +110,7 @@ func initializeCognitoClient() (*cognitoidentityprovider.Client, error) {
return client, nil
}
func createCognitoUser(ctx context.Context, client *cognitoidentityprovider.Client, config *CognitoConfig, user map[string]string) (*CognitoUserResponse, error) {
func createCognitoUser(ctx context.Context, client *cognitoidentityprovider.Client, config *CognitoConfig, user map[string]string, environmentID string) (*CognitoUserResponse, error) {
// Use email as username
username := user["email"]
@@ -131,6 +134,14 @@ func createCognitoUser(ctx context.Context, client *cognitoidentityprovider.Clie
},
}
// Append environment ID attribute when configured
if environmentID != "" {
userAttributes = append(userAttributes, types.AttributeType{
Name: aws.String(usermanagement.CognitoEnvIDAttrName),
Value: aws.String(environmentID),
})
}
// Prepare the AdminCreateUser input
createUserInput := &cognitoidentityprovider.AdminCreateUserInput{
UserPoolId: aws.String(config.UserPoolID),
@@ -156,13 +167,14 @@ func createCognitoUser(ctx context.Context, client *cognitoidentityprovider.Clie
// Build response
response := &CognitoUserResponse{
SubjectID: subjectID,
Username: aws.ToString(result.User.Username),
Email: user["email"],
FirstName: user["first_name"],
LastName: user["last_name"],
Status: string(result.User.UserStatus),
Enabled: result.User.Enabled,
SubjectID: subjectID,
Username: aws.ToString(result.User.Username),
Email: user["email"],
FirstName: user["first_name"],
LastName: user["last_name"],
Status: string(result.User.UserStatus),
Enabled: result.User.Enabled,
EnvironmentID: environmentID,
}
return response, nil
@@ -195,7 +207,7 @@ func getCognitoUser(ctx context.Context, client *cognitoidentityprovider.Client,
}
// Extract attributes
var subjectID, email, firstName, lastName string
var subjectID, email, firstName, lastName, environmentID string
for _, attr := range result.UserAttributes {
switch aws.ToString(attr.Name) {
case "sub":
@@ -206,17 +218,20 @@ func getCognitoUser(ctx context.Context, client *cognitoidentityprovider.Client,
firstName = aws.ToString(attr.Value)
case "family_name":
lastName = aws.ToString(attr.Value)
case usermanagement.CognitoEnvIDAttrName:
environmentID = aws.ToString(attr.Value)
}
}
return &CognitoUserResponse{
SubjectID: subjectID,
Username: aws.ToString(result.Username),
Email: email,
FirstName: firstName,
LastName: lastName,
Status: string(result.UserStatus),
Enabled: result.Enabled,
SubjectID: subjectID,
Username: aws.ToString(result.Username),
Email: email,
FirstName: firstName,
LastName: lastName,
Status: string(result.UserStatus),
Enabled: result.Enabled,
EnvironmentID: environmentID,
}, nil
}
@@ -0,0 +1,116 @@
package main
import (
"flag"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"queryorchestration/internal/usermanagement"
)
// TestParseFlags_CognitoEnvIDFlagOverridesEnvVar verifies that the -cognito-env-id flag
// takes precedence over the COGNITO_ENVIRONMENT_ID environment variable. This tests the
// fallback logic at cognito-permit-sync.go:393-396.
func TestParseFlags_CognitoEnvIDFlagOverridesEnvVar(t *testing.T) {
// Set the env var that would normally be the fallback
t.Setenv(usermanagement.CognitoEnvIDEnvVar, "fromenv")
// Reset the global flag.CommandLine to allow re-parsing
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
// Simulate command-line args with the flag set
origArgs := os.Args
os.Args = []string{"test", "-cognito-env-id=fromflag", "-csv=dummy.csv", "-project=test", "-env=test"}
defer func() { os.Args = origArgs }()
config := parseFlags()
assert.Equal(t, "fromflag", config.CognitoEnvironmentID,
"Flag -cognito-env-id should override COGNITO_ENVIRONMENT_ID env var")
}
// TestParseFlags_CognitoEnvIDFallsBackToEnvVar verifies that when the -cognito-env-id
// flag is not provided, the COGNITO_ENVIRONMENT_ID env var is used as a fallback.
func TestParseFlags_CognitoEnvIDFallsBackToEnvVar(t *testing.T) {
t.Setenv(usermanagement.CognitoEnvIDEnvVar, "fromenv")
// Reset the global flag.CommandLine to allow re-parsing
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
origArgs := os.Args
os.Args = []string{"test", "-csv=dummy.csv", "-project=test", "-env=test"}
defer func() { os.Args = origArgs }()
config := parseFlags()
assert.Equal(t, "fromenv", config.CognitoEnvironmentID,
"COGNITO_ENVIRONMENT_ID env var should be used when flag is not set")
}
// TestValidateConfig_InvalidCognitoEnvID verifies that validateConfig returns an error
// when CognitoEnvironmentID contains invalid characters (e.g., uppercase letters).
func TestValidateConfig_InvalidCognitoEnvID(t *testing.T) {
// Create a temp CSV file so file validation passes
tmpFile, err := os.CreateTemp(t.TempDir(), "test-*.csv")
require.NoError(t, err)
defer tmpFile.Close()
config := AppConfig{
PermitAPIKey: "permit_key_test",
PermitProjectID: "testproject",
PermitEnvID: "testenv",
CognitoUserPoolID: "us-east-1_TestPool",
CognitoRegion: "us-east-1",
CognitoEnvironmentID: "UPPERCASE", // invalid: must be lowercase
CSVPath: tmpFile.Name(),
}
err = validateConfig(config)
assert.Error(t, err, "Should reject uppercase environment ID")
assert.Contains(t, err.Error(), "invalid cognito environment ID")
}
// TestValidateConfig_ValidCognitoEnvID verifies that validateConfig accepts a valid
// lowercase environment ID without errors from the env ID validation.
func TestValidateConfig_ValidCognitoEnvID(t *testing.T) {
tmpFile, err := os.CreateTemp(t.TempDir(), "test-*.csv")
require.NoError(t, err)
defer tmpFile.Close()
config := AppConfig{
PermitAPIKey: "permit_key_test",
PermitProjectID: "testproject",
PermitEnvID: "testenv",
CognitoUserPoolID: "us-east-1_TestPool",
CognitoRegion: "us-east-1",
CognitoEnvironmentID: "acmehealth", // valid
CSVPath: tmpFile.Name(),
}
err = validateConfig(config)
assert.NoError(t, err, "Should accept valid lowercase environment ID")
}
// TestValidateConfig_EmptyCognitoEnvID verifies that an empty CognitoEnvironmentID
// passes validation (backward compatibility -- not all deployments use env isolation).
func TestValidateConfig_EmptyCognitoEnvID(t *testing.T) {
tmpFile, err := os.CreateTemp(t.TempDir(), "test-*.csv")
require.NoError(t, err)
defer tmpFile.Close()
config := AppConfig{
PermitAPIKey: "permit_key_test",
PermitProjectID: "testproject",
PermitEnvID: "testenv",
CognitoUserPoolID: "us-east-1_TestPool",
CognitoRegion: "us-east-1",
CognitoEnvironmentID: "", // empty is valid
CSVPath: tmpFile.Name(),
}
err = validateConfig(config)
assert.NoError(t, err, "Should accept empty environment ID (backward compat)")
}
@@ -330,7 +330,7 @@ func TestDisableEnableOperations(t *testing.T) {
}
// Create user
_, err = createCognitoUser(ctx, cognitoClient, cognitoConfig, testUser)
_, err = createCognitoUser(ctx, cognitoClient, cognitoConfig, testUser, "")
if err != nil {
// If user already exists, that's fine
if !strings.Contains(err.Error(), "already exists") && !strings.Contains(err.Error(), "UsernameExistsException") {
@@ -411,7 +411,7 @@ func TestIdempotentDisableEnable(t *testing.T) {
Region: os.Getenv("AWS_REGION"),
}
_, err = createCognitoUser(ctx, cognitoClient, cognitoConfig, testUser)
_, err = createCognitoUser(ctx, cognitoClient, cognitoConfig, testUser, "")
if err != nil && !strings.Contains(err.Error(), "already exists") && !strings.Contains(err.Error(), "UsernameExistsException") {
t.Fatalf("Failed to create test user: %v", err)
}
+121
View File
@@ -0,0 +1,121 @@
#!/bin/sh
# list.users.by.env.sh - Lists Cognito users filtered by custom:environment_id attribute.
#
# Usage: ./list.users.by.env.sh <user-pool-id> [environment-id]
#
# Arguments:
# user-pool-id - AWS Cognito User Pool ID (required)
# environment-id - Environment ID to filter by (optional, falls back to COGNITO_ENVIRONMENT_ID env var)
#
# If no environment-id is provided (via argument or env var), lists all users with their
# environment_id attribute value.
#
# Requires: aws cli, jq
#
# Examples:
# ./list.users.by.env.sh us-east-2_abc123 acmehealth
# COGNITO_ENVIRONMENT_ID=acmehealth ./list.users.by.env.sh us-east-2_abc123
# ./list.users.by.env.sh us-east-2_abc123 # lists all users with env info
set -e
USER_POOL_ID="$1"
ENV_ID="${2:-$COGNITO_ENVIRONMENT_ID}"
if [ -z "$USER_POOL_ID" ]; then
echo "Usage: $0 <user-pool-id> [environment-id]" >&2
echo "" >&2
echo "Arguments:" >&2
echo " user-pool-id AWS Cognito User Pool ID (required)" >&2
echo " environment-id Environment ID to filter by (optional)" >&2
echo "" >&2
echo "Falls back to COGNITO_ENVIRONMENT_ID env var if environment-id not provided." >&2
echo "If neither is set, lists all users with their environment_id value." >&2
exit 1
fi
# Check for required tools
if ! command -v aws >/dev/null 2>&1; then
echo "Error: aws cli is required but not installed." >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is required but not installed." >&2
exit 1
fi
# Fetch all users with pagination
ALL_USERS="[]"
PAGINATION_TOKEN=""
while true; do
if [ -z "$PAGINATION_TOKEN" ]; then
RESPONSE=$(aws cognito-idp list-users \
--user-pool-id "$USER_POOL_ID" \
--max-results 60 \
--output json 2>&1)
else
RESPONSE=$(aws cognito-idp list-users \
--user-pool-id "$USER_POOL_ID" \
--max-results 60 \
--pagination-token "$PAGINATION_TOKEN" \
--output json 2>&1)
fi
if [ $? -ne 0 ]; then
echo "Error fetching users: $RESPONSE" >&2
exit 1
fi
PAGE_USERS=$(echo "$RESPONSE" | jq '.Users')
ALL_USERS=$(echo "$ALL_USERS $PAGE_USERS" | jq -s '.[0] + .[1]')
PAGINATION_TOKEN=$(echo "$RESPONSE" | jq -r '.PaginationToken // empty')
if [ -z "$PAGINATION_TOKEN" ]; then
break
fi
done
TOTAL=$(echo "$ALL_USERS" | jq 'length')
if [ -n "$ENV_ID" ]; then
# Filter: show users matching this environment_id OR users with no environment_id (untagged)
FILTERED=$(echo "$ALL_USERS" | jq --arg env "$ENV_ID" '
[.[] | select(
(.Attributes // [] | map(select(.Name == "custom:environment_id")) | length == 0) or
(.Attributes // [] | map(select(.Name == "custom:environment_id" and .Value == $env)) | length > 0)
)]
')
FILTERED_COUNT=$(echo "$FILTERED" | jq 'length')
echo "Users for environment: $ENV_ID"
echo "Showing $FILTERED_COUNT of $TOTAL total users (matching + untagged)"
echo "============================================="
echo ""
echo "$FILTERED" | jq -r '
.[] |
"Email: " + (.Attributes // [] | map(select(.Name == "email")) | .[0].Value // "N/A") +
"\nUsername: " + .Username +
"\nStatus: " + .UserStatus +
"\nEnabled: " + (.Enabled | tostring) +
"\nEnvironment: " + (.Attributes // [] | map(select(.Name == "custom:environment_id")) | .[0].Value // "(untagged)") +
"\n---"
'
else
# No filter: list all users with their environment_id
echo "All users (no environment filter)"
echo "Total: $TOTAL users"
echo "============================================="
echo ""
echo "$ALL_USERS" | jq -r '
.[] |
"Email: " + (.Attributes // [] | map(select(.Name == "email")) | .[0].Value // "N/A") +
"\nUsername: " + .Username +
"\nStatus: " + .UserStatus +
"\nEnabled: " + (.Enabled | tostring) +
"\nEnvironment: " + (.Attributes // [] | map(select(.Name == "custom:environment_id")) | .[0].Value // "(untagged)") +
"\n---"
'
fi
+15 -8
View File
@@ -85,15 +85,22 @@ func getAPIKeyScope(client *http.Client, config AppConfig) (*APIKeyScope, error)
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)
attributes := map[string]interface{}{
"cognito_username": cognitoUser.Username,
"cognito_status": cognitoUser.Status,
}
// Add environment_id to Permit.io attributes when configured
if config.CognitoEnvironmentID != "" {
attributes["environment_id"] = config.CognitoEnvironmentID
}
userObj := PermitUser{
Key: cognitoUser.SubjectID, // Use Cognito Subject ID as the key
Email: cognitoUser.Email,
FirstName: cognitoUser.FirstName,
LastName: cognitoUser.LastName,
Attributes: map[string]interface{}{
"cognito_username": cognitoUser.Username,
"cognito_status": cognitoUser.Status,
},
Key: cognitoUser.SubjectID, // Use Cognito Subject ID as the key
Email: cognitoUser.Email,
FirstName: cognitoUser.FirstName,
LastName: cognitoUser.LastName,
Attributes: attributes,
}
jsonData, err := json.Marshal(userObj)
+64 -2
View File
@@ -120,6 +120,11 @@ PERMIT_KEY=your-permit-api-key-here
# AWS Cognito Configuration
COGNITO_USER_POOL_ID=us-east-1_xxxxxxxxx
# Environment Isolation (optional)
# Tags created users with this environment ID in Cognito and Permit.io.
# Can also be set via the -cognito-env-id flag (flag takes precedence).
COGNITO_ENVIRONMENT_ID=acmehealth
# AWS Configuration
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-access-key # Or use IAM role
@@ -216,6 +221,7 @@ To check what project/environment your API key is scoped to:
- `-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)
- `-cognito-env-id`: Environment ID to tag users with in Cognito (`custom:environment_id` attribute) and Permit.io. Falls back to `COGNITO_ENVIRONMENT_ID` environment variable if not provided. See [Environment Isolation](#environment-isolation) below.
- `--delete-users`: Delete users instead of creating/updating them
- `--disable-users`: Disable users in Cognito instead of creating/updating them
- `--enable-users`: Enable users in Cognito instead of creating/updating them
@@ -223,6 +229,59 @@ To check what project/environment your API key is scoped to:
**Important**: Only one operation mode can be specified at a time (`--delete-users`, `--disable-users`, or `--enable-users`). If none are specified, the tool defaults to create/update mode.
## Environment Isolation
When operating in a multi-environment deployment with a shared Cognito user pool, the tool supports tagging users with an environment identifier. This scopes users to a specific server environment so that each deployment only sees its own users.
For a full description of the multi-environment user isolation feature, see the [cognitoauth readme](../../../internal/cognitoauth/readme.md#multi-environment-user-isolation).
### Setting the Environment ID
The environment ID can be provided in two ways (flag takes precedence):
1. **Command line flag**: `-cognito-env-id acmehealth`
2. **Environment variable**: `COGNITO_ENVIRONMENT_ID=acmehealth`
The value must follow the naming rules: lowercase alphanumeric and underscores, must start with a letter, must end with a letter or digit, no consecutive underscores, max 50 characters. The tool validates the value at startup and exits with an error if invalid.
### Usage Examples
```bash
# Create users tagged with environment ID via flag
./cognito-permit-sync -project myproject -env production -csv users.csv -cognito-env-id acmehealth
# Create users tagged with environment ID via env var
export COGNITO_ENVIRONMENT_ID=acmehealth
./cognito-permit-sync -project myproject -env production -csv users.csv
# Dry run to preview environment tagging
./cognito-permit-sync -project myproject -env production -csv users.csv -cognito-env-id acmehealth --dry-run
```
### What Gets Tagged
When an environment ID is configured:
- **Cognito**: Each created user gets a `custom:environment_id` attribute set to the configured value
- **Permit.io**: Each created user gets an `environment_id` custom attribute set to the configured value
When no environment ID is configured, users are created without any environment tag (backward compatible).
### Listing Users by Environment
The `list.users.by.env.sh` script lists all Cognito users that have a specific `custom:environment_id` attribute value:
```bash
# List users by environment ID passed as argument
./list.users.by.env.sh <user-pool-id> [environment-id]
# Falls back to COGNITO_ENVIRONMENT_ID env var if environment-id argument is omitted
export COGNITO_ENVIRONMENT_ID=acmehealth
./list.users.by.env.sh us-east-2_21upuTkkT
```
This script requires the AWS CLI and `jq` to be installed. It uses `aws cognito-idp list-users` and filters the results by the `custom:environment_id` attribute.
## How It Works
### Create/Update Mode (Default)
@@ -354,11 +413,14 @@ Before running this tool, the following must already exist in your Permit.io env
"last_name": "Doe",
"attributes": {
"cognito_username": "user@example.com",
"cognito_status": "CONFIRMED"
"cognito_status": "CONFIRMED",
"environment_id": "acmehealth"
}
}
```
The `environment_id` attribute is included only when `-cognito-env-id` or `COGNITO_ENVIRONMENT_ID` is configured. When not set, this attribute is omitted.
### What the Tool Deletes from Permit.io
| Resource | API Endpoint | Details |
@@ -383,7 +445,7 @@ Before running this tool, the following must already exist in your Permit.io env
| **User Key** | Uses Cognito `Subject ID` (the `sub` claim from JWT) as the Permit.io user key - **not** the email address |
| **Tenant** | All role assignments use a hardcoded `"default"` tenant. Multi-tenant support is not currently implemented. |
| **Role Sync** | Idempotent - removes roles no longer in CSV, adds missing ones, skips unchanged roles |
| **User Attributes** | Stores `cognito_username` and `cognito_status` as custom attributes on the Permit.io user |
| **User Attributes** | Stores `cognito_username`, `cognito_status`, and optionally `environment_id` as custom attributes on the Permit.io user |
| **Role Validation** | Validates all roles exist **before** processing any users. If any role is missing, the tool exits with an error listing the missing roles. |
### Creating Required Roles in Permit.io
@@ -1,5 +1,6 @@
# permit stuff
export PERMIT_KEY="permit_key_redacted"
# development environment only
# Cognito stuff
export COGNITO_USER_POOL_ID="us-east-2_21upuTkkT"
@@ -12,5 +13,8 @@ export AWS_REGION=us-east-2
export AWS_PROFILE=aarete
# must do a project list to get the guid values for the project and environment before making the call to import.
#go run ./... -project list
go run ./... -project ebde1e1e9623491cab6f8112e67bd61c -env 9d6801123cfd4a0ea2ef1df2f430e9d3 -csv ./users.to.import.csv --dry-run
#go run ./... -project ebde1e1e9623491cab6f8112e67bd61c -env 9d6801123cfd4a0ea2ef1df2f430e9d3 -csv ./users.to.import.csv --dry-run
#go run ./... -project ebde1e1e9623491cab6f8112e67bd61c -env 9d6801123cfd4a0ea2ef1df2f430e9d3 -csv ./users.to.import.csv
AWS_ENDPOINT_URL="" go run ./... -project use-key -csv users.to.import.csv -cognito-env-id dev
# --dry-run
@@ -349,6 +349,83 @@ func getCognitoUserSubject(ctx context.Context, client *cognitoidentityprovider.
return "", fmt.Errorf("subject ID not found for user %s", username)
}
// TestUserLifecycleWithEnvironmentID verifies that users created with COGNITO_ENVIRONMENT_ID
// have the custom:environment_id attribute set in Cognito. Creates users with the env ID,
// verifies the attribute, then cleans up.
func TestUserLifecycleWithEnvironmentID(t *testing.T) {
if !checkRequiredEnvVars(t) {
return
}
const testCognitoEnvID = "integration-test-env"
envCSVPath := "./temp-envid-users.csv"
// Create a minimal CSV with one test user
envCSVContent := "email,first_name,last_name,role1\n"
envCSVContent += "envid-test-lifecycle@example.com,EnvTest,Lifecycle,viewer\n"
if err := os.WriteFile(envCSVPath, []byte(envCSVContent), 0600); err != nil {
t.Fatalf("Failed to create env test CSV: %v", err)
}
defer func() { _ = os.Remove(envCSVPath) }()
// Set COGNITO_ENVIRONMENT_ID for the tool to pick up
t.Setenv("COGNITO_ENVIRONMENT_ID", testCognitoEnvID)
t.Run("CreateUsersWithEnvID", func(t *testing.T) {
originalArgs := os.Args
defer func() { os.Args = originalArgs }()
os.Args = []string{
"cognito-permit-sync",
"-project", testProjectID,
"-env", testEnvID,
"-csv", envCSVPath,
}
resetFlags()
if err := run(); err != nil {
t.Fatalf("Failed to create users with environment ID: %v", err)
}
})
t.Run("VerifyEnvironmentIDAttribute", func(t *testing.T) {
ctx := t.Context()
cognitoClient, err := initializeCognitoClient()
if err != nil {
t.Fatalf("Failed to initialize Cognito client: %v", err)
}
userPoolID := os.Getenv("COGNITO_USER_POOL_ID")
user, err := getCognitoUser(ctx, cognitoClient, userPoolID, "envid-test-lifecycle@example.com")
if err != nil {
t.Fatalf("Failed to get user from Cognito: %v", err)
}
if user.EnvironmentID != testCognitoEnvID {
t.Fatalf("Expected environment_id=%q, got %q", testCognitoEnvID, user.EnvironmentID)
}
t.Logf("Verified user has environment_id=%q", user.EnvironmentID)
})
t.Run("CleanupEnvIDTestUsers", func(t *testing.T) {
originalArgs := os.Args
defer func() { os.Args = originalArgs }()
os.Args = []string{
"cognito-permit-sync",
"-project", testProjectID,
"-env", testEnvID,
"-csv", envCSVPath,
"--delete-users",
}
resetFlags()
if err := run(); err != nil {
t.Fatalf("Failed to delete env test users: %v", err)
}
})
}
// Helper function to reset flags for testing
func resetFlags() {
// Reset the default command line flag set
+23 -1
View File
@@ -31,9 +31,13 @@ import (
"queryorchestration/internal/label"
"queryorchestration/internal/server/api"
"queryorchestration/internal/serviceconfig"
awsc "queryorchestration/internal/serviceconfig/aws"
"queryorchestration/internal/serviceconfig/build"
"queryorchestration/internal/serviceconfig/objectstore"
"queryorchestration/internal/serviceconfig/queue/clientsync"
"queryorchestration/internal/usermanagement"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
queryapi "queryorchestration/api/queryAPI"
documentbatch "queryorchestration/internal/document/batch"
@@ -134,8 +138,26 @@ func main() {
os.Exit(1)
}
// Create Cognito SDK client for environment attribute registration and middleware use.
// This client is created once and reused for the lifetime of the process.
awsCfg, err := awsc.GetAWSConfig(ctx)
if err != nil {
slog.Error("Failed to load AWS config for Cognito client", "error", err)
os.Exit(1)
}
cognitoClient := cognitoidentityprovider.NewFromConfig(awsCfg)
// Ensure the custom:environment_id attribute is registered on the Cognito user pool.
// This is safe to call on every startup (idempotent). Failure is non-fatal because
// the attribute may already exist or the environment may not support this API (e.g. LocalStack).
if err := usermanagement.EnsureEnvironmentIDAttribute(ctx, cognitoClient, cfg.GetAuthUserPoolID(), cfg.GetLogger()); err != nil {
cfg.GetLogger().Warn("Failed to ensure Cognito environment_id attribute", "error", err)
} else {
cfg.GetLogger().Info("Cognito custom attribute custom:environment_id is registered")
}
// Initialize S3 client before creating the server (needed for background worker)
err := cfg.SetStoreClient(ctx)
err = cfg.SetStoreClient(ctx)
if err != nil {
slog.Error(err.Error())
os.Exit(1)