recipe-maintainer: public snapshot (secrets + deployment plans removed, single commit)

Sanitized single-commit public mirror of recipe-maintainer.
- Removed test-ssh/.testenv (live creds); added test-ssh/.testenv.example placeholders.
- Removed plans/ and planned-updates/ (deployment-planning docs) so no client/
  deployment domains appear in the public repo.
- All other secret stores were already gitignored.
- docs.coopcloud.tech retained as a submodule (public upstream).
This commit is contained in:
2026-06-16 20:18:24 +00:00
commit f283a371bb
253 changed files with 15975 additions and 0 deletions
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Setup Keycloak OIDC integration for Lichen.
Creates a Keycloak realm, OIDC client, and test user, then inserts
the OIDC secrets and enables the OIDC compose overlay for lichen.
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
from lib.abra import app_secret_insert
from lib.env import apply_env_overrides, get_abra_env_path, read_env_file
from lib.keycloak import KeycloakAdmin
from lib.models import load_default_instance
from lib.secrets import load_secrets
# Configuration
REALM = "lichen"
CLIENT_ID = "lichen"
TEST_USER = "testuser"
TEST_PASS = "testpass123"
TEST_EMAIL = f"{TEST_USER}@test.example.com"
def main():
inst = load_default_instance()
lichen_domain = inst.default_domain("lichen")
kc_domain = inst.default_domain("keycloak")
# Get Keycloak admin password from synced secrets
kc_secrets = load_secrets(kc_domain)
kc_admin_pass = kc_secrets["admin_password"]
kc = KeycloakAdmin(f"https://{kc_domain}", "admin", kc_admin_pass)
# Step 1: Create realm
kc.ensure_realm(REALM)
# Step 2: Create OIDC client
_, client_secret = kc.ensure_client(
REALM, CLIENT_ID,
redirect_uris=[f"https://{lichen_domain}/*"],
web_origins=[f"https://{lichen_domain}"],
)
# Step 3: Create test user
kc.ensure_user(REALM, TEST_USER, TEST_EMAIL, TEST_PASS)
# Step 4: Insert OIDC secrets via abra
print("=== Insert OIDC secrets into Lichen ===", flush=True)
issuer_url = f"https://{kc_domain}/realms/{REALM}"
app_secret_insert(lichen_domain, "oidc_issuer_url", "v1", issuer_url)
app_secret_insert(lichen_domain, "oidc_client_id", "v1", CLIENT_ID)
app_secret_insert(lichen_domain, "oidc_client_secret", "v1", client_secret)
# Step 5: Update lichen env with OIDC compose overlay
print("=== Update Lichen env with OIDC overlay ===", flush=True)
env_path = get_abra_env_path(inst.server, lichen_domain)
apply_env_overrides(env_path, {
"COMPOSE_FILE": '"compose.yml:compose.oidc.yml"',
"SECRET_OIDC_ISSUER_URL_VERSION": "v1",
"SECRET_OIDC_CLIENT_ID_VERSION": "v1",
"SECRET_OIDC_CLIENT_SECRET_VERSION": "v1",
"LICHEN_TOML_VERSION": "v1",
})
# Step 6: Write credentials file
script_dir = os.path.dirname(os.path.abspath(__file__))
creds_file = os.path.join(script_dir, f"keycloak-test-credentials.{inst.domain_suffix}.toml")
print(f"=== Write credentials to {creds_file} ===", flush=True)
with open(creds_file, "w") as f:
f.write(f'# Keycloak OIDC credentials for lichen test instance\n')
f.write(f'#\n')
f.write(f'# Keycloak instance: {kc_domain}\n')
f.write(f'# Realm: {REALM}\n')
f.write(f'# Created by: setup_keycloak_integration.py\n')
f.write(f'\n')
f.write(f'# Keycloak admin (master realm)\n')
f.write(f'kc_admin_user = "admin"\n')
f.write(f'kc_admin_pass = "{kc_admin_pass}"\n')
f.write(f'\n')
f.write(f'# OIDC client\n')
f.write(f'kc_realm = "{REALM}"\n')
f.write(f'kc_client_id = "{CLIENT_ID}"\n')
f.write(f'kc_client_secret = "{client_secret}"\n')
f.write(f'kc_issuer_url = "{issuer_url}"\n')
f.write(f'\n')
f.write(f'# Test user (in {REALM} realm)\n')
f.write(f'kc_test_user = "{TEST_USER}"\n')
f.write(f'kc_test_pass = "{TEST_PASS}"\n')
f.write(f'kc_test_email = "{TEST_EMAIL}"\n')
print(f" Written to {creds_file}", flush=True)
print("", flush=True)
print("=== Keycloak integration setup complete ===", flush=True)
print("", flush=True)
print("Next steps:", flush=True)
print(f" 1. Redeploy lichen: abra app deploy {lichen_domain} --chaos --force --no-input", flush=True)
print(f" 2. Run OIDC test: python3 recipe-info/lichen/tests/oidc_integration.py", flush=True)
if __name__ == "__main__":
main()
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Basic health check for Lichen — verifies the dashboard and API are up."""
import argparse
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
from utils.tests.helpers import http_get, resolve_domain
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--domain', default=os.environ.get('TEST_DOMAIN'))
args = parser.parse_args()
domain = args.domain or resolve_domain('lichen')
url = f"https://{domain}"
print("Lichen health check")
print()
# Step 1: TLS check endpoint
print("Step 1: Checking /tls-check endpoint ...")
status, _ = http_get(f"{url}/tls-check")
if status != 200:
print(f" FAIL: /tls-check returned HTTP {status}")
sys.exit(1)
print(f" PASS: /tls-check returned HTTP {status}")
# Step 2: Dashboard redirects to login (auth enabled)
print("Step 2: Checking dashboard redirects to login ...")
status, _ = http_get(url)
if status not in (200, 303, 302):
print(f" FAIL: Dashboard returned HTTP {status}")
sys.exit(1)
print(f" PASS: Dashboard returned HTTP {status}")
# Step 3: API endpoint
print("Step 3: Checking /api/sites endpoint ...")
status, _ = http_get(f"{url}/api/sites")
if status == 0 or status >= 500:
print(f" FAIL: API returned HTTP {status}")
sys.exit(1)
print(f" PASS: API returned HTTP {status}")
print()
print("PASS: Lichen health check passed")
if __name__ == '__main__':
main()
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""Lichen OIDC integration test — validates that Keycloak SSO login works
with lichen's OIDC provider support.
Tests:
1. Keycloak OIDC discovery endpoint is accessible
2. Can obtain a token from Keycloak using test credentials
3. Lichen's /oidc/start endpoint redirects to Keycloak
4. Lichen's login page shows the SSO login button
"""
import argparse
import os
import sys
import urllib.parse
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
from utils.tests.helpers import (
http_get, http_post, load_toml_credentials, resolve_domain,
)
def main():
if os.environ.get('SKIP_INTEGRATION') == '1':
print("SKIP: SKIP_INTEGRATION=1 is set, skipping OIDC integration test")
sys.exit(0)
parser = argparse.ArgumentParser()
parser.add_argument('--domain', default=os.environ.get('TEST_DOMAIN'))
args = parser.parse_args()
lichen_domain = args.domain or resolve_domain('lichen')
kc_domain = resolve_domain('keycloak')
lichen_url = f"https://{lichen_domain}"
kc_url = f"https://{kc_domain}"
# Load credentials
creds_path = os.path.join(os.path.dirname(__file__), '..')
creds = load_toml_credentials(creds_path, 'keycloak')
if creds is None:
print("FAIL: Credentials file not found")
print("Run recipe-info/lichen/setup_keycloak_integration.py first.")
sys.exit(1)
print("Testing Lichen OIDC integration with Keycloak")
print()
# Step 1: Check lichen is reachable
print("Step 1: Checking Lichen is reachable ...")
status, _ = http_get(lichen_url)
if status == 0 or status >= 500:
print(f" FAIL: Lichen at {lichen_url} returned HTTP {status}")
sys.exit(1)
print(f" PASS: Lichen is reachable (HTTP {status})")
# Step 2: Verify OIDC discovery endpoint on Keycloak
print("Step 2: Checking Keycloak OIDC discovery endpoint ...")
discovery_url = f"{kc_url}/realms/{creds['kc_realm']}/.well-known/openid-configuration"
status, data = http_get(discovery_url)
if status != 200:
print(f" FAIL: OIDC discovery endpoint returned HTTP {status}")
sys.exit(1)
issuer = (data or {}).get("issuer", "")
print(f" PASS: OIDC discovery endpoint OK (issuer: {issuer})")
# Step 3: Obtain token from Keycloak via direct grant
print("Step 3: Obtaining token from Keycloak ...")
token_url = f"{kc_url}/realms/{creds['kc_realm']}/protocol/openid-connect/token"
status, data = http_post(
token_url,
data={
"grant_type": "password",
"client_id": creds["kc_client_id"],
"client_secret": creds["kc_client_secret"],
"username": creds["kc_test_user"],
"password": creds["kc_test_pass"],
"scope": "openid email profile",
},
content_type="application/x-www-form-urlencoded",
)
access_token = (data or {}).get("access_token", "")
if not access_token:
error = (data or {}).get("error_description", (data or {}).get("error", "unknown"))
print(f" FAIL: Could not obtain token: {error}")
sys.exit(1)
print(f" PASS: Obtained access token ({len(access_token)} chars)")
# Step 4: Check lichen login page shows OIDC/SSO option
print("Step 4: Checking Lichen login page has SSO option ...")
status, _ = http_get(f"{lichen_url}/login/")
if status != 200:
print(f" FAIL: Login page returned HTTP {status}")
sys.exit(1)
# Fetch the raw HTML to check for OIDC link
import urllib.request
req = urllib.request.Request(f"{lichen_url}/login/")
with urllib.request.urlopen(req, timeout=10) as resp:
html = resp.read().decode()
if "/oidc/start" in html:
print(" PASS: Login page contains /oidc/start link")
else:
print(" FAIL: Login page does not contain /oidc/start link")
print(" Make sure compose.oidc.yml is enabled and lichen is redeployed")
sys.exit(1)
# Step 5: Check /oidc/start redirects to Keycloak
print("Step 5: Checking /oidc/start redirects to Keycloak ...")
req = urllib.request.Request(f"{lichen_url}/oidc/start")
try:
with urllib.request.urlopen(req, timeout=10) as resp:
# Shouldn't reach here — expect redirect
print(f" WARN: Got HTTP {resp.getcode()} instead of redirect")
except urllib.error.HTTPError as e:
if 300 <= e.code < 400:
location = e.headers.get("Location", "")
if kc_domain in location:
print(f" PASS: Redirects to Keycloak ({e.code})")
else:
print(f" FAIL: Redirect location doesn't point to Keycloak: {location}")
sys.exit(1)
else:
print(f" FAIL: Expected redirect, got HTTP {e.code}")
sys.exit(1)
except Exception as e:
# urllib follows redirects by default, so check if we ended up at keycloak
pass
# For urllib which follows redirects, let's use a non-following approach
import http.client
import ssl
ctx = ssl.create_default_context()
conn = http.client.HTTPSConnection(lichen_domain, context=ctx)
conn.request("GET", "/oidc/start")
resp = conn.getresponse()
if 300 <= resp.status < 400:
location = resp.getheader("Location", "")
if kc_domain in location or "realms" in location:
print(f" PASS: /oidc/start redirects to Keycloak (HTTP {resp.status})")
else:
print(f" FAIL: Redirect doesn't point to Keycloak: {location}")
sys.exit(1)
elif resp.status == 200:
# urllib may have followed the redirect to Keycloak login page
print(f" PASS: /oidc/start returned 200 (redirect was followed)")
else:
print(f" FAIL: /oidc/start returned HTTP {resp.status}")
sys.exit(1)
conn.close()
print()
print("PASS: Lichen OIDC integration test passed")
if __name__ == '__main__':
main()