Files
recipe-maintainer/utils/setup_cryptpad_sso.py
autonomic-bot f283a371bb 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).
2026-06-16 20:18:24 +00:00

216 lines
7.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Configure Authentik as the OIDC provider for CryptPad SSO.
Requires both Authentik and CryptPad to already be deployed via abra.
Ensures the OAuth2 provider/application exist in Authentik, updates the
CryptPad abra .env file, and inserts the client secret as a Docker secret.
No pip dependencies -- stdlib only.
Usage:
python3 utils/setup_cryptpad_sso.py \
--authentik-domain auth.example.com \
--authentik-token <admin-api-token> \
--cryptpad-domain pad.example.com
"""
import argparse
import os
import re
import subprocess
import sys
# Allow importing authentik_client from the same directory
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from authentik_client import AuthentikClient
def parse_args():
p = argparse.ArgumentParser(
description="Set up Authentik OIDC integration for CryptPad SSO"
)
p.add_argument("--authentik-domain", required=True,
help="Authentik domain (e.g. auth.example.com)")
p.add_argument("--authentik-token", required=True,
help="Authentik admin API token")
p.add_argument("--cryptpad-domain", required=True,
help="CryptPad domain (e.g. pad.example.com)")
p.add_argument("--client-id", default="cryptpad",
help="OAuth2 client ID (default: cryptpad)")
p.add_argument("--app-slug", default="cryptpad",
help="Authentik application slug (default: cryptpad)")
p.add_argument("--test-user", default="testuser",
help="Test user username (default: testuser)")
p.add_argument("--test-pass", default="testpass123",
help="Test user password (default: testpass123)")
p.add_argument("--test-email", default="testuser@test.example.com",
help="Test user email")
p.add_argument("--no-test-user", action="store_true",
help="Skip test user creation")
return p.parse_args()
def find_env_file(cryptpad_domain):
"""Locate the abra .env file for the CryptPad instance.
Searches ~/.abra/servers/*/cryptpad_domain.env
"""
abra_servers = os.path.expanduser("~/.abra/servers")
if not os.path.isdir(abra_servers):
return None
for server in os.listdir(abra_servers):
candidate = os.path.join(abra_servers, server, f"{cryptpad_domain}.env")
if os.path.isfile(candidate):
return candidate
return None
def update_env_file(env_path, oidc_url, client_id):
"""Update CryptPad env file with SSO settings."""
with open(env_path, "r") as f:
content = f.read()
replacements = {
"COMPOSE_FILE": "compose.yml:compose.sso.yml",
"SSO_ENABLED": "true",
"SSO_PROVIDER_NAME": "Authentik",
"SSO_OIDC_URL": oidc_url,
"SSO_CLIENT_ID": client_id,
"SSO_CLIENT_SECRET_VERSION": "v1",
}
for key, value in replacements.items():
pattern = rf"^{re.escape(key)}=.*$"
replacement = f"{key}={value}"
if re.search(pattern, content, re.MULTILINE):
content = re.sub(pattern, replacement, content, flags=re.MULTILINE)
else:
content = content.rstrip("\n") + f"\n{replacement}\n"
print(f" Set {key}={value}")
# Remove old SSO_CLIENT_SECRET env var if present (now a Docker secret)
if re.search(r"^SSO_CLIENT_SECRET=", content, re.MULTILINE):
content = re.sub(
r"^SSO_CLIENT_SECRET=.*\n?", "", content, flags=re.MULTILINE
)
print(" Removed old SSO_CLIENT_SECRET env var (now a Docker secret)")
with open(env_path, "w") as f:
f.write(content)
def insert_docker_secret(cryptpad_domain, client_secret):
"""Insert client secret as a Docker secret via abra."""
cmd = [
"abra", "app", "secret", "insert",
cryptpad_domain, "sso_client_s", "v1", client_secret,
"--chaos", "--no-input",
]
print(f" Running: abra app secret insert {cryptpad_domain} sso_client_s v1 ***")
# abra secret insert needs a TTY wrapper
wrapped = f'script -qefc "{" ".join(cmd)}" /dev/null'
result = subprocess.run(
wrapped, shell=True, capture_output=True, text=True
)
if result.returncode != 0:
# Secret may already exist -- check stderr
output = result.stdout + result.stderr
if "already exists" in output.lower() or "secret already" in output.lower():
print(" Secret already exists, skipping insert")
else:
print(f" WARNING: secret insert returned {result.returncode}")
if output.strip():
print(f" Output: {output.strip()}")
else:
print(" Inserted sso_client_s v1")
def main():
args = parse_args()
ak_url = f"https://{args.authentik_domain}"
cpad_url = f"https://{args.cryptpad_domain}"
client = AuthentikClient(ak_url, args.authentik_token)
# Step 1: Resolve UUIDs
print("=== Resolving Authentik UUIDs ===")
uuids = client.resolve_uuids()
print()
# Step 2: Create OAuth2 provider
print(f"=== Ensuring OAuth2 provider '{args.client_id}' ===")
redirect_uris = [
{"matching_mode": "strict", "url": f"{cpad_url}/ssoauth"},
]
provider_pk, client_secret = client.ensure_oauth2_provider(
name=args.client_id,
client_id=args.client_id,
redirect_uris=redirect_uris,
uuids=uuids,
)
print(f" Client ID: {args.client_id}")
print(f" Client secret: {client_secret}")
print()
# Step 3: Create application
print(f"=== Ensuring application '{args.app_slug}' ===")
client.ensure_application(
name="CryptPad",
slug=args.app_slug,
provider_pk=provider_pk,
launch_url=cpad_url,
)
print()
# Step 4: Optionally create test user
app_password = None
if not args.no_test_user:
print(f"=== Creating test user '{args.test_user}' ===")
user_pk = client.ensure_test_user(
args.test_user, args.test_email, args.test_pass
)
app_password = client.ensure_app_password(
user_pk, f"{args.test_user}-app-password"
)
print()
# Step 5: Update CryptPad env file
print("=== Updating CryptPad env file ===")
oidc_url = f"{ak_url}/application/o/{args.app_slug}"
env_path = find_env_file(args.cryptpad_domain)
if env_path is None:
print(f" ERROR: CryptPad abra app not found for {args.cryptpad_domain}")
print(" The app must exist before running this script.")
print(" Create it first with: abra app new cryptpad ...")
sys.exit(1)
print(f" Found env file: {env_path}")
update_env_file(env_path, oidc_url, args.client_id)
print()
# Step 6: Insert Docker secret
print("=== Inserting Docker secret ===")
insert_docker_secret(args.cryptpad_domain, client_secret)
# Summary
print()
print("=== Setup complete ===")
print()
print(f" Authentik domain: {args.authentik_domain}")
print(f" CryptPad domain: {args.cryptpad_domain}")
print(f" Client ID: {args.client_id}")
print(f" App slug: {args.app_slug}")
print(f" OIDC URL: {oidc_url}")
if app_password:
print(f" Test user: {args.test_user}")
print(f" App password: {app_password[:10]}...")
print()
print("Redeploy CryptPad to activate SSO:")
print(f" abra app deploy {args.cryptpad_domain} --chaos --force --no-input")
if __name__ == "__main__":
main()