aboutsummaryrefslogtreecommitdiff
path: root/test/s3/iam/setup_keycloak.sh
blob: 14fb08435eba68ea19fec7343cb44b89c3bcd9a3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
#!/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 (matches Docker setup script logic: removes non-alphabetic chars + "123")
get_user_password() {
  case "$1" in
    "admin-user") echo "adminuser123" ;;        # "admin-user" -> "adminuser123"
    "read-user") echo "readuser123" ;;          # "read-user" -> "readuser123"
    "write-user") echo "writeuser123" ;;        # "write-user" -> "writeuser123"
    "write-only-user") echo "writeonlyuser123" ;;  # "write-only-user" -> "writeonlyuser123"
    *) 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}[OK] 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}[OK] 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}[OK] 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}[OK] Keycloak health check passed${NC}"
      return 0
    fi
    if curl -sf "${KEYCLOAK_URL}/realms/master" >/dev/null; then
      echo -e "${GREEN}[OK] Keycloak master realm accessible${NC}"
      return 0
    fi
    sleep 2
  done
  echo -e "${RED}[FAIL] 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}[FAIL] 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}[OK] 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}[OK] 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}[OK] Realm '${REALM_NAME}' already exists (created concurrently)${NC}"
      else
        echo -e "${RED}[FAIL] 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}[OK] 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}[OK] 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}[OK] 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}[OK] 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}[OK] 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}[FAIL] 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}[OK] 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}[FAIL] Failed to create realm roles mapper${NC}"
        return 1
      }
    
    echo -e "${GREEN}[OK] Realm roles mapper created${NC}"
  fi
}

configure_audience_mapper() {
  echo -e "${YELLOW}🔧 Configuring audience 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}[FAIL] Could not find client ${CLIENT_ID} to configure audience mapper${NC}"
    return 1
  fi
  
  # Check if an audience 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=="audience-mapper" and .protocolMapper=="oidc-audience-mapper") | .id // empty')
  
  if [[ -n "${existing_mapper}" ]]; then
    echo -e "${GREEN}[OK] Audience mapper already exists${NC}"
  else
    echo -e "${YELLOW}📝 Creating audience mapper...${NC}"
    
    # Create protocol mapper for audience
    kcadm create "clients/${internal_id}/protocol-mappers/models" -r "${REALM_NAME}" \
      -s name="audience-mapper" \
      -s protocol="openid-connect" \
      -s protocolMapper="oidc-audience-mapper" \
      -s consentRequired=false \
      -s 'config."included.client.audience"='"${CLIENT_ID}" \
      -s 'config."id.token.claim"=false' \
      -s 'config."access.token.claim"=true' >/dev/null || {
        echo -e "${RED}[FAIL] Failed to create audience mapper${NC}"
        return 1
      }
    
    echo -e "${GREEN}[OK] Audience mapper created${NC}"
  fi
}

main() {
  command -v docker >/dev/null || { echo -e "${RED}[FAIL] Docker is required${NC}"; exit 1; }
  command -v jq >/dev/null || { echo -e "${RED}[FAIL] jq is required${NC}"; exit 1; }

  ensure_container
  echo "Keycloak URL: ${KEYCLOAK_URL}"
  wait_ready
  admin_login
  ensure_realm
  ensure_client
  configure_role_mapper
  configure_audience_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=adminuser123" \
    -d "scope=openid profile email" \
    -o /tmp/auth_test_response.json)
  
  if [[ "${validation_result: -3}" == "200" ]]; then
    echo -e "${GREEN}[OK] 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}[OK] 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}[FAIL] 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}[OK] 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}[FAIL] 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}[OK] IAM configuration set successfully${NC}"
  echo "   - Using config: $config_source"
  echo "   - Keycloak issuer: $detected_issuer"
}

main "$@"