Skip to content

Commit bf24fe9

Browse files
committed
feat(lab-04): iRedMail SSO Integration -- Keycloak LDAP federation, ldap-provider API
1 parent f40f1d3 commit bf24fe9

3 files changed

Lines changed: 275 additions & 78 deletions

File tree

.github/workflows/ci.yml

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,31 @@ jobs:
162162
run: docker compose -f docker/docker-compose.advanced.yml logs
163163
- name: Cleanup
164164
if: always()
165-
run: docker compose -f docker/docker-compose.advanced.yml down -v
165+
run: docker compose -f docker/docker-compose.advanced.yml down -v
166+
lab-04-smoke:
167+
name: Lab 04 — iRedMail SSO Integration (Keycloak LDAP federation)
168+
runs-on: ubuntu-latest
169+
needs: validate
170+
continue-on-error: true
171+
steps:
172+
- uses: actions/checkout@v4
173+
- name: Install tools
174+
run: sudo apt-get install -y curl netcat-openbsd ldap-utils
175+
- name: Validate SSO compose
176+
run: docker compose -f docker/docker-compose.sso.yml config -q && echo "SSO compose valid"
177+
- name: Start SSO stack
178+
run: docker compose -f docker/docker-compose.sso.yml up -d
179+
- name: Wait for OpenLDAP
180+
run: timeout 60 bash -c 'until timeout 3 bash -c "echo > /dev/tcp/localhost/389" 2>/dev/null; do sleep 2; done'
181+
- name: Wait for Keycloak
182+
run: timeout 120 bash -c 'until curl -sf http://localhost:8087/health/ready | grep -q UP; do sleep 5; done'
183+
- name: Wait for iRedMail
184+
run: timeout 360 bash -c 'until curl -sf http://localhost:9080/ | grep -qi "roundcube"; do sleep 10; done'
185+
- name: Run Lab 09-04 test script
186+
run: bash tests/labs/test-lab-09-04.sh
187+
- name: Collect logs on failure
188+
if: failure()
189+
run: docker compose -f docker/docker-compose.sso.yml logs
190+
- name: Cleanup
191+
if: always()
192+
run: docker compose -f docker/docker-compose.sso.yml down -v

docker/docker-compose.sso.yml

Lines changed: 129 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,141 @@
1-
# Lab 04 — SSO Integration: iredmail with Keycloak OIDC authentication
2-
---
1+
# docker-compose.sso.yml — Lab 09-04: SSO Integration
2+
# iRedMail with Keycloak LDAP user federation (Keycloak federates from OpenLDAP)
3+
name: iredmail-sso
4+
35
services:
4-
iredmail:
5-
image: iredmail/iredmail:stable
6-
container_name: it-stack-iredmail
7-
restart: unless-stopped
8-
ports:
9-
- "25:$firstPort"
6+
ldap:
7+
image: osixia/openldap:1.5.0
8+
container_name: iredmail-sso-ldap
109
environment:
11-
- IT_STACK_ENV=lab-04-sso
12-
- KEYCLOAK_URL=
13-
- KEYCLOAK_REALM=
14-
- KEYCLOAK_CLIENT_ID=iredmail
15-
- KEYCLOAK_CLIENT_SECRET=
10+
LDAP_ORGANISATION: "Lab Organization"
11+
LDAP_DOMAIN: lab.local
12+
LDAP_BASE_DN: "dc=lab,dc=local"
13+
LDAP_ADMIN_PASSWORD: Lab04Password!
14+
LDAP_READONLY_USER: "true"
15+
LDAP_READONLY_USER_USERNAME: readonly
16+
LDAP_READONLY_USER_PASSWORD: ReadOnlyPass04!
17+
LDAP_TLS: "false"
18+
ports:
19+
- "389:389"
20+
volumes:
21+
- ldap_sso_db:/var/lib/ldap
22+
- ldap_sso_config:/etc/ldap/slapd.d
1623
networks:
17-
- it-stack-net
24+
- mail-dir-net
25+
healthcheck:
26+
test: ["CMD-SHELL", "ldapsearch -x -H ldap://localhost -b dc=lab,dc=local -D 'cn=admin,dc=lab,dc=local' -w Lab04Password! -LLL -z 1 dn 2>/dev/null | grep -q 'dn:'"]
27+
interval: 20s
28+
timeout: 10s
29+
retries: 5
30+
start_period: 20s
31+
deploy:
32+
resources:
33+
limits:
34+
cpus: "0.25"
35+
memory: 256M
36+
restart: unless-stopped
1837

