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
+8
View File
@@ -0,0 +1,8 @@
name = "lasuite-docs"
[dependencies]
requires = ["keycloak"]
[sso]
provider = "keycloak"
setup_script = "setup/sso_integration.py"
+42
View File
@@ -0,0 +1,42 @@
# La Suite Docs — First-Time Setup
## Prerequisites
- DNS: `lasuite-docs.<domain_suffix>` must resolve to the server
- **Keycloak** must be deployed and running (dependency)
## Steps
1. **Create the app:**
```bash
abra app new lasuite-docs --server <SERVER> --domain lasuite-docs.<DOMAIN_SUFFIX> --no-input
```
2. **Generate secrets:**
```bash
abra app secret generate lasuite-docs.<DOMAIN_SUFFIX> --all -m --no-input
```
Save output to `recipe-info/testsecrets/lasuite-docs.<DOMAIN_SUFFIX>`.
3. **Deploy:**
```bash
abra app deploy lasuite-docs.<DOMAIN_SUFFIX> --chaos --force --no-input
```
4. **Keycloak SSO integration:**
```bash
python3 recipe-info/lasuite-docs/setup_keycloak_integration.py
```
This creates a `lasuite-docs` realm, OIDC client, and test user in Keycloak. It also inserts the client secret and updates the env file.
5. **Redeploy with SSO config:**
```bash
abra app deploy lasuite-docs.<DOMAIN_SUFFIX> --chaos --force --no-input
```
6. **Verify:** curl `https://lasuite-docs.<DOMAIN_SUFFIX>` returns HTTP 200.
## Notes
- Credentials are saved to `recipe-info/lasuite-docs/keycloak-test-credentials.<DOMAIN_SUFFIX>.toml`.
- OIDC test user: `testuser` / `testpass123`.
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Setup Keycloak OIDC integration for La Suite Docs.
Creates a Keycloak realm, OIDC client, and test user, then inserts
the client secret and updates the Docs env file with OIDC settings.
"""
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 = "lasuite-docs"
CLIENT_ID = "docs"
TEST_USER = "testuser"
TEST_PASS = "testpass123"
TEST_EMAIL = f"{TEST_USER}@test.example.com"
def main():
inst = load_default_instance()
docs_domain = inst.default_domain("lasuite-docs")
kc_domain = inst.default_domain("keycloak")
# Get Keycloak admin credentials from synced secrets
kc_secrets = load_secrets(kc_domain)
kc_admin_pass = kc_secrets["admin_password"]
# Try the configured admin username first; fall back to temp-admin.
# temp-admin is created when kc.sh bootstrap-admin user is run for recovery
# and may be the only working admin if the original admin password changed.
kc_url = f"https://{kc_domain}"
for username in ("admin", "temp-admin"):
kc = KeycloakAdmin(kc_url, username, kc_admin_pass)
try:
kc.get_admin_token()
print(f"Authenticated as '{username}'", flush=True)
break
except Exception:
print(f"Login as '{username}' failed, trying next...", flush=True)
else:
print("ERROR: Could not authenticate with Keycloak admin API", flush=True)
sys.exit(1)
# 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://{docs_domain}/*"],
web_origins=[f"https://{docs_domain}"],
)
# Step 3: Create test user
kc.ensure_user(REALM, TEST_USER, TEST_EMAIL, TEST_PASS)
# Step 4: Insert client secret via abra
print("=== Insert OIDC client secret into Docs ===", flush=True)
env_path = get_abra_env_path(inst.server, docs_domain)
env_data = read_env_file(env_path)
current_version = env_data.get("SECRET_OIDC_RPCS_VERSION", "v1")
next_num = int(current_version.lstrip("v")) + 1
next_version = f"v{next_num}"
print(f" Current secret version: {current_version}", flush=True)
print(f" Inserting as: {next_version}", flush=True)
app_secret_insert(docs_domain, "oidc_rpcs", next_version, client_secret)
# Step 5: Update Docs env with OIDC settings
print("=== Update Docs OIDC settings in env file ===", flush=True)
apply_env_overrides(env_path, {
"SECRET_OIDC_RPCS_VERSION": next_version,
"OIDC_REALM": REALM,
"AUTH_DOMAIN": kc_domain,
"OIDC_RP_CLIENT_ID": CLIENT_ID,
})
# 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 lasuite-docs 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'\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 Docs: abra app deploy {docs_domain} --chaos --force --no-input", flush=True)
print(f" 2. Run migrations: script -qefc 'abra app cmd {docs_domain} backend migrate --no-input' /dev/null", flush=True)
print(f" 3. Run OIDC test: python3 recipe-info/lasuite-docs/tests/oidc_login.py", flush=True)
if __name__ == "__main__":
main()
+85
View File
@@ -0,0 +1,85 @@
# La Suite Docs Tests
## Requires
- keycloak
## Target
- **URL:** https://lasuite-docs.<DOMAIN_SUFFIX>
- **Keycloak:** https://keycloak.<DOMAIN_SUFFIX> (realm: `lasuite-docs`)
## Prerequisites
Keycloak (`keycloak.<DOMAIN_SUFFIX>`) must be deployed before testing lasuite-docs. The OIDC login test and any manual authentication testing depend on it. If Keycloak is not running, deploy it first with `/recipe-deploy keycloak`.
## Automated Checks
Run the scripts in `tests/` to perform automated testing:
- `tests/health_check.py` — Confirms the instance is reachable and returns HTTP 200.
- `tests/oidc_login.py` — Tests the full OIDC authentication flow end-to-end:
1. Verifies Docs' `/api/v1.0/authenticate/` redirects to Keycloak
2. Obtains an access token from Keycloak via direct access grant (password flow)
3. Calls Docs' `/api/v1.0/users/me/` with the token and verifies the correct user is returned
This test reads credentials from `keycloak-test-credentials.<DOMAIN_SUFFIX>.toml`.
## Keycloak OIDC Integration
La Suite Docs **requires** an OIDC provider. The test instance uses Keycloak at `keycloak.<DOMAIN_SUFFIX>`.
### Setup
Run `setup_keycloak_integration.py` to configure everything automatically. The script:
1. Creates a `lasuite-docs` realm in Keycloak
2. Creates a `docs` OIDC client (confidential, standard flow + direct access grants)
3. Creates a test user (`testuser` / `testpass123`)
4. Inserts the OIDC client secret into the Docs app via `abra app secret insert`
5. Updates the Docs env file with `OIDC_REALM`, `AUTH_DOMAIN`, `OIDC_RP_CLIENT_ID`
6. Writes all credentials to `keycloak-test-credentials.<DOMAIN_SUFFIX>.toml`
After running the setup script, redeploy Docs:
```
abra app deploy lasuite-docs.<DOMAIN_SUFFIX> --chaos --force --no-input
```
The script is idempotent — it skips resources that already exist and resets the test user password.
### Credentials
All Keycloak credentials are stored in `keycloak-test-credentials.<DOMAIN_SUFFIX>.toml` (sourceable):
| Variable | Description |
|----------|-------------|
| `KC_ADMIN_USER` / `KC_ADMIN_PASS` | Keycloak admin (master realm) |
| `KC_REALM` | Keycloak realm name (`lasuite-docs`) |
| `KC_CLIENT_ID` / `KC_CLIENT_SECRET` | OIDC client ID and secret |
| `KC_TEST_USER` / `KC_TEST_PASS` | Test user credentials |
| `KC_TEST_EMAIL` | Test user email |
### Key Endpoints
| Endpoint | Purpose |
|----------|---------|
| `https://lasuite-docs.<DOMAIN_SUFFIX>/api/v1.0/authenticate/` | Initiates OIDC login (302 redirect to Keycloak) |
| `https://lasuite-docs.<DOMAIN_SUFFIX>/api/v1.0/callback/` | OIDC callback (Keycloak redirects here after login) |
| `https://keycloak.<DOMAIN_SUFFIX>/realms/lasuite-docs/protocol/openid-connect/token` | Keycloak token endpoint |
## Post-Deploy Steps
After deploying Docs for the first time:
1. **Keycloak integration:** `python3 recipe-info/lasuite-docs/setup_keycloak_integration.py` then redeploy
Migrations and Minio buckets are created automatically on startup — no manual steps needed.
## Manual Verification
1. Open https://lasuite-docs.<DOMAIN_SUFFIX> in a browser.
2. Confirm the La Suite Docs landing page loads without errors.
3. Click "Login" and verify the OIDC redirect to Keycloak works.
4. Log in with test credentials (`testuser` / `testpass123`).
5. After logging in, verify you can create and edit a document.
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""Health check for La Suite Docs."""
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('lasuite-docs')
url = f"https://{domain}"
print(f"Checking La Suite Docs at {url} ...")
status, _ = http_get(url)
if status == 200:
print(f"PASS: La Suite Docs returned HTTP {status}")
else:
print(f"FAIL: La Suite Docs returned HTTP {status} (expected 200)")
sys.exit(1)
if __name__ == '__main__':
main()
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""OIDC integration test for La Suite Docs + Keycloak."""
import argparse
import os
import sys
import urllib.request
import urllib.error
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():
parser = argparse.ArgumentParser()
parser.add_argument('--domain', default=os.environ.get('TEST_DOMAIN'))
args = parser.parse_args()
recipe_dir = os.path.join(os.path.dirname(__file__), '..')
creds = load_toml_credentials(recipe_dir, 'keycloak')
if creds is None:
print("FAIL: Credentials file not found: keycloak-test-credentials.<domain_suffix>.toml")
print("Run setup_keycloak_integration.py first.")
sys.exit(1)
docs_domain = args.domain or resolve_domain('lasuite-docs')
kc_domain = resolve_domain('keycloak')
docs_url = f"https://{docs_domain}"
kc_url = f"https://{kc_domain}"
print("Testing OIDC integration: La Suite Docs <-> Keycloak")
print()
# Step 1: Verify Docs redirects to Keycloak
print("Step 1: Checking Docs OIDC redirect ...")
try:
req = urllib.request.Request(f"{docs_url}/api/v1.0/authenticate/")
opener = urllib.request.build_opener(NoRedirectHandler())
resp = opener.open(req, timeout=15)
redirect_url = resp.headers.get('Location', '')
except urllib.error.HTTPError as e:
redirect_url = e.headers.get('Location', '') if e.headers else ''
expected_prefix = f"{kc_url}/realms/{creds['kc_realm']}/protocol/openid-connect/auth"
if expected_prefix in (redirect_url or ''):
print(f" PASS: Docs redirects to Keycloak realm '{creds['kc_realm']}'")
else:
print(f" FAIL: Expected redirect to Keycloak, got: {redirect_url}")
sys.exit(1)
# Step 2: Obtain token from Keycloak
print("Step 2: 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",
},
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 from Keycloak: {error}")
sys.exit(1)
print(f" PASS: Obtained access token from Keycloak ({len(access_token)} chars)")
# Step 3: Use token to access Docs API
print("Step 3: Accessing Docs API with Keycloak token ...")
status, body = http_get(
f"{docs_url}/api/v1.0/users/me/",
headers={"Authorization": f"Bearer {access_token}"},
)
if status != 200:
print(f" FAIL: Docs API returned HTTP {status} (expected 200)")
sys.exit(1)
user_email = (body or {}).get("email", "")
expected_email = f"{creds['kc_test_user']}@test.example.com"
if user_email == expected_email:
print(f" PASS: Docs API returned user with email '{user_email}'")
else:
print(f" FAIL: Expected email '{expected_email}', got '{user_email}'")
sys.exit(1)
print()
print("PASS: OIDC integration test passed — Docs authenticates via Keycloak")
class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
raise urllib.error.HTTPError(newurl, code, msg, headers, fp)
if __name__ == '__main__':
main()
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""Upload + conversion test for La Suite Docs.
Verifies the end-to-end conversion path:
- `.md` upload → backend → y-provider (yjs)
- `.docx` upload → backend → docspec (BlockNote JSON) → y-provider (yjs)
Exercises CONVERSION_UPLOAD_ENABLED on the backend, backend→y-provider auth
(Y_PROVIDER_API_KEY_FILE) and routing (Y_PROVIDER_API_BASE_URL), and
DOCSPEC_API_URL for the .docx import path.
"""
import argparse
import io
import json
import os
import sys
import urllib.error
import urllib.request
import uuid
import zipfile
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
from utils.tests.helpers import (
http_post, load_toml_credentials, resolve_domain,
)
def build_minimal_docx() -> bytes:
"""Build a minimal valid .docx (OOXML) in memory."""
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as z:
z.writestr('[Content_Types].xml',
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
'<Default Extension="xml" ContentType="application/xml"/>'
'<Override PartName="/word/document.xml" '
'ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>'
'</Types>')
z.writestr('_rels/.rels',
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'<Relationship Id="rId1" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" '
'Target="word/document.xml"/>'
'</Relationships>')
z.writestr('word/document.xml',
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">'
'<w:body><w:p><w:r><w:t>Upload conversion test</w:t></w:r></w:p></w:body>'
'</w:document>')
return buf.getvalue()
def get_oidc_token(kc_url, creds):
"""Direct grant flow against the lasuite-docs realm."""
status, data = http_post(
f"{kc_url}/realms/{creds['kc_realm']}/protocol/openid-connect/token",
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",
},
content_type="application/x-www-form-urlencoded",
)
return (data or {}).get("access_token", "")
def multipart_upload(url, token, filename, content_type, body):
"""POST a file via multipart/form-data with a Bearer token."""
boundary = uuid.uuid4().hex
parts = [
f"--{boundary}\r\n".encode(),
b'Content-Disposition: form-data; name="title"\r\n\r\n',
b"upload-conversion-test\r\n",
f"--{boundary}\r\n".encode(),
f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode(),
f"Content-Type: {content_type}\r\n\r\n".encode(),
body,
f"\r\n--{boundary}--\r\n".encode(),
]
req_body = b"".join(parts)
req = urllib.request.Request(
url,
data=req_body,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": f"multipart/form-data; boundary={boundary}",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return resp.status, resp.read().decode()
except urllib.error.HTTPError as e:
return e.code, e.read().decode()
def upload_step(label, docs_url, token, filename, content_type, body):
print(f"Step: Upload {label} ...")
status, raw = multipart_upload(
f"{docs_url}/api/v1.0/documents/", token, filename, content_type, body,
)
if status != 201:
print(f" FAIL: {label} upload returned HTTP {status}: {raw[:200]}")
sys.exit(1)
try:
doc = json.loads(raw)
except json.JSONDecodeError:
print(f" FAIL: {label} response not JSON: {raw[:200]}")
sys.exit(1)
doc_id = doc.get('id')
if not doc_id:
print(f" FAIL: {label} response missing 'id': {raw[:200]}")
sys.exit(1)
print(f" PASS: {label} converted and persisted (document id {doc_id})")
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--domain', default=os.environ.get('TEST_DOMAIN'))
args = parser.parse_args()
recipe_dir = os.path.join(os.path.dirname(__file__), '..')
creds = load_toml_credentials(recipe_dir, 'keycloak')
if creds is None:
print("FAIL: Credentials file not found: keycloak-test-credentials.<domain_suffix>.toml")
print("Run setup_keycloak_integration.py first.")
sys.exit(1)
docs_domain = args.domain or resolve_domain('lasuite-docs')
kc_domain = resolve_domain('keycloak')
docs_url = f"https://{docs_domain}"
kc_url = f"https://{kc_domain}"
print("Testing file upload + conversion (CONVERSION_UPLOAD_ENABLED + Y_PROVIDER)")
print()
print("Step 1: Obtaining access token from Keycloak ...")
token = get_oidc_token(kc_url, creds)
if not token:
print(" FAIL: Could not obtain access token from Keycloak")
sys.exit(1)
print(f" PASS: Obtained access token ({len(token)} chars)")
upload_step(
".md file", docs_url, token,
"upload-test.md", "text/markdown",
b"# Upload Conversion Test\n\nValidating **CONVERSION_UPLOAD_ENABLED** end-to-end.\n",
)
upload_step(
".docx file", docs_url, token,
"upload-test.docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
build_minimal_docx(),
)
print()
print("PASS: Upload + conversion working end-to-end (.md and .docx)")
if __name__ == '__main__':
main()
+20
View File
@@ -0,0 +1,20 @@
# La Suite Docs Upstream
## Main Project
- **Repository:** https://github.com/suitenumerique/docs
- **Releases:** https://github.com/suitenumerique/docs/releases
## Images
| Service | Image | Release Notes |
|---------|-------|---------------|
| app | `lasuite/impress-frontend` | https://github.com/suitenumerique/docs/releases |
| backend | `lasuite/impress-backend` | https://github.com/suitenumerique/docs/releases |
| celery | `lasuite/impress-backend` | https://github.com/suitenumerique/docs/releases |
| y-provider | `lasuite/impress-y-provider` | https://github.com/suitenumerique/docs/releases |
| db | `pgautoupgrade/pgautoupgrade` | https://github.com/pgautoupgrade/docker-pgautoupgrade/releases |
| redis | `redis` | https://github.com/redis/redis/releases |
| minio | `minio/minio` | https://github.com/minio/minio/releases |
| minio-bootstrap | `minio/mc` | https://github.com/minio/mc/releases |
| web | `nginx` | https://nginx.org/en/CHANGES |