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).
216 lines
7.4 KiB
Python
Executable File
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()
|