19-
# Local Keycloak for SSO lab (replace with lab-id1 in real env)
2038
keycloak:
21-
image: quay.io/keycloak/keycloak:24
22-
container_name: it-stack-iredmail-keycloak
39+
image: quay.io/keycloak/keycloak:24.0
40+
container_name: iredmail-sso-keycloak
2341
command: start-dev
2442
environment:
2543
KEYCLOAK_ADMIN: admin
26-
KEYCLOAK_ADMIN_PASSWORD: admin
44+
KEYCLOAK_ADMIN_PASSWORD: Lab04Admin!
45+
KC_HTTP_PORT: "8080"
46+
KC_HOSTNAME_STRICT: "false"
47+
KC_HOSTNAME_STRICT_HTTPS: "false"
48+
KC_HTTP_ENABLED: "true"
49+
KC_HEALTH_ENABLED: "true"
50+
KC_PROXY: edge
51+
ports:
52+
- "8087:8080"
53+
networks:
54+
- mail-app-net
55+
- mail-dir-net
56+
depends_on:
57+
ldap:
58+
condition: service_healthy
59+
healthcheck:
60+
test: ["CMD-SHELL", "curl -sf http://localhost:8080/health/ready | grep -q UP"]
61+
interval: 20s
62+
timeout: 10s
63+
retries: 10
64+
start_period: 60s
65+
deploy:
66+
resources:
67+
limits:
68+
cpus: "1.0"
69+
memory: 768M
70+
restart: unless-stopped
71+
72+
smtp-relay:
73+
image: mailhog/mailhog:v1.0.1
74+
container_name: iredmail-sso-smtp-relay
2775
ports:
28-
- "8080:8080"
76+
- "8025:8025"
2977
networks:
30-
- it-stack-net
78+
- mail-app-net
79+
restart: unless-stopped
80+
81+
iredmail:
82+
image: iredmail/mariadb:stable
83+
container_name: iredmail-sso-app
84+
hostname: mail.lab.local
85+
ports:
86+
- "9080:80"
87+
- "9443:443"
88+
- "9025:25"
89+
- "9587:587"
90+
- "9143:143"
91+
- "9993:993"
92+
environment:
93+
FIRST_MAIL_DOMAIN: lab.local
94+
FIRST_MAIL_DOMAIN_ADMIN_PASSWORD: Lab04Password!
95+
MLMMJADMIN_API_TOKEN: Lab04Token!
96+
ROUNDCUBE_DES_KEY: Lab04RoundcubeDesKey01
97+
LDAP_SERVER_HOST: ldap
98+
LDAP_SERVER_PORT: "389"
99+
LDAP_BIND_DN: "cn=readonly,dc=lab,dc=local"
100+
LDAP_BIND_PASSWORD: ReadOnlyPass04!
101+
LDAP_BASE_DN: "dc=lab,dc=local"
102+
ENABLE_DKIM: "1"
103+
DKIM_SELECTOR: lab
104+
POSTFIX_RELAYHOST: "[smtp-relay]:1025"
105+
volumes:
106+
- iredmail_sso_data:/var/vmail
107+
- iredmail_sso_db:/var/lib/mysql
108+
- iredmail_sso_ssl:/opt/iredmail/ssl
109+
- iredmail_sso_dkim:/opt/dkim
110+
networks:
111+
- mail-app-net
112+
- mail-dir-net
113+
depends_on:
114+
ldap:
115+
condition: service_healthy
116+
keycloak:
117+
condition: service_healthy
118+
healthcheck:
119+
test: ["CMD-SHELL", "curl -sf http://localhost/mail/ | grep -qi 'roundcube\\|webmail\\|login'"]
120+
interval: 30s
121+
timeout: 15s
122+
retries: 10
123+
start_period: 120s
124+
deploy:
125+
resources:
126+
limits:
127+
cpus: "1.0"
128+
memory: 1G
129+
restart: unless-stopped
130+
131+
volumes:
132+
ldap_sso_db:
133+
ldap_sso_config:
134+
iredmail_sso_data:
135+
iredmail_sso_db:
136+
iredmail_sso_ssl:
137+
iredmail_sso_dkim:
31138

