setup_keycloak.sh 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. # Colors
  4. RED='\033[0;31m'
  5. GREEN='\033[0;32m'
  6. YELLOW='\033[1;33m'
  7. BLUE='\033[0;34m'
  8. NC='\033[0m'
  9. KEYCLOAK_IMAGE="quay.io/keycloak/keycloak:26.0.7"
  10. CONTAINER_NAME="keycloak-iam-test"
  11. KEYCLOAK_PORT="8080" # Default external port
  12. KEYCLOAK_INTERNAL_PORT="8080" # Internal container port (always 8080)
  13. KEYCLOAK_URL="http://localhost:${KEYCLOAK_PORT}"
  14. # Realm and test fixtures expected by tests
  15. REALM_NAME="seaweedfs-test"
  16. CLIENT_ID="seaweedfs-s3"
  17. CLIENT_SECRET="seaweedfs-s3-secret"
  18. ROLE_ADMIN="s3-admin"
  19. ROLE_READONLY="s3-read-only"
  20. ROLE_WRITEONLY="s3-write-only"
  21. ROLE_READWRITE="s3-read-write"
  22. # User credentials (matches Docker setup script logic: removes non-alphabetic chars + "123")
  23. get_user_password() {
  24. case "$1" in
  25. "admin-user") echo "adminuser123" ;; # "admin-user" -> "adminuser123"
  26. "read-user") echo "readuser123" ;; # "read-user" -> "readuser123"
  27. "write-user") echo "writeuser123" ;; # "write-user" -> "writeuser123"
  28. "write-only-user") echo "writeonlyuser123" ;; # "write-only-user" -> "writeonlyuser123"
  29. *) echo "" ;;
  30. esac
  31. }
  32. # List of users to create
  33. USERS="admin-user read-user write-user write-only-user"
  34. echo -e "${BLUE}🔧 Setting up Keycloak realm and users for SeaweedFS S3 IAM testing...${NC}"
  35. ensure_container() {
  36. # Check for any existing Keycloak container and detect its port
  37. local keycloak_containers=$(docker ps --format '{{.Names}}\t{{.Ports}}' | grep -E "(keycloak|quay.io/keycloak)")
  38. if [[ -n "$keycloak_containers" ]]; then
  39. # Parse the first available Keycloak container
  40. CONTAINER_NAME=$(echo "$keycloak_containers" | head -1 | awk '{print $1}')
  41. # Extract the external port from the port mapping using sed (compatible with older bash)
  42. local port_mapping=$(echo "$keycloak_containers" | head -1 | awk '{print $2}')
  43. local extracted_port=$(echo "$port_mapping" | sed -n 's/.*:\([0-9]*\)->8080.*/\1/p')
  44. if [[ -n "$extracted_port" ]]; then
  45. KEYCLOAK_PORT="$extracted_port"
  46. KEYCLOAK_URL="http://localhost:${KEYCLOAK_PORT}"
  47. echo -e "${GREEN}✅ Using existing container '${CONTAINER_NAME}' on port ${KEYCLOAK_PORT}${NC}"
  48. return 0
  49. fi
  50. fi
  51. # Fallback: check for specific container names
  52. if docker ps --format '{{.Names}}' | grep -q '^keycloak$'; then
  53. CONTAINER_NAME="keycloak"
  54. # Try to detect port for 'keycloak' container using docker port command
  55. local ports=$(docker port keycloak 8080 2>/dev/null | head -1)
  56. if [[ -n "$ports" ]]; then
  57. local extracted_port=$(echo "$ports" | sed -n 's/.*:\([0-9]*\)$/\1/p')
  58. if [[ -n "$extracted_port" ]]; then
  59. KEYCLOAK_PORT="$extracted_port"
  60. KEYCLOAK_URL="http://localhost:${KEYCLOAK_PORT}"
  61. fi
  62. fi
  63. echo -e "${GREEN}✅ Using existing container '${CONTAINER_NAME}' on port ${KEYCLOAK_PORT}${NC}"
  64. return 0
  65. fi
  66. if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
  67. echo -e "${GREEN}✅ Using existing container '${CONTAINER_NAME}'${NC}"
  68. return 0
  69. fi
  70. echo -e "${YELLOW}🐳 Starting Keycloak container (${KEYCLOAK_IMAGE})...${NC}"
  71. docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true
  72. docker run -d --name "${CONTAINER_NAME}" -p "${KEYCLOAK_PORT}:8080" \
  73. -e KEYCLOAK_ADMIN=admin \
  74. -e KEYCLOAK_ADMIN_PASSWORD=admin \
  75. -e KC_HTTP_ENABLED=true \
  76. -e KC_HOSTNAME_STRICT=false \
  77. -e KC_HOSTNAME_STRICT_HTTPS=false \
  78. -e KC_HEALTH_ENABLED=true \
  79. "${KEYCLOAK_IMAGE}" start-dev >/dev/null
  80. }
  81. wait_ready() {
  82. echo -e "${YELLOW}⏳ Waiting for Keycloak to be ready...${NC}"
  83. for i in $(seq 1 120); do
  84. if curl -sf "${KEYCLOAK_URL}/health/ready" >/dev/null; then
  85. echo -e "${GREEN}✅ Keycloak health check passed${NC}"
  86. return 0
  87. fi
  88. if curl -sf "${KEYCLOAK_URL}/realms/master" >/dev/null; then
  89. echo -e "${GREEN}✅ Keycloak master realm accessible${NC}"
  90. return 0
  91. fi
  92. sleep 2
  93. done
  94. echo -e "${RED}❌ Keycloak did not become ready in time${NC}"
  95. exit 1
  96. }
  97. kcadm() {
  98. # Always authenticate before each command to ensure context
  99. # Try different admin passwords that might be used in different environments
  100. # GitHub Actions uses "admin", local testing might use "admin123"
  101. local admin_passwords=("admin" "admin123" "password")
  102. local auth_success=false
  103. for pwd in "${admin_passwords[@]}"; do
  104. 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
  105. auth_success=true
  106. break
  107. fi
  108. done
  109. if [[ "$auth_success" == false ]]; then
  110. echo -e "${RED}❌ Failed to authenticate with any known admin password${NC}"
  111. return 1
  112. fi
  113. docker exec -i "${CONTAINER_NAME}" /opt/keycloak/bin/kcadm.sh "$@"
  114. }
  115. admin_login() {
  116. # This is now handled by each kcadm() call
  117. echo "Logging into http://localhost:${KEYCLOAK_INTERNAL_PORT} as user admin of realm master"
  118. }
  119. ensure_realm() {
  120. if kcadm get realms | grep -q "${REALM_NAME}"; then
  121. echo -e "${GREEN}✅ Realm '${REALM_NAME}' already exists${NC}"
  122. else
  123. echo -e "${YELLOW}📝 Creating realm '${REALM_NAME}'...${NC}"
  124. if kcadm create realms -s realm="${REALM_NAME}" -s enabled=true 2>/dev/null; then
  125. echo -e "${GREEN}✅ Realm created${NC}"
  126. else
  127. # Check if it exists now (might have been created by another process)
  128. if kcadm get realms | grep -q "${REALM_NAME}"; then
  129. echo -e "${GREEN}✅ Realm '${REALM_NAME}' already exists (created concurrently)${NC}"
  130. else
  131. echo -e "${RED}❌ Failed to create realm '${REALM_NAME}'${NC}"
  132. return 1
  133. fi
  134. fi
  135. fi
  136. }
  137. ensure_client() {
  138. local id
  139. id=$(kcadm get clients -r "${REALM_NAME}" -q clientId="${CLIENT_ID}" | jq -r '.[0].id // empty')
  140. if [[ -n "${id}" ]]; then
  141. echo -e "${GREEN}✅ Client '${CLIENT_ID}' already exists${NC}"
  142. else
  143. echo -e "${YELLOW}📝 Creating client '${CLIENT_ID}'...${NC}"
  144. kcadm create clients -r "${REALM_NAME}" \
  145. -s clientId="${CLIENT_ID}" \
  146. -s protocol=openid-connect \
  147. -s publicClient=false \
  148. -s serviceAccountsEnabled=true \
  149. -s directAccessGrantsEnabled=true \
  150. -s standardFlowEnabled=true \
  151. -s implicitFlowEnabled=false \
  152. -s secret="${CLIENT_SECRET}" >/dev/null
  153. echo -e "${GREEN}✅ Client created${NC}"
  154. fi
  155. # Create and configure role mapper for the client
  156. configure_role_mapper "${CLIENT_ID}"
  157. }
  158. ensure_role() {
  159. local role="$1"
  160. if kcadm get roles -r "${REALM_NAME}" | jq -r '.[].name' | grep -qx "${role}"; then
  161. echo -e "${GREEN}✅ Role '${role}' exists${NC}"
  162. else
  163. echo -e "${YELLOW}📝 Creating role '${role}'...${NC}"
  164. kcadm create roles -r "${REALM_NAME}" -s name="${role}" >/dev/null
  165. fi
  166. }
  167. ensure_user() {
  168. local username="$1" password="$2"
  169. local uid
  170. uid=$(kcadm get users -r "${REALM_NAME}" -q username="${username}" | jq -r '.[0].id // empty')
  171. if [[ -z "${uid}" ]]; then
  172. echo -e "${YELLOW}📝 Creating user '${username}'...${NC}"
  173. uid=$(kcadm create users -r "${REALM_NAME}" \
  174. -s username="${username}" \
  175. -s enabled=true \
  176. -s email="${username}@seaweedfs.test" \
  177. -s emailVerified=true \
  178. -s firstName="${username}" \
  179. -s lastName="User" \
  180. -i)
  181. else
  182. echo -e "${GREEN}✅ User '${username}' exists${NC}"
  183. fi
  184. echo -e "${YELLOW}🔑 Setting password for '${username}'...${NC}"
  185. kcadm set-password -r "${REALM_NAME}" --userid "${uid}" --new-password "${password}" --temporary=false >/dev/null
  186. }
  187. assign_role() {
  188. local username="$1" role="$2"
  189. local uid rid
  190. uid=$(kcadm get users -r "${REALM_NAME}" -q username="${username}" | jq -r '.[0].id')
  191. rid=$(kcadm get roles -r "${REALM_NAME}" | jq -r ".[] | select(.name==\"${role}\") | .id")
  192. # Check if role already assigned
  193. if kcadm get "users/${uid}/role-mappings/realm" -r "${REALM_NAME}" | jq -r '.[].name' | grep -qx "${role}"; then
  194. echo -e "${GREEN}✅ User '${username}' already has role '${role}'${NC}"
  195. return 0
  196. fi
  197. echo -e "${YELLOW}➕ Assigning role '${role}' to '${username}'...${NC}"
  198. kcadm add-roles -r "${REALM_NAME}" --uid "${uid}" --rolename "${role}" >/dev/null
  199. }
  200. configure_role_mapper() {
  201. echo -e "${YELLOW}🔧 Configuring role mapper for client '${CLIENT_ID}'...${NC}"
  202. # Get client's internal ID
  203. local internal_id
  204. internal_id=$(kcadm get clients -r "${REALM_NAME}" -q clientId="${CLIENT_ID}" | jq -r '.[0].id // empty')
  205. if [[ -z "${internal_id}" ]]; then
  206. echo -e "${RED}❌ Could not find client ${client_id} to configure role mapper${NC}"
  207. return 1
  208. fi
  209. # Check if a realm roles mapper already exists for this client
  210. local existing_mapper
  211. 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')
  212. if [[ -n "${existing_mapper}" ]]; then
  213. echo -e "${GREEN}✅ Realm roles mapper already exists${NC}"
  214. else
  215. echo -e "${YELLOW}📝 Creating realm roles mapper...${NC}"
  216. # Create protocol mapper for realm roles
  217. kcadm create "clients/${internal_id}/protocol-mappers/models" -r "${REALM_NAME}" \
  218. -s name="realm roles" \
  219. -s protocol="openid-connect" \
  220. -s protocolMapper="oidc-usermodel-realm-role-mapper" \
  221. -s consentRequired=false \
  222. -s 'config."multivalued"=true' \
  223. -s 'config."userinfo.token.claim"=true' \
  224. -s 'config."id.token.claim"=true' \
  225. -s 'config."access.token.claim"=true' \
  226. -s 'config."claim.name"=roles' \
  227. -s 'config."jsonType.label"=String' >/dev/null || {
  228. echo -e "${RED}❌ Failed to create realm roles mapper${NC}"
  229. return 1
  230. }
  231. echo -e "${GREEN}✅ Realm roles mapper created${NC}"
  232. fi
  233. }
  234. configure_audience_mapper() {
  235. echo -e "${YELLOW}🔧 Configuring audience mapper for client '${CLIENT_ID}'...${NC}"
  236. # Get client's internal ID
  237. local internal_id
  238. internal_id=$(kcadm get clients -r "${REALM_NAME}" -q clientId="${CLIENT_ID}" | jq -r '.[0].id // empty')
  239. if [[ -z "${internal_id}" ]]; then
  240. echo -e "${RED}❌ Could not find client ${CLIENT_ID} to configure audience mapper${NC}"
  241. return 1
  242. fi
  243. # Check if an audience mapper already exists for this client
  244. local existing_mapper
  245. 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')
  246. if [[ -n "${existing_mapper}" ]]; then
  247. echo -e "${GREEN}✅ Audience mapper already exists${NC}"
  248. else
  249. echo -e "${YELLOW}📝 Creating audience mapper...${NC}"
  250. # Create protocol mapper for audience
  251. kcadm create "clients/${internal_id}/protocol-mappers/models" -r "${REALM_NAME}" \
  252. -s name="audience-mapper" \
  253. -s protocol="openid-connect" \
  254. -s protocolMapper="oidc-audience-mapper" \
  255. -s consentRequired=false \
  256. -s 'config."included.client.audience"='"${CLIENT_ID}" \
  257. -s 'config."id.token.claim"=false' \
  258. -s 'config."access.token.claim"=true' >/dev/null || {
  259. echo -e "${RED}❌ Failed to create audience mapper${NC}"
  260. return 1
  261. }
  262. echo -e "${GREEN}✅ Audience mapper created${NC}"
  263. fi
  264. }
  265. main() {
  266. command -v docker >/dev/null || { echo -e "${RED}❌ Docker is required${NC}"; exit 1; }
  267. command -v jq >/dev/null || { echo -e "${RED}❌ jq is required${NC}"; exit 1; }
  268. ensure_container
  269. echo "Keycloak URL: ${KEYCLOAK_URL}"
  270. wait_ready
  271. admin_login
  272. ensure_realm
  273. ensure_client
  274. configure_role_mapper
  275. configure_audience_mapper
  276. ensure_role "${ROLE_ADMIN}"
  277. ensure_role "${ROLE_READONLY}"
  278. ensure_role "${ROLE_WRITEONLY}"
  279. ensure_role "${ROLE_READWRITE}"
  280. for u in $USERS; do
  281. ensure_user "$u" "$(get_user_password "$u")"
  282. done
  283. assign_role admin-user "${ROLE_ADMIN}"
  284. assign_role read-user "${ROLE_READONLY}"
  285. assign_role write-user "${ROLE_READWRITE}"
  286. # Also create a dedicated write-only user for testing
  287. ensure_user write-only-user "$(get_user_password write-only-user)"
  288. assign_role write-only-user "${ROLE_WRITEONLY}"
  289. # Copy the appropriate IAM configuration for this environment
  290. setup_iam_config
  291. # Validate the setup by testing authentication and role inclusion
  292. echo -e "${YELLOW}🔍 Validating setup by testing admin-user authentication and role mapping...${NC}"
  293. sleep 2
  294. local validation_result=$(curl -s -w "%{http_code}" -X POST "http://localhost:${KEYCLOAK_PORT}/realms/${REALM_NAME}/protocol/openid-connect/token" \
  295. -H "Content-Type: application/x-www-form-urlencoded" \
  296. -d "grant_type=password" \
  297. -d "client_id=${CLIENT_ID}" \
  298. -d "client_secret=${CLIENT_SECRET}" \
  299. -d "username=admin-user" \
  300. -d "password=adminuser123" \
  301. -d "scope=openid profile email" \
  302. -o /tmp/auth_test_response.json)
  303. if [[ "${validation_result: -3}" == "200" ]]; then
  304. echo -e "${GREEN}✅ Authentication validation successful${NC}"
  305. # Extract and decode JWT token to check for roles
  306. local access_token=$(cat /tmp/auth_test_response.json | jq -r '.access_token // empty')
  307. if [[ -n "${access_token}" ]]; then
  308. # Decode JWT payload (second part) and check for roles
  309. local payload=$(echo "${access_token}" | cut -d'.' -f2)
  310. # Add padding if needed for base64 decode
  311. while [[ $((${#payload} % 4)) -ne 0 ]]; do
  312. payload="${payload}="
  313. done
  314. local decoded=$(echo "${payload}" | base64 -d 2>/dev/null || echo "{}")
  315. local roles=$(echo "${decoded}" | jq -r '.roles // empty' 2>/dev/null || echo "")
  316. if [[ -n "${roles}" && "${roles}" != "null" ]]; then
  317. echo -e "${GREEN}✅ JWT token includes roles: ${roles}${NC}"
  318. else
  319. echo -e "${YELLOW}⚠️ JWT token does not include 'roles' claim${NC}"
  320. echo -e "${YELLOW}Decoded payload sample:${NC}"
  321. echo "${decoded}" | jq '.' 2>/dev/null || echo "${decoded}"
  322. fi
  323. fi
  324. else
  325. echo -e "${RED}❌ Authentication validation failed with HTTP ${validation_result: -3}${NC}"
  326. echo -e "${YELLOW}Response body:${NC}"
  327. cat /tmp/auth_test_response.json 2>/dev/null || echo "No response body"
  328. echo -e "${YELLOW}This may indicate a setup issue that needs to be resolved${NC}"
  329. fi
  330. rm -f /tmp/auth_test_response.json
  331. echo -e "${GREEN}✅ Keycloak test realm '${REALM_NAME}' configured${NC}"
  332. }
  333. setup_iam_config() {
  334. echo -e "${BLUE}🔧 Setting up IAM configuration for detected environment${NC}"
  335. # Change to script directory to ensure config files are found
  336. local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  337. cd "$script_dir"
  338. # Choose the appropriate config based on detected port
  339. local config_source
  340. if [[ "${KEYCLOAK_PORT}" == "8080" ]]; then
  341. config_source="iam_config.github.json"
  342. echo " Using GitHub Actions configuration (port 8080)"
  343. else
  344. config_source="iam_config.local.json"
  345. echo " Using local development configuration (port ${KEYCLOAK_PORT})"
  346. fi
  347. # Verify source config exists
  348. if [[ ! -f "$config_source" ]]; then
  349. echo -e "${RED}❌ Config file $config_source not found in $script_dir${NC}"
  350. exit 1
  351. fi
  352. # Copy the appropriate config
  353. cp "$config_source" "iam_config.json"
  354. local detected_issuer=$(cat iam_config.json | jq -r '.providers[] | select(.name=="keycloak") | .config.issuer')
  355. echo -e "${GREEN}✅ IAM configuration set successfully${NC}"
  356. echo " - Using config: $config_source"
  357. echo " - Keycloak issuer: $detected_issuer"
  358. }
  359. main "$@"