diff --git a/test/s3/iam/iam_config.github.json b/test/s3/iam/iam_config.github.json new file mode 100644 index 000000000..d931f8389 --- /dev/null +++ b/test/s3/iam/iam_config.github.json @@ -0,0 +1,294 @@ +{ + "sts": { + "tokenDuration": 3600000000000, + "maxSessionLength": 43200000000000, + "issuer": "seaweedfs-sts", + "signingKey": "dGVzdC1zaWduaW5nLWtleS0zMi1jaGFyYWN0ZXJzLWxvbmc=" + }, + "providers": [ + { + "name": "test-oidc", + "type": "mock", + "config": { + "issuer": "test-oidc-issuer", + "clientId": "test-oidc-client" + } + }, + { + "name": "keycloak", + "type": "oidc", + "enabled": true, + "config": { + "issuer": "http://localhost:8080/realms/seaweedfs-test", + "clientId": "seaweedfs-s3", + "clientSecret": "seaweedfs-s3-secret", + "jwksUri": "http://localhost:8080/realms/seaweedfs-test/protocol/openid-connect/certs", + "userInfoUri": "http://localhost:8080/realms/seaweedfs-test/protocol/openid-connect/userinfo", + "scopes": ["openid", "profile", "email"], + "claimsMapping": { + "username": "preferred_username", + "email": "email", + "name": "name" + }, + "roleMapping": { + "rules": [ + { + "claim": "roles", + "value": "s3-admin", + "role": "arn:seaweed:iam::role/KeycloakAdminRole" + }, + { + "claim": "roles", + "value": "s3-read-only", + "role": "arn:seaweed:iam::role/KeycloakReadOnlyRole" + }, + { + "claim": "roles", + "value": "s3-write-only", + "role": "arn:seaweed:iam::role/KeycloakWriteOnlyRole" + }, + { + "claim": "roles", + "value": "s3-read-write", + "role": "arn:seaweed:iam::role/KeycloakReadWriteRole" + } + ], + "defaultRole": "arn:seaweed:iam::role/KeycloakReadOnlyRole" + } + } + } + ], + "policy": { + "defaultEffect": "Deny", + "storeType": "memory" + }, + "roles": [ + { + "roleName": "TestAdminRole", + "roleArn": "arn:seaweed:iam::role/TestAdminRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "test-oidc" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3AdminPolicy"], + "description": "Admin role for testing" + }, + { + "roleName": "TestReadOnlyRole", + "roleArn": "arn:seaweed:iam::role/TestReadOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "test-oidc" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3ReadOnlyPolicy"], + "description": "Read-only role for testing" + }, + { + "roleName": "TestWriteOnlyRole", + "roleArn": "arn:seaweed:iam::role/TestWriteOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "test-oidc" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3WriteOnlyPolicy"], + "description": "Write-only role for testing" + }, + { + "roleName": "KeycloakAdminRole", + "roleArn": "arn:seaweed:iam::role/KeycloakAdminRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3AdminPolicy"], + "description": "Admin role for Keycloak users" + }, + { + "roleName": "KeycloakReadOnlyRole", + "roleArn": "arn:seaweed:iam::role/KeycloakReadOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3ReadOnlyPolicy"], + "description": "Read-only role for Keycloak users" + }, + { + "roleName": "KeycloakWriteOnlyRole", + "roleArn": "arn:seaweed:iam::role/KeycloakWriteOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3WriteOnlyPolicy"], + "description": "Write-only role for Keycloak users" + }, + { + "roleName": "KeycloakReadWriteRole", + "roleArn": "arn:seaweed:iam::role/KeycloakReadWriteRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3ReadWritePolicy"], + "description": "Read-write role for Keycloak users" + } + ], + "policies": [ + { + "name": "S3AdminPolicy", + "document": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["s3:*"], + "Resource": ["*"] + }, + { + "Effect": "Allow", + "Action": ["sts:ValidateSession"], + "Resource": ["*"] + } + ] + } + }, + { + "name": "S3ReadOnlyPolicy", + "document": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:ListBucket" + ], + "Resource": [ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*" + ] + }, + { + "Effect": "Allow", + "Action": ["sts:ValidateSession"], + "Resource": ["*"] + } + ] + } + }, + { + "name": "S3WriteOnlyPolicy", + "document": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:*" + ], + "Resource": [ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*" + ] + }, + { + "Effect": "Deny", + "Action": [ + "s3:GetObject", + "s3:ListBucket" + ], + "Resource": [ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*" + ] + }, + { + "Effect": "Allow", + "Action": ["sts:ValidateSession"], + "Resource": ["*"] + } + ] + } + }, + { + "name": "S3ReadWritePolicy", + "document": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:*" + ], + "Resource": [ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*" + ] + }, + { + "Effect": "Allow", + "Action": ["sts:ValidateSession"], + "Resource": ["*"] + } + ] + } + } + ] +} diff --git a/test/s3/iam/iam_config.json b/test/s3/iam/iam_config.json new file mode 100644 index 000000000..d931f8389 --- /dev/null +++ b/test/s3/iam/iam_config.json @@ -0,0 +1,294 @@ +{ + "sts": { + "tokenDuration": 3600000000000, + "maxSessionLength": 43200000000000, + "issuer": "seaweedfs-sts", + "signingKey": "dGVzdC1zaWduaW5nLWtleS0zMi1jaGFyYWN0ZXJzLWxvbmc=" + }, + "providers": [ + { + "name": "test-oidc", + "type": "mock", + "config": { + "issuer": "test-oidc-issuer", + "clientId": "test-oidc-client" + } + }, + { + "name": "keycloak", + "type": "oidc", + "enabled": true, + "config": { + "issuer": "http://localhost:8080/realms/seaweedfs-test", + "clientId": "seaweedfs-s3", + "clientSecret": "seaweedfs-s3-secret", + "jwksUri": "http://localhost:8080/realms/seaweedfs-test/protocol/openid-connect/certs", + "userInfoUri": "http://localhost:8080/realms/seaweedfs-test/protocol/openid-connect/userinfo", + "scopes": ["openid", "profile", "email"], + "claimsMapping": { + "username": "preferred_username", + "email": "email", + "name": "name" + }, + "roleMapping": { + "rules": [ + { + "claim": "roles", + "value": "s3-admin", + "role": "arn:seaweed:iam::role/KeycloakAdminRole" + }, + { + "claim": "roles", + "value": "s3-read-only", + "role": "arn:seaweed:iam::role/KeycloakReadOnlyRole" + }, + { + "claim": "roles", + "value": "s3-write-only", + "role": "arn:seaweed:iam::role/KeycloakWriteOnlyRole" + }, + { + "claim": "roles", + "value": "s3-read-write", + "role": "arn:seaweed:iam::role/KeycloakReadWriteRole" + } + ], + "defaultRole": "arn:seaweed:iam::role/KeycloakReadOnlyRole" + } + } + } + ], + "policy": { + "defaultEffect": "Deny", + "storeType": "memory" + }, + "roles": [ + { + "roleName": "TestAdminRole", + "roleArn": "arn:seaweed:iam::role/TestAdminRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "test-oidc" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3AdminPolicy"], + "description": "Admin role for testing" + }, + { + "roleName": "TestReadOnlyRole", + "roleArn": "arn:seaweed:iam::role/TestReadOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "test-oidc" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3ReadOnlyPolicy"], + "description": "Read-only role for testing" + }, + { + "roleName": "TestWriteOnlyRole", + "roleArn": "arn:seaweed:iam::role/TestWriteOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "test-oidc" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3WriteOnlyPolicy"], + "description": "Write-only role for testing" + }, + { + "roleName": "KeycloakAdminRole", + "roleArn": "arn:seaweed:iam::role/KeycloakAdminRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3AdminPolicy"], + "description": "Admin role for Keycloak users" + }, + { + "roleName": "KeycloakReadOnlyRole", + "roleArn": "arn:seaweed:iam::role/KeycloakReadOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3ReadOnlyPolicy"], + "description": "Read-only role for Keycloak users" + }, + { + "roleName": "KeycloakWriteOnlyRole", + "roleArn": "arn:seaweed:iam::role/KeycloakWriteOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3WriteOnlyPolicy"], + "description": "Write-only role for Keycloak users" + }, + { + "roleName": "KeycloakReadWriteRole", + "roleArn": "arn:seaweed:iam::role/KeycloakReadWriteRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3ReadWritePolicy"], + "description": "Read-write role for Keycloak users" + } + ], + "policies": [ + { + "name": "S3AdminPolicy", + "document": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["s3:*"], + "Resource": ["*"] + }, + { + "Effect": "Allow", + "Action": ["sts:ValidateSession"], + "Resource": ["*"] + } + ] + } + }, + { + "name": "S3ReadOnlyPolicy", + "document": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:ListBucket" + ], + "Resource": [ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*" + ] + }, + { + "Effect": "Allow", + "Action": ["sts:ValidateSession"], + "Resource": ["*"] + } + ] + } + }, + { + "name": "S3WriteOnlyPolicy", + "document": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:*" + ], + "Resource": [ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*" + ] + }, + { + "Effect": "Deny", + "Action": [ + "s3:GetObject", + "s3:ListBucket" + ], + "Resource": [ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*" + ] + }, + { + "Effect": "Allow", + "Action": ["sts:ValidateSession"], + "Resource": ["*"] + } + ] + } + }, + { + "name": "S3ReadWritePolicy", + "document": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:*" + ], + "Resource": [ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*" + ] + }, + { + "Effect": "Allow", + "Action": ["sts:ValidateSession"], + "Resource": ["*"] + } + ] + } + } + ] +} diff --git a/test/s3/iam/iam_config.local.json b/test/s3/iam/iam_config.local.json new file mode 100644 index 000000000..e87bbca96 --- /dev/null +++ b/test/s3/iam/iam_config.local.json @@ -0,0 +1,346 @@ +{ + "sts": { + "tokenDuration": 3600000000000, + "maxSessionLength": 43200000000000, + "issuer": "seaweedfs-sts", + "signingKey": "dGVzdC1zaWduaW5nLWtleS0zMi1jaGFyYWN0ZXJzLWxvbmc=" + }, + "providers": [ + { + "name": "test-oidc", + "type": "mock", + "config": { + "issuer": "test-oidc-issuer", + "clientId": "test-oidc-client" + } + }, + { + "name": "keycloak", + "type": "oidc", + "enabled": true, + "config": { + "issuer": "http://localhost:8090/realms/seaweedfs-test", + "clientId": "seaweedfs-s3", + "clientSecret": "seaweedfs-s3-secret", + "jwksUri": "http://localhost:8090/realms/seaweedfs-test/protocol/openid-connect/certs", + "userInfoUri": "http://localhost:8090/realms/seaweedfs-test/protocol/openid-connect/userinfo", + "scopes": [ + "openid", + "profile", + "email" + ], + "claimsMapping": { + "username": "preferred_username", + "email": "email", + "name": "name" + }, + "roleMapping": { + "rules": [ + { + "claim": "roles", + "value": "s3-admin", + "role": "arn:seaweed:iam::role/KeycloakAdminRole" + }, + { + "claim": "roles", + "value": "s3-read-only", + "role": "arn:seaweed:iam::role/KeycloakReadOnlyRole" + }, + { + "claim": "roles", + "value": "s3-write-only", + "role": "arn:seaweed:iam::role/KeycloakWriteOnlyRole" + }, + { + "claim": "roles", + "value": "s3-read-write", + "role": "arn:seaweed:iam::role/KeycloakReadWriteRole" + } + ], + "defaultRole": "arn:seaweed:iam::role/KeycloakReadOnlyRole" + } + } + } + ], + "policy": { + "defaultEffect": "Deny", + "storeType": "memory" + }, + "roles": [ + { + "roleName": "TestAdminRole", + "roleArn": "arn:seaweed:iam::role/TestAdminRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "test-oidc" + }, + "Action": [ + "sts:AssumeRoleWithWebIdentity" + ] + } + ] + }, + "attachedPolicies": [ + "S3AdminPolicy" + ], + "description": "Admin role for testing" + }, + { + "roleName": "TestReadOnlyRole", + "roleArn": "arn:seaweed:iam::role/TestReadOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "test-oidc" + }, + "Action": [ + "sts:AssumeRoleWithWebIdentity" + ] + } + ] + }, + "attachedPolicies": [ + "S3ReadOnlyPolicy" + ], + "description": "Read-only role for testing" + }, + { + "roleName": "TestWriteOnlyRole", + "roleArn": "arn:seaweed:iam::role/TestWriteOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "test-oidc" + }, + "Action": [ + "sts:AssumeRoleWithWebIdentity" + ] + } + ] + }, + "attachedPolicies": [ + "S3WriteOnlyPolicy" + ], + "description": "Write-only role for testing" + }, + { + "roleName": "KeycloakAdminRole", + "roleArn": "arn:seaweed:iam::role/KeycloakAdminRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": [ + "sts:AssumeRoleWithWebIdentity" + ] + } + ] + }, + "attachedPolicies": [ + "S3AdminPolicy" + ], + "description": "Admin role for Keycloak users" + }, + { + "roleName": "KeycloakReadOnlyRole", + "roleArn": "arn:seaweed:iam::role/KeycloakReadOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": [ + "sts:AssumeRoleWithWebIdentity" + ] + } + ] + }, + "attachedPolicies": [ + "S3ReadOnlyPolicy" + ], + "description": "Read-only role for Keycloak users" + }, + { + "roleName": "KeycloakWriteOnlyRole", + "roleArn": "arn:seaweed:iam::role/KeycloakWriteOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": [ + "sts:AssumeRoleWithWebIdentity" + ] + } + ] + }, + "attachedPolicies": [ + "S3WriteOnlyPolicy" + ], + "description": "Write-only role for Keycloak users" + }, + { + "roleName": "KeycloakReadWriteRole", + "roleArn": "arn:seaweed:iam::role/KeycloakReadWriteRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": [ + "sts:AssumeRoleWithWebIdentity" + ] + } + ] + }, + "attachedPolicies": [ + "S3ReadWritePolicy" + ], + "description": "Read-write role for Keycloak users" + } + ], + "policies": [ + { + "name": "S3AdminPolicy", + "document": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:*" + ], + "Resource": [ + "*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "sts:ValidateSession" + ], + "Resource": [ + "*" + ] + } + ] + } + }, + { + "name": "S3ReadOnlyPolicy", + "document": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:ListBucket" + ], + "Resource": [ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "sts:ValidateSession" + ], + "Resource": [ + "*" + ] + } + ] + } + }, + { + "name": "S3WriteOnlyPolicy", + "document": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:*" + ], + "Resource": [ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*" + ] + }, + { + "Effect": "Deny", + "Action": [ + "s3:GetObject", + "s3:ListBucket" + ], + "Resource": [ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "sts:ValidateSession" + ], + "Resource": [ + "*" + ] + } + ] + } + }, + { + "name": "S3ReadWritePolicy", + "document": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:*" + ], + "Resource": [ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "sts:ValidateSession" + ], + "Resource": [ + "*" + ] + } + ] + } + } + ] +} diff --git a/test/s3/iam/setup_keycloak.sh b/test/s3/iam/setup_keycloak.sh new file mode 100644 index 000000000..0f59e165b --- /dev/null +++ b/test/s3/iam/setup_keycloak.sh @@ -0,0 +1,377 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +KEYCLOAK_IMAGE="quay.io/keycloak/keycloak:26.0.7" +CONTAINER_NAME="keycloak-iam-test" +KEYCLOAK_PORT="8080" # Default external port +KEYCLOAK_INTERNAL_PORT="8080" # Internal container port (always 8080) +KEYCLOAK_URL="http://localhost:${KEYCLOAK_PORT}" + +# Realm and test fixtures expected by tests +REALM_NAME="seaweedfs-test" +CLIENT_ID="seaweedfs-s3" +CLIENT_SECRET="seaweedfs-s3-secret" +ROLE_ADMIN="s3-admin" +ROLE_READONLY="s3-read-only" +ROLE_WRITEONLY="s3-write-only" +ROLE_READWRITE="s3-read-write" + +# User credentials (compatible with older bash versions) +get_user_password() { + case "$1" in + "admin-user") echo "admin123" ;; + "read-user") echo "read123" ;; + "write-user") echo "readwrite123" ;; + "write-only-user") echo "writeonly123" ;; + *) echo "" ;; + esac +} + +# List of users to create +USERS="admin-user read-user write-user write-only-user" + +echo -e "${BLUE}🔧 Setting up Keycloak realm and users for SeaweedFS S3 IAM testing...${NC}" + +ensure_container() { + # Check for any existing Keycloak container and detect its port + local keycloak_containers=$(docker ps --format '{{.Names}}\t{{.Ports}}' | grep -E "(keycloak|quay.io/keycloak)") + + if [[ -n "$keycloak_containers" ]]; then + # Parse the first available Keycloak container + CONTAINER_NAME=$(echo "$keycloak_containers" | head -1 | awk '{print $1}') + + # Extract the external port from the port mapping using sed (compatible with older bash) + local port_mapping=$(echo "$keycloak_containers" | head -1 | awk '{print $2}') + local extracted_port=$(echo "$port_mapping" | sed -n 's/.*:\([0-9]*\)->8080.*/\1/p') + if [[ -n "$extracted_port" ]]; then + KEYCLOAK_PORT="$extracted_port" + KEYCLOAK_URL="http://localhost:${KEYCLOAK_PORT}" + echo -e "${GREEN}✅ Using existing container '${CONTAINER_NAME}' on port ${KEYCLOAK_PORT}${NC}" + return 0 + fi + fi + + # Fallback: check for specific container names + if docker ps --format '{{.Names}}' | grep -q '^keycloak$'; then + CONTAINER_NAME="keycloak" + # Try to detect port for 'keycloak' container using docker port command + local ports=$(docker port keycloak 8080 2>/dev/null | head -1) + if [[ -n "$ports" ]]; then + local extracted_port=$(echo "$ports" | sed -n 's/.*:\([0-9]*\)$/\1/p') + if [[ -n "$extracted_port" ]]; then + KEYCLOAK_PORT="$extracted_port" + KEYCLOAK_URL="http://localhost:${KEYCLOAK_PORT}" + fi + fi + echo -e "${GREEN}✅ Using existing container '${CONTAINER_NAME}' on port ${KEYCLOAK_PORT}${NC}" + return 0 + fi + if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo -e "${GREEN}✅ Using existing container '${CONTAINER_NAME}'${NC}" + return 0 + fi + echo -e "${YELLOW}🐳 Starting Keycloak container (${KEYCLOAK_IMAGE})...${NC}" + docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true + docker run -d --name "${CONTAINER_NAME}" -p "${KEYCLOAK_PORT}:8080" \ + -e KEYCLOAK_ADMIN=admin \ + -e KEYCLOAK_ADMIN_PASSWORD=admin \ + -e KC_HTTP_ENABLED=true \ + -e KC_HOSTNAME_STRICT=false \ + -e KC_HOSTNAME_STRICT_HTTPS=false \ + -e KC_HEALTH_ENABLED=true \ + "${KEYCLOAK_IMAGE}" start-dev >/dev/null +} + +wait_ready() { + echo -e "${YELLOW}⏳ Waiting for Keycloak to be ready...${NC}" + for i in $(seq 1 120); do + if curl -sf "${KEYCLOAK_URL}/health/ready" >/dev/null; then + echo -e "${GREEN}✅ Keycloak health check passed${NC}" + return 0 + fi + if curl -sf "${KEYCLOAK_URL}/realms/master" >/dev/null; then + echo -e "${GREEN}✅ Keycloak master realm accessible${NC}" + return 0 + fi + sleep 2 + done + echo -e "${RED}❌ Keycloak did not become ready in time${NC}" + exit 1 +} + +kcadm() { + # Always authenticate before each command to ensure context + # Try different admin passwords that might be used in different environments + # GitHub Actions uses "admin", local testing might use "admin123" + local admin_passwords=("admin" "admin123" "password") + local auth_success=false + + for pwd in "${admin_passwords[@]}"; do + if docker exec -i "${CONTAINER_NAME}" /opt/keycloak/bin/kcadm.sh config credentials --server "http://localhost:${KEYCLOAK_INTERNAL_PORT}" --realm master --user admin --password "$pwd" >/dev/null 2>&1; then + auth_success=true + break + fi + done + + if [[ "$auth_success" == false ]]; then + echo -e "${RED}❌ Failed to authenticate with any known admin password${NC}" + return 1 + fi + + docker exec -i "${CONTAINER_NAME}" /opt/keycloak/bin/kcadm.sh "$@" +} + +admin_login() { + # This is now handled by each kcadm() call + echo "Logging into http://localhost:${KEYCLOAK_INTERNAL_PORT} as user admin of realm master" +} + +ensure_realm() { + if kcadm get realms | grep -q "${REALM_NAME}"; then + echo -e "${GREEN}✅ Realm '${REALM_NAME}' already exists${NC}" + else + echo -e "${YELLOW}📝 Creating realm '${REALM_NAME}'...${NC}" + if kcadm create realms -s realm="${REALM_NAME}" -s enabled=true 2>/dev/null; then + echo -e "${GREEN}✅ Realm created${NC}" + else + # Check if it exists now (might have been created by another process) + if kcadm get realms | grep -q "${REALM_NAME}"; then + echo -e "${GREEN}✅ Realm '${REALM_NAME}' already exists (created concurrently)${NC}" + else + echo -e "${RED}❌ Failed to create realm '${REALM_NAME}'${NC}" + return 1 + fi + fi + fi +} + +ensure_client() { + local id + id=$(kcadm get clients -r "${REALM_NAME}" -q clientId="${CLIENT_ID}" | jq -r '.[0].id // empty') + if [[ -n "${id}" ]]; then + echo -e "${GREEN}✅ Client '${CLIENT_ID}' already exists${NC}" + else + echo -e "${YELLOW}📝 Creating client '${CLIENT_ID}'...${NC}" + kcadm create clients -r "${REALM_NAME}" \ + -s clientId="${CLIENT_ID}" \ + -s protocol=openid-connect \ + -s publicClient=false \ + -s serviceAccountsEnabled=true \ + -s directAccessGrantsEnabled=true \ + -s standardFlowEnabled=true \ + -s implicitFlowEnabled=false \ + -s secret="${CLIENT_SECRET}" >/dev/null + echo -e "${GREEN}✅ Client created${NC}" + fi + + # Create and configure role mapper for the client + configure_role_mapper "${CLIENT_ID}" +} + +ensure_role() { + local role="$1" + if kcadm get roles -r "${REALM_NAME}" | jq -r '.[].name' | grep -qx "${role}"; then + echo -e "${GREEN}✅ Role '${role}' exists${NC}" + else + echo -e "${YELLOW}📝 Creating role '${role}'...${NC}" + kcadm create roles -r "${REALM_NAME}" -s name="${role}" >/dev/null + fi +} + +ensure_user() { + local username="$1" password="$2" + local uid + uid=$(kcadm get users -r "${REALM_NAME}" -q username="${username}" | jq -r '.[0].id // empty') + if [[ -z "${uid}" ]]; then + echo -e "${YELLOW}📝 Creating user '${username}'...${NC}" + uid=$(kcadm create users -r "${REALM_NAME}" \ + -s username="${username}" \ + -s enabled=true \ + -s email="${username}@seaweedfs.test" \ + -s emailVerified=true \ + -s firstName="${username}" \ + -s lastName="User" \ + -i) + else + echo -e "${GREEN}✅ User '${username}' exists${NC}" + fi + echo -e "${YELLOW}🔑 Setting password for '${username}'...${NC}" + kcadm set-password -r "${REALM_NAME}" --userid "${uid}" --new-password "${password}" --temporary=false >/dev/null +} + +assign_role() { + local username="$1" role="$2" + local uid rid + uid=$(kcadm get users -r "${REALM_NAME}" -q username="${username}" | jq -r '.[0].id') + rid=$(kcadm get roles -r "${REALM_NAME}" | jq -r ".[] | select(.name==\"${role}\") | .id") + # Check if role already assigned + if kcadm get "users/${uid}/role-mappings/realm" -r "${REALM_NAME}" | jq -r '.[].name' | grep -qx "${role}"; then + echo -e "${GREEN}✅ User '${username}' already has role '${role}'${NC}" + return 0 + fi + echo -e "${YELLOW}➕ Assigning role '${role}' to '${username}'...${NC}" + kcadm add-roles -r "${REALM_NAME}" --uid "${uid}" --rolename "${role}" >/dev/null +} + +configure_role_mapper() { + echo -e "${YELLOW}🔧 Configuring role mapper for client '${CLIENT_ID}'...${NC}" + + # Get client's internal ID + local internal_id + internal_id=$(kcadm get clients -r "${REALM_NAME}" -q clientId="${CLIENT_ID}" | jq -r '.[0].id // empty') + + if [[ -z "${internal_id}" ]]; then + echo -e "${RED}❌ Could not find client ${client_id} to configure role mapper${NC}" + return 1 + fi + + # Check if a realm roles mapper already exists for this client + local existing_mapper + existing_mapper=$(kcadm get "clients/${internal_id}/protocol-mappers/models" -r "${REALM_NAME}" | jq -r '.[] | select(.name=="realm roles" and .protocolMapper=="oidc-usermodel-realm-role-mapper") | .id // empty') + + if [[ -n "${existing_mapper}" ]]; then + echo -e "${GREEN}✅ Realm roles mapper already exists${NC}" + else + echo -e "${YELLOW}📝 Creating realm roles mapper...${NC}" + + # Create protocol mapper for realm roles + kcadm create "clients/${internal_id}/protocol-mappers/models" -r "${REALM_NAME}" \ + -s name="realm roles" \ + -s protocol="openid-connect" \ + -s protocolMapper="oidc-usermodel-realm-role-mapper" \ + -s consentRequired=false \ + -s 'config."multivalued"=true' \ + -s 'config."userinfo.token.claim"=true' \ + -s 'config."id.token.claim"=true' \ + -s 'config."access.token.claim"=true' \ + -s 'config."claim.name"=roles' \ + -s 'config."jsonType.label"=String' >/dev/null || { + echo -e "${RED}❌ Failed to create realm roles mapper${NC}" + return 1 + } + + echo -e "${GREEN}✅ Realm roles mapper created${NC}" + fi +} + +main() { + command -v docker >/dev/null || { echo -e "${RED}❌ Docker is required${NC}"; exit 1; } + command -v jq >/dev/null || { echo -e "${RED}❌ jq is required${NC}"; exit 1; } + + ensure_container + echo "Keycloak URL: ${KEYCLOAK_URL}" + wait_ready + admin_login + ensure_realm + ensure_client + configure_role_mapper + ensure_role "${ROLE_ADMIN}" + ensure_role "${ROLE_READONLY}" + ensure_role "${ROLE_WRITEONLY}" + ensure_role "${ROLE_READWRITE}" + + for u in $USERS; do + ensure_user "$u" "$(get_user_password "$u")" + done + + assign_role admin-user "${ROLE_ADMIN}" + assign_role read-user "${ROLE_READONLY}" + assign_role write-user "${ROLE_READWRITE}" + + # Also create a dedicated write-only user for testing + ensure_user write-only-user "$(get_user_password write-only-user)" + assign_role write-only-user "${ROLE_WRITEONLY}" + + # Copy the appropriate IAM configuration for this environment + setup_iam_config + + # Validate the setup by testing authentication and role inclusion + echo -e "${YELLOW}🔍 Validating setup by testing admin-user authentication and role mapping...${NC}" + sleep 2 + + local validation_result=$(curl -s -w "%{http_code}" -X POST "http://localhost:${KEYCLOAK_PORT}/realms/${REALM_NAME}/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password" \ + -d "client_id=${CLIENT_ID}" \ + -d "client_secret=${CLIENT_SECRET}" \ + -d "username=admin-user" \ + -d "password=admin123" \ + -d "scope=openid profile email" \ + -o /tmp/auth_test_response.json) + + if [[ "${validation_result: -3}" == "200" ]]; then + echo -e "${GREEN}✅ Authentication validation successful${NC}" + + # Extract and decode JWT token to check for roles + local access_token=$(cat /tmp/auth_test_response.json | jq -r '.access_token // empty') + if [[ -n "${access_token}" ]]; then + # Decode JWT payload (second part) and check for roles + local payload=$(echo "${access_token}" | cut -d'.' -f2) + # Add padding if needed for base64 decode + while [[ $((${#payload} % 4)) -ne 0 ]]; do + payload="${payload}=" + done + + local decoded=$(echo "${payload}" | base64 -d 2>/dev/null || echo "{}") + local roles=$(echo "${decoded}" | jq -r '.roles // empty' 2>/dev/null || echo "") + + if [[ -n "${roles}" && "${roles}" != "null" ]]; then + echo -e "${GREEN}✅ JWT token includes roles: ${roles}${NC}" + else + echo -e "${YELLOW}⚠️ JWT token does not include 'roles' claim${NC}" + echo -e "${YELLOW}Decoded payload sample:${NC}" + echo "${decoded}" | jq '.' 2>/dev/null || echo "${decoded}" + fi + fi + else + echo -e "${RED}❌ Authentication validation failed with HTTP ${validation_result: -3}${NC}" + echo -e "${YELLOW}Response body:${NC}" + cat /tmp/auth_test_response.json 2>/dev/null || echo "No response body" + echo -e "${YELLOW}This may indicate a setup issue that needs to be resolved${NC}" + fi + rm -f /tmp/auth_test_response.json + + echo -e "${GREEN}✅ Keycloak test realm '${REALM_NAME}' configured${NC}" +} + +setup_iam_config() { + echo -e "${BLUE}🔧 Setting up IAM configuration for detected environment${NC}" + + # Change to script directory to ensure config files are found + local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + cd "$script_dir" + + # Choose the appropriate config based on detected port + local config_source + if [[ "${KEYCLOAK_PORT}" == "8080" ]]; then + config_source="iam_config.github.json" + echo " Using GitHub Actions configuration (port 8080)" + else + config_source="iam_config.local.json" + echo " Using local development configuration (port ${KEYCLOAK_PORT})" + fi + + # Verify source config exists + if [[ ! -f "$config_source" ]]; then + echo -e "${RED}❌ Config file $config_source not found in $script_dir${NC}" + exit 1 + fi + + # Copy the appropriate config + cp "$config_source" "iam_config.json" + + local detected_issuer=$(cat iam_config.json | jq -r '.providers[] | select(.name=="keycloak") | .config.issuer') + echo -e "${GREEN}✅ IAM configuration set successfully${NC}" + echo " - Using config: $config_source" + echo " - Keycloak issuer: $detected_issuer" +} + +main "$@"