32139
networks:
33-
it-stack-net:
34-
driver: bridge
140+
mail-app-net:
141+
mail-dir-net:

tests/labs/test-lab-09-04.sh

Lines changed: 118 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,134 @@
11
#!/usr/bin/env bash
2-
# test-lab-09-04.sh — Lab 09-04: SSO Integration
3-
# Module 09: iRedMail email server
4-
# iredmail with Keycloak OIDC/SAML authentication
2+
# test-lab-09-04.sh — Lab 09-04: iRedMail SSO Integration
3+
# Tests: Keycloak running, LDAP federation config, Keycloak user sync from LDAP
54
set -euo pipefail
6-
7-
LAB_ID="09-04"
8-
LAB_NAME="SSO Integration"
9-
MODULE="iredmail"
105
COMPOSE_FILE="docker/docker-compose.sso.yml"
11-
PASS=0
12-
FAIL=0
13-
14-
# ── Colors ────────────────────────────────────────────────────────────────────
15-
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
16-
CYAN='\033[0;36m'; NC='\033[0m'
17-
18-
pass() { echo -e "${GREEN}[PASS]${NC} $1"; ((PASS++)); }
19-
fail() { echo -e "${RED}[FAIL]${NC} $1"; ((FAIL++)); }
20-
info() { echo -e "${CYAN}[INFO]${NC} $1"; }
21-
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
6+
KC_PORT="8087"
7+
PASS=0; FAIL=0
8+
pass() { echo " [PASS] $1"; PASS=$((PASS+1)); }
9+
fail() { echo " [FAIL] $1"; FAIL=$((FAIL+1)); }
10+
section() { echo; echo "=== $1 ==="; }
2211

23-
echo -e "${CYAN}======================================${NC}"
24-
echo -e "${CYAN} Lab ${LAB_ID}: ${LAB_NAME}${NC}"
25-
echo -e "${CYAN} Module: ${MODULE}${NC}"
26-
echo -e "${CYAN}======================================${NC}"
27-
echo ""
12+
section "Container health"
13+
for c in iredmail-sso-ldap iredmail-sso-smtp-relay iredmail-sso-keycloak iredmail-sso-app; do
14+
if docker inspect --format '{{.State.Running}}' "$c" 2>/dev/null | grep -q true; then
15+
pass "Container $c is running"
16+
else
17+
fail "Container $c is not running"
18+
fi
19+
done
2820

29-
# ── PHASE 1: Setup ────────────────────────────────────────────────────────────
30-
info "Phase 1: Setup"
31-
docker compose -f "${COMPOSE_FILE}" up -d
32-
info "Waiting 30s for ${MODULE} to initialize..."
33-
sleep 30
21+
section "OpenLDAP connectivity"
22+
if timeout 5 bash -c 'echo > /dev/tcp/localhost/389' 2>/dev/null; then
23+
pass "OpenLDAP :389 reachable"
24+
else
25+
fail "OpenLDAP :389 not reachable"
26+
fi
27+
LDAP_SEARCH=$(ldapsearch -x -H ldap://localhost:389 \
28+
-b "dc=lab,dc=local" \
29+
-D "cn=admin,dc=lab,dc=local" \
30+
-w Lab04Password! -LLL -z 1 dn 2>/dev/null | head -3) || LDAP_SEARCH=""
31+
if echo "$LDAP_SEARCH" | grep -q "dn:"; then
32+
pass "LDAP admin bind and search successful"
33+
else
34+
fail "LDAP admin bind failed"
35+
fi
3436

35-
# ── PHASE 2: Health Checks ────────────────────────────────────────────────────
36-
info "Phase 2: Health Checks"
37+
section "Keycloak health"
38+
KC_HEALTH=$(curl -sf "http://localhost:${KC_PORT}/health/ready" 2>/dev/null) || KC_HEALTH=""
39+
if echo "$KC_HEALTH" | grep -q "UP"; then
40+
pass "Keycloak health/ready = UP"
41+
else
42+
fail "Keycloak health/ready not UP"
43+
fi
3744

38-
if docker compose -f "${COMPOSE_FILE}" ps | grep -q "running\|Up"; then
39-
pass "Container is running"
45+
section "Keycloak admin API + realm"
46+
KC_TOKEN=$(curl -sf -X POST \
47+
"http://localhost:${KC_PORT}/realms/master/protocol/openid-connect/token" \
48+
-H "Content-Type: application/x-www-form-urlencoded" \
49+
-d "client_id=admin-cli&username=admin&password=Lab04Admin!&grant_type=password" 2>/dev/null \
50+
| grep -o '"access_token":"[^"]*"' | cut -d'"' -f4) || KC_TOKEN=""
51+
if [ -n "$KC_TOKEN" ]; then
52+
pass "Keycloak admin token obtained"
4053
else
41-
fail "Container is not running"
54+
fail "Keycloak admin login failed"
4255
fi
4356

44-
# ── PHASE 3: Functional Tests ─────────────────────────────────────────────────
45-
info "Phase 3: Functional Tests (Lab 04 — SSO Integration)"
57+
if [ -n "$KC_TOKEN" ]; then
58+
curl -sf -X POST "http://localhost:${KC_PORT}/admin/realms" \
59+
-H "Authorization: Bearer $KC_TOKEN" -H "Content-Type: application/json" \
60+
-d '{"realm":"it-stack","enabled":true}' 2>/dev/null || true
61+
REALM_CHECK=$(curl -sf "http://localhost:${KC_PORT}/admin/realms/it-stack" \
62+
-H "Authorization: Bearer $KC_TOKEN" 2>/dev/null) || REALM_CHECK=""
63+
if echo "$REALM_CHECK" | grep -q '"realm":"it-stack"'; then
64+
pass "Keycloak realm 'it-stack' exists"
65+
else
66+
fail "Keycloak realm 'it-stack' not found"
67+
fi
68+
else
69+
fail "Skipping realm check (no admin token)"
70+
fi
4671

47-
# TODO: Add module-specific functional tests here
48-
# Example:
49-
# if curl -sf http://localhost:25/health > /dev/null 2>&1; then
50-
# pass "Health endpoint responds"
51-
# else
52-
# fail "Health endpoint not reachable"
53-
# fi
72+
section "Keycloak LDAP user federation"
73+
if [ -n "$KC_TOKEN" ]; then
74+
LDAP_PROVIDER=$(curl -sf -X POST \
75+
"http://localhost:${KC_PORT}/admin/realms/it-stack/components" \
76+
-H "Authorization: Bearer $KC_TOKEN" \
77+
-H "Content-Type: application/json" \
78+
-d '{"name":"ldap","providerId":"ldap","providerType":"org.keycloak.storage.UserStorageProvider","config":{"vendor":["other"],"connectionUrl":["ldap://ldap:389"],"bindDn":["cn=readonly,dc=lab,dc=local"],"bindCredential":["ReadOnlyPass04!"],"usersDn":["ou=People,dc=lab,dc=local"],"usernameLDAPAttribute":["mail"],"rdnLDAPAttribute":["uid"],"uuidLDAPAttribute":["entryUUID"],"userObjectClasses":["inetOrgPerson"],"importEnabled":["true"],"syncRegistrations":["false"]}}' \
79+
2>/dev/null; echo $?) || LDAP_PROVIDER=0
80+
COMPONENTS=$(curl -sf \
81+
"http://localhost:${KC_PORT}/admin/realms/it-stack/components?type=org.keycloak.storage.UserStorageProvider" \
82+
-H "Authorization: Bearer $KC_TOKEN" 2>/dev/null) || COMPONENTS=""
83+
if echo "$COMPONENTS" | grep -q '"providerId":"ldap"'; then
84+
pass "Keycloak LDAP user federation provider registered"
85+
else
86+
fail "Keycloak LDAP user federation not found"
87+
fi
88+
else
89+
fail "Skipping LDAP federation check (no admin token)"
90+
fi
5491

55-
warn "Functional tests for Lab 09-04 pending implementation"
92+
section "iRedMail LDAP config in env"
93+
IM_ENV=$(docker inspect iredmail-sso-app --format '{{json .Config.Env}}' 2>/dev/null) || IM_ENV="[]"
94+
if echo "$IM_ENV" | grep -q "LDAP_SERVER_HOST=ldap"; then
95+
pass "LDAP_SERVER_HOST=ldap configured"
96+
else
97+
fail "LDAP_SERVER_HOST not set to 'ldap'"
98+
fi
99+
if echo "$IM_ENV" | grep -q '"LDAP_BIND_DN=cn=readonly,dc=lab,dc=local"'; then
100+
pass "LDAP_BIND_DN = cn=readonly,dc=lab,dc=local"
101+
else
102+
fail "LDAP_BIND_DN not configured"
103+
fi
56104

57-
# ── PHASE 4: Cleanup ──────────────────────────────────────────────────────────
58-
info "Phase 4: Cleanup"
59-
docker compose -f "${COMPOSE_FILE}" down -v --remove-orphans
60-
info "Cleanup complete"
105+
section "Roundcube webmail"
106+
HTTP_CODE=$(curl -sw '%{http_code}' -o /dev/null http://localhost:9080/mail/ 2>/dev/null) || HTTP_CODE="000"
107+
if echo "$HTTP_CODE" | grep -qE "^(200|301|302)"; then
108+
pass "Roundcube /mail/ HTTP $HTTP_CODE"
109+
else
110+
fail "Roundcube /mail/ returned $HTTP_CODE"
111+
fi
61112

62-
# ── Results ───────────────────────────────────────────────────────────────────
63-
echo ""
64-
echo -e "${CYAN}======================================${NC}"
65-
echo -e " Lab ${LAB_ID} Complete"
66-
echo -e " ${GREEN}PASS: ${PASS}${NC} | ${RED}FAIL: ${FAIL}${NC}"
67-
echo -e "${CYAN}======================================${NC}"
113+
section "SMTP port"
114+
SMTP_BANNER=$(echo "QUIT" | timeout 5 nc localhost 9025 2>/dev/null | head -1) || SMTP_BANNER=""
115+
if echo "$SMTP_BANNER" | grep -q "220"; then
116+
pass "SMTP :9025 banner OK"
117+
else
118+
fail "SMTP :9025 banner failed"
119+
fi
68120

69-
if [ "${FAIL}" -gt 0 ]; then
70-
exit 1
121+
section "Keycloak OIDC discovery"
122+
KC_OIDC=$(curl -sf "http://localhost:${KC_PORT}/realms/it-stack/.well-known/openid-configuration" 2>/dev/null) || KC_OIDC=""
123+
if echo "$KC_OIDC" | grep -q '"issuer"'; then
124+
pass "Keycloak OIDC discovery reachable"
125+
else
126+
fail "Keycloak OIDC discovery failed"
71127
fi
128+
129+
echo
130+
echo "====================================="
131+
echo " iRedMail Lab 09-04 Results"
132+
echo " PASS: $PASS FAIL: $FAIL"
133+
echo "====================================="
134+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1

0 commit comments

Comments
 (0)