Compare commits
No commits in common. "2af7618f5265471f4048db49eb1353924cf322f6" and "7d3e7a6ec56e1739526235a05d2c16857fc4cdfa" have entirely different histories.
2af7618f52
...
7d3e7a6ec5
13 changed files with 3 additions and 1207 deletions
|
|
@ -10,8 +10,6 @@ template_slug="${AUTHENTIK_TAILSCALE_TEMPLATE_SLUG:-ts}"
|
||||||
client_id="${AUTHENTIK_TAILSCALE_CLIENT_ID:-tailscale.burrow.net}"
|
client_id="${AUTHENTIK_TAILSCALE_CLIENT_ID:-tailscale.burrow.net}"
|
||||||
client_secret="${AUTHENTIK_TAILSCALE_CLIENT_SECRET:-}"
|
client_secret="${AUTHENTIK_TAILSCALE_CLIENT_SECRET:-}"
|
||||||
launch_url="${AUTHENTIK_TAILSCALE_LAUNCH_URL:-https://login.tailscale.com/start/oidc}"
|
launch_url="${AUTHENTIK_TAILSCALE_LAUNCH_URL:-https://login.tailscale.com/start/oidc}"
|
||||||
access_group="${AUTHENTIK_TAILSCALE_ACCESS_GROUP:-}"
|
|
||||||
default_external_application_slug="${AUTHENTIK_DEFAULT_EXTERNAL_APPLICATION_SLUG:-}"
|
|
||||||
redirect_uris_json="${AUTHENTIK_TAILSCALE_REDIRECT_URIS_JSON:-[
|
redirect_uris_json="${AUTHENTIK_TAILSCALE_REDIRECT_URIS_JSON:-[
|
||||||
\"https://login.tailscale.com/a/oauth_response\"
|
\"https://login.tailscale.com/a/oauth_response\"
|
||||||
]}"
|
]}"
|
||||||
|
|
@ -33,8 +31,6 @@ Optional environment:
|
||||||
AUTHENTIK_TAILSCALE_CLIENT_ID
|
AUTHENTIK_TAILSCALE_CLIENT_ID
|
||||||
AUTHENTIK_TAILSCALE_LAUNCH_URL
|
AUTHENTIK_TAILSCALE_LAUNCH_URL
|
||||||
AUTHENTIK_TAILSCALE_REDIRECT_URIS_JSON
|
AUTHENTIK_TAILSCALE_REDIRECT_URIS_JSON
|
||||||
AUTHENTIK_TAILSCALE_ACCESS_GROUP
|
|
||||||
AUTHENTIK_DEFAULT_EXTERNAL_APPLICATION_SLUG
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,111 +123,6 @@ wait_for_authentik() {
|
||||||
|
|
||||||
wait_for_authentik
|
wait_for_authentik
|
||||||
|
|
||||||
lookup_group_pk() {
|
|
||||||
local group_name="$1"
|
|
||||||
|
|
||||||
api GET "/api/v3/core/groups/?page_size=200" \
|
|
||||||
| jq -r --arg group_name "$group_name" '.results[]? | select(.name == $group_name) | .pk // empty' \
|
|
||||||
| head -n1
|
|
||||||
}
|
|
||||||
|
|
||||||
lookup_application_pk() {
|
|
||||||
local slug="$1"
|
|
||||||
local application_pk lookup_result lookup_status
|
|
||||||
|
|
||||||
application_pk="$(
|
|
||||||
api GET "/api/v3/core/applications/?page_size=200" \
|
|
||||||
| jq -r --arg slug "$slug" '.results[]? | select(.slug == $slug) | .pk // empty' \
|
|
||||||
| head -n1
|
|
||||||
)"
|
|
||||||
|
|
||||||
if [[ -n "$application_pk" ]]; then
|
|
||||||
printf '%s\n' "$application_pk"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
lookup_result="$(api_with_status GET "/api/v3/core/applications/${slug}/")"
|
|
||||||
lookup_status="$(printf '%s\n' "$lookup_result" | sed -n '1p')"
|
|
||||||
if [[ "$lookup_status" =~ ^20[01]$ ]]; then
|
|
||||||
printf '%s\n' "$lookup_result" | sed '1d' | jq -r '.pk // empty'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_application_group_binding() {
|
|
||||||
local application_slug="$1"
|
|
||||||
local group_name="$2"
|
|
||||||
local application_pk group_pk existing payload binding_pk
|
|
||||||
|
|
||||||
application_pk="$(lookup_application_pk "$application_slug")"
|
|
||||||
if [[ -z "$application_pk" ]]; then
|
|
||||||
echo "warning: could not resolve Authentik application ${application_slug}; skipping application group binding" >&2
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
group_pk="$(lookup_group_pk "$group_name")"
|
|
||||||
if [[ -z "$group_pk" ]]; then
|
|
||||||
echo "error: could not resolve Authentik group ${group_name}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
existing="$(
|
|
||||||
api GET "/api/v3/policies/bindings/?page_size=200&target=${application_pk}" \
|
|
||||||
| jq -c --arg group_pk "$group_pk" '.results[]? | select(.group == $group_pk)' \
|
|
||||||
| head -n1
|
|
||||||
)"
|
|
||||||
|
|
||||||
payload="$(
|
|
||||||
jq -cn \
|
|
||||||
--arg target "$application_pk" \
|
|
||||||
--arg group "$group_pk" \
|
|
||||||
'{
|
|
||||||
group: $group,
|
|
||||||
target: $target,
|
|
||||||
negate: false,
|
|
||||||
enabled: true,
|
|
||||||
order: 100,
|
|
||||||
timeout: 30,
|
|
||||||
failure_result: false
|
|
||||||
}'
|
|
||||||
)"
|
|
||||||
|
|
||||||
if [[ -n "$existing" ]]; then
|
|
||||||
binding_pk="$(printf '%s\n' "$existing" | jq -r '.pk')"
|
|
||||||
api PATCH "/api/v3/policies/bindings/${binding_pk}/" "$payload" >/dev/null
|
|
||||||
else
|
|
||||||
api POST "/api/v3/policies/bindings/" "$payload" >/dev/null
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_default_external_application() {
|
|
||||||
local application_slug="$1"
|
|
||||||
local application_pk default_brand brand_payload
|
|
||||||
|
|
||||||
application_pk="$(lookup_application_pk "$application_slug")"
|
|
||||||
if [[ -z "$application_pk" ]]; then
|
|
||||||
echo "error: could not resolve Authentik application ${application_slug} for brand default application" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
default_brand="$(
|
|
||||||
api GET "/api/v3/core/brands/?page_size=200" \
|
|
||||||
| jq -c '.results[]? | select(.default == true)' \
|
|
||||||
| head -n1
|
|
||||||
)"
|
|
||||||
|
|
||||||
if [[ -z "$default_brand" ]]; then
|
|
||||||
echo "warning: could not resolve the default Authentik brand; skipping external default application" >&2
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
brand_payload="$(
|
|
||||||
printf '%s\n' "$default_brand" \
|
|
||||||
| jq --arg application_pk "$application_pk" '.default_application = $application_pk'
|
|
||||||
)"
|
|
||||||
|
|
||||||
api PUT "/api/v3/core/brands/$(printf '%s\n' "$default_brand" | jq -r '.brand_uuid')/" "$brand_payload" >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
template_provider="$(
|
template_provider="$(
|
||||||
api GET "/api/v3/providers/oauth2/?page_size=200" \
|
api GET "/api/v3/providers/oauth2/?page_size=200" \
|
||||||
| jq -c --arg template_slug "$template_slug" '.results[]? | select(.assigned_application_slug == $template_slug)' \
|
| jq -c --arg template_slug "$template_slug" '.results[]? | select(.assigned_application_slug == $template_slug)' \
|
||||||
|
|
@ -322,7 +213,6 @@ existing_application="$(
|
||||||
|
|
||||||
if [[ -n "$existing_application" ]]; then
|
if [[ -n "$existing_application" ]]; then
|
||||||
application_pk="$(printf '%s\n' "$existing_application" | jq -r '.pk')"
|
application_pk="$(printf '%s\n' "$existing_application" | jq -r '.pk')"
|
||||||
api PATCH "/api/v3/core/applications/${application_pk}/" "$application_payload" >/dev/null
|
|
||||||
else
|
else
|
||||||
create_application_result="$(
|
create_application_result="$(
|
||||||
api_with_status POST "/api/v3/core/applications/" "$application_payload"
|
api_with_status POST "/api/v3/core/applications/" "$application_payload"
|
||||||
|
|
@ -349,14 +239,6 @@ if [[ -z "${application_pk:-}" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$access_group" ]]; then
|
|
||||||
ensure_application_group_binding "$application_slug" "$access_group"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$default_external_application_slug" ]]; then
|
|
||||||
ensure_default_external_application "$default_external_application_slug"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for _ in $(seq 1 30); do
|
for _ in $(seq 1 30); do
|
||||||
if curl -fsS "${authentik_url}/application/o/${application_slug}/.well-known/openid-configuration" >/dev/null 2>&1; then
|
if curl -fsS "${authentik_url}/application/o/${application_slug}/.well-known/openid-configuration" >/dev/null 2>&1; then
|
||||||
echo "Synced Authentik Tailscale OIDC application ${application_slug} (${application_name})."
|
echo "Synced Authentik Tailscale OIDC application ${application_slug} (${application_name})."
|
||||||
|
|
|
||||||
|
|
@ -1,398 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
authentik_url="${AUTHENTIK_URL:-https://auth.burrow.net}"
|
|
||||||
bootstrap_token="${AUTHENTIK_BOOTSTRAP_TOKEN:-}"
|
|
||||||
application_slug="${AUTHENTIK_ZULIP_APPLICATION_SLUG:-zulip}"
|
|
||||||
application_name="${AUTHENTIK_ZULIP_APPLICATION_NAME:-Zulip}"
|
|
||||||
provider_name="${AUTHENTIK_ZULIP_PROVIDER_NAME:-Zulip}"
|
|
||||||
acs_url="${AUTHENTIK_ZULIP_ACS_URL:-https://chat.burrow.net/complete/saml/}"
|
|
||||||
audience="${AUTHENTIK_ZULIP_AUDIENCE:-https://chat.burrow.net}"
|
|
||||||
launch_url="${AUTHENTIK_ZULIP_LAUNCH_URL:-https://chat.burrow.net/}"
|
|
||||||
access_group="${AUTHENTIK_ZULIP_ACCESS_GROUP:-}"
|
|
||||||
issuer="${AUTHENTIK_ZULIP_ISSUER:-$authentik_url}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: Scripts/authentik-sync-zulip-saml.sh
|
|
||||||
|
|
||||||
Required environment:
|
|
||||||
AUTHENTIK_BOOTSTRAP_TOKEN
|
|
||||||
|
|
||||||
Optional environment:
|
|
||||||
AUTHENTIK_URL
|
|
||||||
AUTHENTIK_ZULIP_APPLICATION_SLUG
|
|
||||||
AUTHENTIK_ZULIP_APPLICATION_NAME
|
|
||||||
AUTHENTIK_ZULIP_PROVIDER_NAME
|
|
||||||
AUTHENTIK_ZULIP_ACS_URL
|
|
||||||
AUTHENTIK_ZULIP_AUDIENCE
|
|
||||||
AUTHENTIK_ZULIP_LAUNCH_URL
|
|
||||||
AUTHENTIK_ZULIP_ACCESS_GROUP
|
|
||||||
AUTHENTIK_ZULIP_ISSUER
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$bootstrap_token" ]]; then
|
|
||||||
echo "error: AUTHENTIK_BOOTSTRAP_TOKEN is required" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
api() {
|
|
||||||
local method="$1"
|
|
||||||
local path="$2"
|
|
||||||
local data="${3:-}"
|
|
||||||
|
|
||||||
if [[ -n "$data" ]]; then
|
|
||||||
curl -fsS \
|
|
||||||
-X "$method" \
|
|
||||||
-H "Authorization: Bearer ${bootstrap_token}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$data" \
|
|
||||||
"${authentik_url}${path}"
|
|
||||||
else
|
|
||||||
curl -fsS \
|
|
||||||
-X "$method" \
|
|
||||||
-H "Authorization: Bearer ${bootstrap_token}" \
|
|
||||||
"${authentik_url}${path}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
api_with_status() {
|
|
||||||
local method="$1"
|
|
||||||
local path="$2"
|
|
||||||
local data="${3:-}"
|
|
||||||
local response_file status
|
|
||||||
|
|
||||||
response_file="$(mktemp)"
|
|
||||||
trap 'rm -f "$response_file"' RETURN
|
|
||||||
|
|
||||||
if [[ -n "$data" ]]; then
|
|
||||||
status="$(
|
|
||||||
curl -sS \
|
|
||||||
-o "$response_file" \
|
|
||||||
-w '%{http_code}' \
|
|
||||||
-X "$method" \
|
|
||||||
-H "Authorization: Bearer ${bootstrap_token}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$data" \
|
|
||||||
"${authentik_url}${path}"
|
|
||||||
)"
|
|
||||||
else
|
|
||||||
status="$(
|
|
||||||
curl -sS \
|
|
||||||
-o "$response_file" \
|
|
||||||
-w '%{http_code}' \
|
|
||||||
-X "$method" \
|
|
||||||
-H "Authorization: Bearer ${bootstrap_token}" \
|
|
||||||
"${authentik_url}${path}"
|
|
||||||
)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s\n' "$status"
|
|
||||||
cat "$response_file"
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_authentik() {
|
|
||||||
for _ in $(seq 1 90); do
|
|
||||||
if curl -fsS "${authentik_url}/-/health/ready/" >/dev/null 2>&1; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "error: Authentik did not become ready at ${authentik_url}" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
lookup_oauth_template_field() {
|
|
||||||
local field="$1"
|
|
||||||
|
|
||||||
api GET "/api/v3/providers/oauth2/?page_size=200" \
|
|
||||||
| jq -r --arg field "$field" '.results[]? | select(.assigned_application_slug == "ts") | .[$field]' \
|
|
||||||
| head -n1
|
|
||||||
}
|
|
||||||
|
|
||||||
lookup_group_pk() {
|
|
||||||
local group_name="$1"
|
|
||||||
|
|
||||||
api GET "/api/v3/core/groups/?page_size=200" \
|
|
||||||
| jq -r --arg group_name "$group_name" '.results[]? | select(.name == $group_name) | .pk // empty' \
|
|
||||||
| head -n1
|
|
||||||
}
|
|
||||||
|
|
||||||
lookup_application_pk() {
|
|
||||||
local slug="$1"
|
|
||||||
|
|
||||||
api GET "/api/v3/core/applications/?page_size=200" \
|
|
||||||
| jq -r --arg slug "$slug" '.results[]? | select(.slug == $slug) | .pk // empty' \
|
|
||||||
| head -n1
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_application_group_binding() {
|
|
||||||
local application_slug="$1"
|
|
||||||
local group_name="$2"
|
|
||||||
local application_pk group_pk existing payload binding_pk
|
|
||||||
|
|
||||||
application_pk="$(lookup_application_pk "$application_slug")"
|
|
||||||
if [[ -z "$application_pk" ]]; then
|
|
||||||
echo "warning: could not resolve Authentik application ${application_slug}; skipping application group binding" >&2
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
group_pk="$(lookup_group_pk "$group_name")"
|
|
||||||
if [[ -z "$group_pk" ]]; then
|
|
||||||
echo "error: could not resolve Authentik group ${group_name}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
existing="$(
|
|
||||||
api GET "/api/v3/policies/bindings/?page_size=200&target=${application_pk}" \
|
|
||||||
| jq -c --arg group_pk "$group_pk" '.results[]? | select(.group == $group_pk)' \
|
|
||||||
| head -n1
|
|
||||||
)"
|
|
||||||
|
|
||||||
payload="$(
|
|
||||||
jq -cn \
|
|
||||||
--arg target "$application_pk" \
|
|
||||||
--arg group "$group_pk" \
|
|
||||||
'{
|
|
||||||
group: $group,
|
|
||||||
target: $target,
|
|
||||||
negate: false,
|
|
||||||
enabled: true,
|
|
||||||
order: 100,
|
|
||||||
timeout: 30,
|
|
||||||
failure_result: false
|
|
||||||
}'
|
|
||||||
)"
|
|
||||||
|
|
||||||
if [[ -n "$existing" ]]; then
|
|
||||||
binding_pk="$(printf '%s\n' "$existing" | jq -r '.pk')"
|
|
||||||
api PATCH "/api/v3/policies/bindings/${binding_pk}/" "$payload" >/dev/null
|
|
||||||
else
|
|
||||||
api POST "/api/v3/policies/bindings/" "$payload" >/dev/null
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
reconcile_property_mapping() {
|
|
||||||
local name="$1"
|
|
||||||
local saml_name="$2"
|
|
||||||
local friendly_name="$3"
|
|
||||||
local expression="$4"
|
|
||||||
local payload existing_pk
|
|
||||||
|
|
||||||
payload="$(
|
|
||||||
jq -n \
|
|
||||||
--arg name "$name" \
|
|
||||||
--arg saml_name "$saml_name" \
|
|
||||||
--arg friendly_name "$friendly_name" \
|
|
||||||
--arg expression "$expression" \
|
|
||||||
'{
|
|
||||||
name: $name,
|
|
||||||
saml_name: $saml_name,
|
|
||||||
friendly_name: $friendly_name,
|
|
||||||
expression: $expression
|
|
||||||
}'
|
|
||||||
)"
|
|
||||||
|
|
||||||
existing_pk="$(
|
|
||||||
api GET "/api/v3/propertymappings/provider/saml/?page_size=200" \
|
|
||||||
| jq -r --arg name "$name" '.results[]? | select(.name == $name) | .pk' \
|
|
||||||
| head -n1
|
|
||||||
)"
|
|
||||||
|
|
||||||
if [[ -n "$existing_pk" ]]; then
|
|
||||||
api PATCH "/api/v3/propertymappings/provider/saml/${existing_pk}/" "$payload" >/dev/null
|
|
||||||
printf '%s\n' "$existing_pk"
|
|
||||||
else
|
|
||||||
api POST "/api/v3/propertymappings/provider/saml/" "$payload" | jq -r '.pk // empty'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_authentik
|
|
||||||
|
|
||||||
authorization_flow="$(lookup_oauth_template_field authorization_flow)"
|
|
||||||
invalidation_flow="$(lookup_oauth_template_field invalidation_flow)"
|
|
||||||
signing_kp="$(lookup_oauth_template_field signing_key)"
|
|
||||||
|
|
||||||
if [[ -z "$authorization_flow" || -z "$invalidation_flow" || -z "$signing_kp" ]]; then
|
|
||||||
echo "error: could not resolve Authentik provider defaults from Burrow Tailnet template" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
email_mapping_pk="$(
|
|
||||||
reconcile_property_mapping \
|
|
||||||
"Burrow Zulip SAML Email" \
|
|
||||||
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" \
|
|
||||||
"email" \
|
|
||||||
'return request.user.email'
|
|
||||||
)"
|
|
||||||
|
|
||||||
name_mapping_pk="$(
|
|
||||||
reconcile_property_mapping \
|
|
||||||
"Burrow Zulip SAML Name" \
|
|
||||||
"name" \
|
|
||||||
"name" \
|
|
||||||
'return request.user.name or request.user.username'
|
|
||||||
)"
|
|
||||||
|
|
||||||
first_name_mapping_pk="$(
|
|
||||||
reconcile_property_mapping \
|
|
||||||
"Burrow Zulip SAML First Name" \
|
|
||||||
"firstName" \
|
|
||||||
"firstName" \
|
|
||||||
$'parts = (request.user.name or "").split(" ", 1)\nif len(parts) > 0 and parts[0]:\n return parts[0]\nreturn request.user.username'
|
|
||||||
)"
|
|
||||||
|
|
||||||
last_name_mapping_pk="$(
|
|
||||||
reconcile_property_mapping \
|
|
||||||
"Burrow Zulip SAML Last Name" \
|
|
||||||
"lastName" \
|
|
||||||
"lastName" \
|
|
||||||
$'parts = (request.user.name or "").rsplit(" ", 1)\nif len(parts) == 2 and parts[1]:\n return parts[1]\nreturn request.user.username'
|
|
||||||
)"
|
|
||||||
|
|
||||||
if [[ -z "$email_mapping_pk" || -z "$name_mapping_pk" || -z "$first_name_mapping_pk" || -z "$last_name_mapping_pk" ]]; then
|
|
||||||
echo "error: failed to reconcile Zulip SAML property mappings" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
provider_payload="$(
|
|
||||||
jq -n \
|
|
||||||
--arg name "$provider_name" \
|
|
||||||
--arg authorization_flow "$authorization_flow" \
|
|
||||||
--arg invalidation_flow "$invalidation_flow" \
|
|
||||||
--arg acs_url "$acs_url" \
|
|
||||||
--arg audience "$audience" \
|
|
||||||
--arg issuer "$issuer" \
|
|
||||||
--arg signing_kp "$signing_kp" \
|
|
||||||
--arg name_id_mapping "$email_mapping_pk" \
|
|
||||||
--arg email_mapping "$email_mapping_pk" \
|
|
||||||
--arg name_mapping "$name_mapping_pk" \
|
|
||||||
--arg first_name_mapping "$first_name_mapping_pk" \
|
|
||||||
--arg last_name_mapping "$last_name_mapping_pk" \
|
|
||||||
'{
|
|
||||||
name: $name,
|
|
||||||
authorization_flow: $authorization_flow,
|
|
||||||
invalidation_flow: $invalidation_flow,
|
|
||||||
acs_url: $acs_url,
|
|
||||||
audience: $audience,
|
|
||||||
issuer: $issuer,
|
|
||||||
signing_kp: $signing_kp,
|
|
||||||
sign_assertion: true,
|
|
||||||
sign_response: true,
|
|
||||||
sp_binding: "post",
|
|
||||||
name_id_mapping: $name_id_mapping,
|
|
||||||
property_mappings: [
|
|
||||||
$email_mapping,
|
|
||||||
$name_mapping,
|
|
||||||
$first_name_mapping,
|
|
||||||
$last_name_mapping
|
|
||||||
]
|
|
||||||
}'
|
|
||||||
)"
|
|
||||||
|
|
||||||
existing_provider="$(
|
|
||||||
api GET "/api/v3/providers/saml/?page_size=200" \
|
|
||||||
| jq -c \
|
|
||||||
--arg application_slug "$application_slug" \
|
|
||||||
--arg provider_name "$provider_name" \
|
|
||||||
'.results[]? | select(.assigned_application_slug == $application_slug or .name == $provider_name)' \
|
|
||||||
| head -n1
|
|
||||||
)"
|
|
||||||
|
|
||||||
if [[ -n "$existing_provider" ]]; then
|
|
||||||
provider_pk="$(printf '%s\n' "$existing_provider" | jq -r '.pk')"
|
|
||||||
api PATCH "/api/v3/providers/saml/${provider_pk}/" "$provider_payload" >/dev/null
|
|
||||||
else
|
|
||||||
provider_pk="$(
|
|
||||||
api POST "/api/v3/providers/saml/" "$provider_payload" \
|
|
||||||
| jq -r '.pk // empty'
|
|
||||||
)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "${provider_pk:-}" ]]; then
|
|
||||||
echo "error: Zulip SAML provider did not return a primary key" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
application_payload="$(
|
|
||||||
jq -n \
|
|
||||||
--arg name "$application_name" \
|
|
||||||
--arg slug "$application_slug" \
|
|
||||||
--arg provider "$provider_pk" \
|
|
||||||
--arg launch_url "$launch_url" \
|
|
||||||
'{
|
|
||||||
name: $name,
|
|
||||||
slug: $slug,
|
|
||||||
provider: ($provider | tonumber),
|
|
||||||
meta_launch_url: $launch_url,
|
|
||||||
open_in_new_tab: true,
|
|
||||||
policy_engine_mode: "any"
|
|
||||||
}'
|
|
||||||
)"
|
|
||||||
|
|
||||||
existing_application="$(
|
|
||||||
api GET "/api/v3/core/applications/?page_size=200" \
|
|
||||||
| jq -c --arg slug "$application_slug" '.results[]? | select(.slug == $slug)' \
|
|
||||||
| head -n1
|
|
||||||
)"
|
|
||||||
|
|
||||||
if [[ -n "$existing_application" ]]; then
|
|
||||||
application_pk="$(printf '%s\n' "$existing_application" | jq -r '.pk')"
|
|
||||||
api PATCH "/api/v3/core/applications/${application_pk}/" "$application_payload" >/dev/null
|
|
||||||
else
|
|
||||||
create_application_result="$(
|
|
||||||
api_with_status POST "/api/v3/core/applications/" "$application_payload"
|
|
||||||
)"
|
|
||||||
create_application_status="$(printf '%s\n' "$create_application_result" | sed -n '1p')"
|
|
||||||
create_application_body="$(printf '%s\n' "$create_application_result" | sed '1d')"
|
|
||||||
|
|
||||||
if [[ "$create_application_status" =~ ^20[01]$ ]]; then
|
|
||||||
application_pk="$(printf '%s\n' "$create_application_body" | jq -r '.pk // empty')"
|
|
||||||
elif [[ "$create_application_status" == "400" ]] && printf '%s\n' "$create_application_body" | jq -e '
|
|
||||||
(.slug // [] | index("Application with this slug already exists.")) != null
|
|
||||||
or (.provider // [] | index("Application with this provider already exists.")) != null
|
|
||||||
' >/dev/null; then
|
|
||||||
application_pk="existing-duplicate"
|
|
||||||
else
|
|
||||||
printf '%s\n' "$create_application_body" >&2
|
|
||||||
echo "error: could not reconcile Authentik application ${application_slug}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "${application_pk:-}" ]]; then
|
|
||||||
echo "error: Zulip SAML application did not return a primary key" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$access_group" ]]; then
|
|
||||||
ensure_application_group_binding "$application_slug" "$access_group"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for _ in $(seq 1 30); do
|
|
||||||
metadata_status="$(
|
|
||||||
curl -sS \
|
|
||||||
-o /dev/null \
|
|
||||||
-w '%{http_code}' \
|
|
||||||
--max-redirs 0 \
|
|
||||||
"${authentik_url}/application/saml/${application_slug}/metadata/" \
|
|
||||||
|| true
|
|
||||||
)"
|
|
||||||
case "$metadata_status" in
|
|
||||||
200|301|302|307|308)
|
|
||||||
echo "Synced Authentik Zulip SAML application ${application_slug} (${application_name})."
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "warning: Zulip SAML metadata for ${application_slug} was not immediately readable; keeping reconciled config." >&2
|
|
||||||
echo "Synced Authentik Zulip SAML application ${application_slug} (${application_name})."
|
|
||||||
|
|
@ -49,10 +49,6 @@ across vendor-native Google auth flows when Burrow already operates an IdP.
|
||||||
- Add a Burrow-managed Zulip workload on the forge host at `chat.burrow.net`.
|
- Add a Burrow-managed Zulip workload on the forge host at `chat.burrow.net`.
|
||||||
The deployment should be repo-owned and rebuildable from Nix, even if the
|
The deployment should be repo-owned and rebuildable from Nix, even if the
|
||||||
runtime uses vendor-supported container images internally.
|
runtime uses vendor-supported container images internally.
|
||||||
- Prefer host-managed NixOS services for Zulip's stateful dependencies
|
|
||||||
(PostgreSQL, Redis, RabbitMQ, memcached, backups) so Burrow owns the
|
|
||||||
operational surface directly rather than composing a container-side service
|
|
||||||
mesh.
|
|
||||||
- Zulip should authenticate through Authentik SAML rather than local passwords
|
- Zulip should authenticate through Authentik SAML rather than local passwords
|
||||||
as the primary path. Initial bootstrap may still keep an operational escape
|
as the primary path. Initial bootstrap may still keep an operational escape
|
||||||
hatch while the deployment is being validated.
|
hatch while the deployment is being validated.
|
||||||
|
|
@ -72,9 +68,6 @@ across vendor-native Google auth flows when Burrow already operates an IdP.
|
||||||
options instead of hand-edited UI state.
|
options instead of hand-edited UI state.
|
||||||
- Prefer service-specific reconciliation over ad hoc manual setup so rebuilds
|
- Prefer service-specific reconciliation over ad hoc manual setup so rebuilds
|
||||||
and host replacement converge automatically.
|
and host replacement converge automatically.
|
||||||
- When Burrow wants an external-user launcher surface in Authentik, configure
|
|
||||||
the brand's `default_application` explicitly instead of relying on
|
|
||||||
`/if/user/`, which otherwise remains internal-user-only.
|
|
||||||
- Derive Linear SCIM role groups from Burrow's canonical identity metadata.
|
- Derive Linear SCIM role groups from Burrow's canonical identity metadata.
|
||||||
If Burrow-wide admin intent says a user is an operator/admin, the repo-owned
|
If Burrow-wide admin intent says a user is an operator/admin, the repo-owned
|
||||||
configuration should map that intent onto the Linear push group without a
|
configuration should map that intent onto the Linear push group without a
|
||||||
|
|
@ -120,8 +113,6 @@ across vendor-native Google auth flows when Burrow already operates an IdP.
|
||||||
- Authentik exposes working metadata for Zulip and Linear
|
- Authentik exposes working metadata for Zulip and Linear
|
||||||
- Authentik exposes a working OIDC issuer for 1Password
|
- Authentik exposes a working OIDC issuer for 1Password
|
||||||
- users in Burrow admin groups receive the expected access on first login
|
- users in Burrow admin groups receive the expected access on first login
|
||||||
- external Burrow users landing on `auth.burrow.net` reach the intended
|
|
||||||
app launcher target instead of the internal-only Authentik user interface
|
|
||||||
- Record concrete evidence for:
|
- Record concrete evidence for:
|
||||||
- host deployment generation
|
- host deployment generation
|
||||||
- Authentik reconciliation success
|
- Authentik reconciliation success
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,6 @@
|
||||||
nixosModules.burrow-forgejo-nsc = nsc-autoscaler.nixosModules.default;
|
nixosModules.burrow-forgejo-nsc = nsc-autoscaler.nixosModules.default;
|
||||||
nixosModules.burrow-authentik = import ./nixos/modules/burrow-authentik.nix;
|
nixosModules.burrow-authentik = import ./nixos/modules/burrow-authentik.nix;
|
||||||
nixosModules.burrow-headscale = import ./nixos/modules/burrow-headscale.nix;
|
nixosModules.burrow-headscale = import ./nixos/modules/burrow-headscale.nix;
|
||||||
nixosModules.burrow-zulip = import ./nixos/modules/burrow-zulip.nix;
|
|
||||||
nixosConfigurations.burrow-forge = nixpkgs.lib.nixosSystem {
|
nixosConfigurations.burrow-forge = nixpkgs.lib.nixosSystem {
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
specialArgs = {
|
specialArgs = {
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ in
|
||||||
self.nixosModules.burrow-forgejo-nsc
|
self.nixosModules.burrow-forgejo-nsc
|
||||||
self.nixosModules.burrow-authentik
|
self.nixosModules.burrow-authentik
|
||||||
self.nixosModules.burrow-headscale
|
self.nixosModules.burrow-headscale
|
||||||
self.nixosModules.burrow-zulip
|
|
||||||
];
|
];
|
||||||
|
|
||||||
system.stateVersion = "24.11";
|
system.stateVersion = "24.11";
|
||||||
|
|
@ -163,37 +162,9 @@ in
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
};
|
};
|
||||||
|
|
||||||
age.secrets.burrowZulipPostgresPassword = {
|
|
||||||
file = ../../../secrets/infra/zulip-postgres-password.age;
|
|
||||||
owner = "root";
|
|
||||||
group = "root";
|
|
||||||
mode = "0400";
|
|
||||||
};
|
|
||||||
|
|
||||||
age.secrets.burrowZulipRabbitmqPassword = {
|
|
||||||
file = ../../../secrets/infra/zulip-rabbitmq-password.age;
|
|
||||||
owner = "root";
|
|
||||||
group = "root";
|
|
||||||
mode = "0400";
|
|
||||||
};
|
|
||||||
|
|
||||||
age.secrets.burrowZulipRedisPassword = {
|
|
||||||
file = ../../../secrets/infra/zulip-redis-password.age;
|
|
||||||
owner = "root";
|
|
||||||
group = "root";
|
|
||||||
mode = "0400";
|
|
||||||
};
|
|
||||||
|
|
||||||
age.secrets.burrowZulipSecretKey = {
|
|
||||||
file = ../../../secrets/infra/zulip-secret-key.age;
|
|
||||||
owner = "root";
|
|
||||||
group = "root";
|
|
||||||
mode = "0400";
|
|
||||||
};
|
|
||||||
|
|
||||||
networking.extraHosts = ''
|
networking.extraHosts = ''
|
||||||
127.0.0.1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net chat.burrow.net nsc-autoscaler.burrow.net
|
127.0.0.1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net nsc-autoscaler.burrow.net
|
||||||
::1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net chat.burrow.net nsc-autoscaler.burrow.net
|
::1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net nsc-autoscaler.burrow.net
|
||||||
'';
|
'';
|
||||||
|
|
||||||
services.burrow.forge = {
|
services.burrow.forge = {
|
||||||
|
|
@ -237,14 +208,12 @@ in
|
||||||
forgejoClientSecretFile = config.age.secrets.burrowForgejoOidcClientSecret.path;
|
forgejoClientSecretFile = config.age.secrets.burrowForgejoOidcClientSecret.path;
|
||||||
headscaleClientSecretFile = config.age.secrets.burrowHeadscaleOidcClientSecret.path;
|
headscaleClientSecretFile = config.age.secrets.burrowHeadscaleOidcClientSecret.path;
|
||||||
tailscaleClientSecretFile = config.age.secrets.burrowTailscaleOidcClientSecret.path;
|
tailscaleClientSecretFile = config.age.secrets.burrowTailscaleOidcClientSecret.path;
|
||||||
defaultExternalApplicationSlug = "tailscale";
|
|
||||||
googleClientIDFile = config.age.secrets.burrowAuthentikGoogleClientId.path;
|
googleClientIDFile = config.age.secrets.burrowAuthentikGoogleClientId.path;
|
||||||
googleClientSecretFile = config.age.secrets.burrowAuthentikGoogleClientSecret.path;
|
googleClientSecretFile = config.age.secrets.burrowAuthentikGoogleClientSecret.path;
|
||||||
googleAccountMapFile = config.age.secrets.burrowAuthentikGoogleAccountMap.path;
|
googleAccountMapFile = config.age.secrets.burrowAuthentikGoogleAccountMap.path;
|
||||||
googleLoginMode = "redirect";
|
googleLoginMode = "redirect";
|
||||||
userGroupName = contributors.groups.users;
|
userGroupName = contributors.groups.users;
|
||||||
adminGroupName = contributors.groups.admins;
|
adminGroupName = contributors.groups.admins;
|
||||||
tailscaleAccessGroupName = contributors.groups.users;
|
|
||||||
bootstrapUsers = bootstrapUsers;
|
bootstrapUsers = bootstrapUsers;
|
||||||
linearAcsUrl = "https://api.linear.app/auth/sso/d0ca13dc-ac41-4824-8aab-e0ca352fc3de/acs";
|
linearAcsUrl = "https://api.linear.app/auth/sso/d0ca13dc-ac41-4824-8aab-e0ca352fc3de/acs";
|
||||||
linearAudience = "https://auth.linear.app/sso/d0ca13dc-ac41-4824-8aab-e0ca352fc3de";
|
linearAudience = "https://auth.linear.app/sso/d0ca13dc-ac41-4824-8aab-e0ca352fc3de";
|
||||||
|
|
@ -255,7 +224,6 @@ in
|
||||||
linearOwnerGroupName = linearGroups.owners;
|
linearOwnerGroupName = linearGroups.owners;
|
||||||
linearAdminGroupName = linearGroups.admins;
|
linearAdminGroupName = linearGroups.admins;
|
||||||
linearGuestGroupName = linearGroups.guests;
|
linearGuestGroupName = linearGroups.guests;
|
||||||
zulipAccessGroupName = contributors.groups.users;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
services.burrow.headscale = {
|
services.burrow.headscale = {
|
||||||
|
|
@ -263,13 +231,4 @@ in
|
||||||
oidcClientSecretFile = config.age.secrets.burrowHeadscaleOidcClientSecret.path;
|
oidcClientSecretFile = config.age.secrets.burrowHeadscaleOidcClientSecret.path;
|
||||||
bootstrapUsers = headscaleBootstrapUsers;
|
bootstrapUsers = headscaleBootstrapUsers;
|
||||||
};
|
};
|
||||||
|
|
||||||
services.burrow.zulip = {
|
|
||||||
enable = true;
|
|
||||||
administratorEmail = identities.contact.canonicalEmail;
|
|
||||||
postgresPasswordFile = config.age.secrets.burrowZulipPostgresPassword.path;
|
|
||||||
rabbitmqPasswordFile = config.age.secrets.burrowZulipRabbitmqPassword.path;
|
|
||||||
redisPasswordFile = config.age.secrets.burrowZulipRedisPassword.path;
|
|
||||||
secretKeyFile = config.age.secrets.burrowZulipSecretKey.path;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ let
|
||||||
forgejoOidcSyncScript = ../../Scripts/authentik-sync-forgejo-oidc.sh;
|
forgejoOidcSyncScript = ../../Scripts/authentik-sync-forgejo-oidc.sh;
|
||||||
tailscaleOidcSyncScript = ../../Scripts/authentik-sync-tailscale-oidc.sh;
|
tailscaleOidcSyncScript = ../../Scripts/authentik-sync-tailscale-oidc.sh;
|
||||||
onePasswordOidcSyncScript = ../../Scripts/authentik-sync-1password-oidc.sh;
|
onePasswordOidcSyncScript = ../../Scripts/authentik-sync-1password-oidc.sh;
|
||||||
zulipSamlSyncScript = ../../Scripts/authentik-sync-zulip-saml.sh;
|
|
||||||
linearSamlSyncScript = ../../Scripts/authentik-sync-linear-saml.sh;
|
linearSamlSyncScript = ../../Scripts/authentik-sync-linear-saml.sh;
|
||||||
linearScimSyncScript = ../../Scripts/authentik-sync-linear-scim.sh;
|
linearScimSyncScript = ../../Scripts/authentik-sync-linear-scim.sh;
|
||||||
googleSourceSyncScript = ../../Scripts/authentik-sync-google-source.sh;
|
googleSourceSyncScript = ../../Scripts/authentik-sync-google-source.sh;
|
||||||
|
|
@ -154,18 +153,6 @@ in
|
||||||
description = "Host-local file containing the Authentik Tailscale OIDC client secret.";
|
description = "Host-local file containing the Authentik Tailscale OIDC client secret.";
|
||||||
};
|
};
|
||||||
|
|
||||||
tailscaleAccessGroupName = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
description = "Authentik group that should be allowed to launch the Tailscale application.";
|
|
||||||
};
|
|
||||||
|
|
||||||
defaultExternalApplicationSlug = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
description = "Authentik application slug that external users should land on instead of /if/user/.";
|
|
||||||
};
|
|
||||||
|
|
||||||
onePasswordDomain = lib.mkOption {
|
onePasswordDomain = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "burrow-team.1password.com";
|
default = "burrow-team.1password.com";
|
||||||
|
|
@ -199,42 +186,6 @@ in
|
||||||
description = "Authentik application slug for Linear SAML.";
|
description = "Authentik application slug for Linear SAML.";
|
||||||
};
|
};
|
||||||
|
|
||||||
zulipDomain = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "chat.burrow.net";
|
|
||||||
description = "Public Zulip domain exposed through Authentik SAML.";
|
|
||||||
};
|
|
||||||
|
|
||||||
zulipProviderSlug = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "zulip";
|
|
||||||
description = "Authentik application slug for Zulip SAML.";
|
|
||||||
};
|
|
||||||
|
|
||||||
zulipAcsUrl = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "https://${config.services.burrow.authentik.zulipDomain}/complete/saml/";
|
|
||||||
description = "Zulip SAML ACS URL.";
|
|
||||||
};
|
|
||||||
|
|
||||||
zulipAudience = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "https://${config.services.burrow.authentik.zulipDomain}";
|
|
||||||
description = "Zulip SAML audience/entity identifier.";
|
|
||||||
};
|
|
||||||
|
|
||||||
zulipLaunchUrl = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "https://${config.services.burrow.authentik.zulipDomain}/";
|
|
||||||
description = "Zulip URL exposed in Authentik.";
|
|
||||||
};
|
|
||||||
|
|
||||||
zulipAccessGroupName = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
description = "Authentik group allowed to launch Zulip from Burrow SSO surfaces.";
|
|
||||||
};
|
|
||||||
|
|
||||||
linearAcsUrl = lib.mkOption {
|
linearAcsUrl = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = null;
|
||||||
|
|
@ -858,12 +809,6 @@ EOF
|
||||||
export AUTHENTIK_TAILSCALE_CLIENT_SECRET="$(tr -d '\r\n' < ${lib.escapeShellArg cfg.tailscaleClientSecretFile})"
|
export AUTHENTIK_TAILSCALE_CLIENT_SECRET="$(tr -d '\r\n' < ${lib.escapeShellArg cfg.tailscaleClientSecretFile})"
|
||||||
export AUTHENTIK_TAILSCALE_LAUNCH_URL=https://login.tailscale.com/start/oidc
|
export AUTHENTIK_TAILSCALE_LAUNCH_URL=https://login.tailscale.com/start/oidc
|
||||||
export AUTHENTIK_TAILSCALE_REDIRECT_URIS_JSON='["https://login.tailscale.com/a/oauth_response"]'
|
export AUTHENTIK_TAILSCALE_REDIRECT_URIS_JSON='["https://login.tailscale.com/a/oauth_response"]'
|
||||||
${lib.optionalString (cfg.tailscaleAccessGroupName != null) ''
|
|
||||||
export AUTHENTIK_TAILSCALE_ACCESS_GROUP=${lib.escapeShellArg cfg.tailscaleAccessGroupName}
|
|
||||||
''}
|
|
||||||
${lib.optionalString (cfg.defaultExternalApplicationSlug != null) ''
|
|
||||||
export AUTHENTIK_DEFAULT_EXTERNAL_APPLICATION_SLUG=${lib.escapeShellArg cfg.defaultExternalApplicationSlug}
|
|
||||||
''}
|
|
||||||
|
|
||||||
${pkgs.bash}/bin/bash ${tailscaleOidcSyncScript}
|
${pkgs.bash}/bin/bash ${tailscaleOidcSyncScript}
|
||||||
'';
|
'';
|
||||||
|
|
@ -914,53 +859,6 @@ EOF
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.burrow-authentik-zulip-saml = {
|
|
||||||
description = "Reconcile the Burrow Authentik Zulip SAML application";
|
|
||||||
after = [
|
|
||||||
"burrow-authentik-ready.service"
|
|
||||||
"network-online.target"
|
|
||||||
];
|
|
||||||
wants = [
|
|
||||||
"burrow-authentik-ready.service"
|
|
||||||
"network-online.target"
|
|
||||||
];
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
restartTriggers = [
|
|
||||||
zulipSamlSyncScript
|
|
||||||
cfg.envFile
|
|
||||||
];
|
|
||||||
path = [
|
|
||||||
pkgs.bash
|
|
||||||
pkgs.coreutils
|
|
||||||
pkgs.curl
|
|
||||||
pkgs.jq
|
|
||||||
];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
User = "root";
|
|
||||||
Group = "root";
|
|
||||||
};
|
|
||||||
script = ''
|
|
||||||
set -euo pipefail
|
|
||||||
set -a
|
|
||||||
source ${lib.escapeShellArg cfg.envFile}
|
|
||||||
set +a
|
|
||||||
|
|
||||||
export AUTHENTIK_URL=https://${cfg.domain}
|
|
||||||
export AUTHENTIK_ZULIP_APPLICATION_SLUG=${lib.escapeShellArg cfg.zulipProviderSlug}
|
|
||||||
export AUTHENTIK_ZULIP_APPLICATION_NAME=Zulip
|
|
||||||
export AUTHENTIK_ZULIP_PROVIDER_NAME=Zulip
|
|
||||||
export AUTHENTIK_ZULIP_ACS_URL=${lib.escapeShellArg cfg.zulipAcsUrl}
|
|
||||||
export AUTHENTIK_ZULIP_AUDIENCE=${lib.escapeShellArg cfg.zulipAudience}
|
|
||||||
export AUTHENTIK_ZULIP_LAUNCH_URL=${lib.escapeShellArg cfg.zulipLaunchUrl}
|
|
||||||
${lib.optionalString (cfg.zulipAccessGroupName != null) ''
|
|
||||||
export AUTHENTIK_ZULIP_ACCESS_GROUP=${lib.escapeShellArg cfg.zulipAccessGroupName}
|
|
||||||
''}
|
|
||||||
|
|
||||||
${pkgs.bash}/bin/bash ${zulipSamlSyncScript}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.burrow-authentik-linear-saml = lib.mkIf (
|
systemd.services.burrow-authentik-linear-saml = lib.mkIf (
|
||||||
cfg.linearAcsUrl != null && cfg.linearAudience != null
|
cfg.linearAcsUrl != null && cfg.linearAudience != null
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -1,486 +0,0 @@
|
||||||
{ config, lib, pkgs, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.services.burrow.zulip;
|
|
||||||
yamlFormat = pkgs.formats.yaml { };
|
|
||||||
composeFile = yamlFormat.generate "burrow-zulip-compose.yaml" {
|
|
||||||
services = {
|
|
||||||
zulip = {
|
|
||||||
image = "ghcr.io/zulip/zulip-server:11.6-1";
|
|
||||||
restart = "unless-stopped";
|
|
||||||
network_mode = "host";
|
|
||||||
secrets = [
|
|
||||||
"zulip__postgres_password"
|
|
||||||
"zulip__rabbitmq_password"
|
|
||||||
"zulip__redis_password"
|
|
||||||
"zulip__secret_key"
|
|
||||||
"zulip__email_password"
|
|
||||||
];
|
|
||||||
environment = {
|
|
||||||
SETTING_REMOTE_POSTGRES_HOST = "127.0.0.1";
|
|
||||||
SETTING_MEMCACHED_LOCATION = "127.0.0.1:11211";
|
|
||||||
SETTING_RABBITMQ_HOST = "127.0.0.1";
|
|
||||||
SETTING_REDIS_HOST = "127.0.0.1";
|
|
||||||
};
|
|
||||||
volumes = [ "${cfg.dataDir}/data:/data:rw" ];
|
|
||||||
ulimits.nofile = {
|
|
||||||
soft = 1000000;
|
|
||||||
hard = 1048576;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.services.burrow.zulip = {
|
|
||||||
enable = lib.mkEnableOption "the Burrow Zulip deployment";
|
|
||||||
|
|
||||||
domain = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "chat.burrow.net";
|
|
||||||
description = "Public Zulip domain.";
|
|
||||||
};
|
|
||||||
|
|
||||||
port = lib.mkOption {
|
|
||||||
type = lib.types.port;
|
|
||||||
default = 18090;
|
|
||||||
description = "Local loopback port Caddy should proxy to.";
|
|
||||||
};
|
|
||||||
|
|
||||||
dataDir = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "/var/lib/burrow/zulip";
|
|
||||||
description = "Host directory storing Zulip compose state and generated runtime files.";
|
|
||||||
};
|
|
||||||
|
|
||||||
administratorEmail = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "contact@burrow.net";
|
|
||||||
description = "Operational Zulip administrator email.";
|
|
||||||
};
|
|
||||||
|
|
||||||
realmName = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "Burrow";
|
|
||||||
description = "Initial Zulip organization name for single-tenant bootstrap.";
|
|
||||||
};
|
|
||||||
|
|
||||||
realmOwnerName = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "Burrow";
|
|
||||||
description = "Display name used for the initial Zulip organization owner.";
|
|
||||||
};
|
|
||||||
|
|
||||||
authentikDomain = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = config.services.burrow.authentik.domain;
|
|
||||||
description = "Authentik domain Zulip should trust as its SAML IdP.";
|
|
||||||
};
|
|
||||||
|
|
||||||
authentikProviderSlug = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = config.services.burrow.authentik.zulipProviderSlug;
|
|
||||||
description = "Authentik SAML application slug used for Zulip.";
|
|
||||||
};
|
|
||||||
|
|
||||||
postgresPasswordFile = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "File containing the Zulip PostgreSQL password.";
|
|
||||||
};
|
|
||||||
|
|
||||||
rabbitmqPasswordFile = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "File containing the Zulip RabbitMQ password.";
|
|
||||||
};
|
|
||||||
|
|
||||||
redisPasswordFile = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "File containing the Zulip Redis password.";
|
|
||||||
};
|
|
||||||
|
|
||||||
secretKeyFile = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "File containing the Zulip Django secret key.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
environment.systemPackages = [
|
|
||||||
pkgs.podman
|
|
||||||
pkgs.podman-compose
|
|
||||||
];
|
|
||||||
|
|
||||||
services.postgresql = {
|
|
||||||
ensureDatabases = [ "zulip" ];
|
|
||||||
ensureUsers = [
|
|
||||||
{
|
|
||||||
name = "zulip";
|
|
||||||
ensureDBOwnership = true;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
settings = {
|
|
||||||
listen_addresses = lib.mkDefault "127.0.0.1";
|
|
||||||
password_encryption = lib.mkDefault "scram-sha-256";
|
|
||||||
};
|
|
||||||
authentication = lib.mkAfter ''
|
|
||||||
host zulip zulip 127.0.0.1/32 scram-sha-256
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
services.postgresqlBackup = {
|
|
||||||
enable = true;
|
|
||||||
backupAll = false;
|
|
||||||
databases = [ "zulip" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
services.memcached = {
|
|
||||||
enable = true;
|
|
||||||
listen = "127.0.0.1";
|
|
||||||
port = 11211;
|
|
||||||
extraOptions = [ "-U 0" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
services.redis.servers.zulip = {
|
|
||||||
enable = true;
|
|
||||||
bind = "127.0.0.1";
|
|
||||||
port = 6379;
|
|
||||||
requirePassFile = cfg.redisPasswordFile;
|
|
||||||
};
|
|
||||||
|
|
||||||
services.rabbitmq = {
|
|
||||||
enable = true;
|
|
||||||
listenAddress = "127.0.0.1";
|
|
||||||
port = 5672;
|
|
||||||
};
|
|
||||||
|
|
||||||
services.caddy.virtualHosts."${cfg.domain}".extraConfig = ''
|
|
||||||
encode gzip zstd
|
|
||||||
reverse_proxy 127.0.0.1:${toString cfg.port}
|
|
||||||
'';
|
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
"d ${cfg.dataDir} 0755 root root - -"
|
|
||||||
"d ${cfg.dataDir}/data 0755 root root - -"
|
|
||||||
"d ${cfg.dataDir}/data/logs 0755 root root - -"
|
|
||||||
"d ${cfg.dataDir}/data/logs/emails 0755 root root - -"
|
|
||||||
"d ${cfg.dataDir}/data/secrets 0700 root root - -"
|
|
||||||
"d ${cfg.dataDir}/secrets 0700 root root - -"
|
|
||||||
"d ${cfg.dataDir}/logs 0755 root root - -"
|
|
||||||
];
|
|
||||||
|
|
||||||
systemd.services.burrow-zulip-postgres-bootstrap = {
|
|
||||||
description = "Bootstrap PostgreSQL role for Burrow Zulip";
|
|
||||||
after = [ "postgresql.service" ];
|
|
||||||
wants = [ "postgresql.service" ];
|
|
||||||
requiredBy = [ "burrow-zulip.service" ];
|
|
||||||
before = [ "burrow-zulip.service" ];
|
|
||||||
path = [
|
|
||||||
config.services.postgresql.package
|
|
||||||
pkgs.bash
|
|
||||||
pkgs.coreutils
|
|
||||||
pkgs.python3
|
|
||||||
pkgs.util-linux
|
|
||||||
];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
User = "root";
|
|
||||||
Group = "root";
|
|
||||||
};
|
|
||||||
script = ''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
db_password="$(tr -d '\r\n' < ${lib.escapeShellArg cfg.postgresPasswordFile})"
|
|
||||||
db_password_sql="$(printf '%s' "$db_password" | python3 -c "import sys; print(sys.stdin.read().replace(chr(39), chr(39) * 2), end=\"\")")"
|
|
||||||
setup_sql="$(mktemp)"
|
|
||||||
trap 'rm -f "$setup_sql"' EXIT
|
|
||||||
|
|
||||||
cat > "$setup_sql" <<SQL
|
|
||||||
DO \$\$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'zulip') THEN
|
|
||||||
CREATE ROLE zulip LOGIN;
|
|
||||||
END IF;
|
|
||||||
END
|
|
||||||
\$\$;
|
|
||||||
ALTER ROLE zulip WITH LOGIN PASSWORD '$db_password_sql';
|
|
||||||
SQL
|
|
||||||
chmod 0644 "$setup_sql"
|
|
||||||
|
|
||||||
${pkgs.util-linux}/bin/runuser -u postgres -- psql -v ON_ERROR_STOP=1 -f "$setup_sql"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.burrow-zulip-rabbitmq-bootstrap = {
|
|
||||||
description = "Bootstrap RabbitMQ user for Burrow Zulip";
|
|
||||||
after = [ "rabbitmq.service" ];
|
|
||||||
wants = [ "rabbitmq.service" ];
|
|
||||||
requiredBy = [ "burrow-zulip.service" ];
|
|
||||||
before = [ "burrow-zulip.service" ];
|
|
||||||
path = [
|
|
||||||
config.services.rabbitmq.package
|
|
||||||
pkgs.bash
|
|
||||||
pkgs.coreutils
|
|
||||||
pkgs.gawk
|
|
||||||
pkgs.gnugrep
|
|
||||||
];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
User = "root";
|
|
||||||
Group = "root";
|
|
||||||
};
|
|
||||||
script = ''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
rabbit_password="$(tr -d '\r\n' < ${lib.escapeShellArg cfg.rabbitmqPasswordFile})"
|
|
||||||
export HOME=${config.services.rabbitmq.dataDir}
|
|
||||||
|
|
||||||
rabbitmqctl await_startup
|
|
||||||
|
|
||||||
if rabbitmqctl list_users -q | awk '{ print $1 }' | grep -qx zulip; then
|
|
||||||
rabbitmqctl change_password zulip "$rabbit_password"
|
|
||||||
else
|
|
||||||
rabbitmqctl add_user zulip "$rabbit_password"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rabbitmqctl set_permissions -p / zulip '.*' '.*' '.*'
|
|
||||||
|
|
||||||
if rabbitmqctl list_users -q | awk '{ print $1 }' | grep -qx guest; then
|
|
||||||
rabbitmqctl delete_user guest
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.burrow-zulip-runtime = {
|
|
||||||
description = "Prepare Burrow Zulip compose and SAML runtime files";
|
|
||||||
after = [
|
|
||||||
"postgresql.service"
|
|
||||||
"redis-zulip.service"
|
|
||||||
"memcached.service"
|
|
||||||
"rabbitmq.service"
|
|
||||||
"burrow-zulip-postgres-bootstrap.service"
|
|
||||||
"burrow-zulip-rabbitmq-bootstrap.service"
|
|
||||||
"burrow-authentik-ready.service"
|
|
||||||
"burrow-authentik-zulip-saml.service"
|
|
||||||
"network-online.target"
|
|
||||||
];
|
|
||||||
wants = [
|
|
||||||
"postgresql.service"
|
|
||||||
"redis-zulip.service"
|
|
||||||
"memcached.service"
|
|
||||||
"rabbitmq.service"
|
|
||||||
"burrow-zulip-postgres-bootstrap.service"
|
|
||||||
"burrow-zulip-rabbitmq-bootstrap.service"
|
|
||||||
"burrow-authentik-ready.service"
|
|
||||||
"burrow-authentik-zulip-saml.service"
|
|
||||||
"network-online.target"
|
|
||||||
];
|
|
||||||
requiredBy = [ "burrow-zulip.service" ];
|
|
||||||
before = [ "burrow-zulip.service" ];
|
|
||||||
path = [
|
|
||||||
pkgs.bash
|
|
||||||
pkgs.coreutils
|
|
||||||
pkgs.curl
|
|
||||||
pkgs.python3
|
|
||||||
];
|
|
||||||
restartTriggers = [
|
|
||||||
composeFile
|
|
||||||
cfg.postgresPasswordFile
|
|
||||||
cfg.rabbitmqPasswordFile
|
|
||||||
cfg.redisPasswordFile
|
|
||||||
cfg.secretKeyFile
|
|
||||||
];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
User = "root";
|
|
||||||
Group = "root";
|
|
||||||
};
|
|
||||||
script = ''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
install -d -m 0755 ${lib.escapeShellArg cfg.dataDir}
|
|
||||||
install -d -m 0755 ${lib.escapeShellArg "${cfg.dataDir}/data"}
|
|
||||||
install -d -m 0755 ${lib.escapeShellArg "${cfg.dataDir}/data/logs"}
|
|
||||||
install -d -m 0755 ${lib.escapeShellArg "${cfg.dataDir}/data/logs/emails"}
|
|
||||||
install -d -m 0700 ${lib.escapeShellArg "${cfg.dataDir}/data/secrets"}
|
|
||||||
install -d -m 0700 ${lib.escapeShellArg "${cfg.dataDir}/secrets"}
|
|
||||||
install -d -m 0755 ${lib.escapeShellArg "${cfg.dataDir}/logs"}
|
|
||||||
install -m 0644 ${composeFile} ${lib.escapeShellArg "${cfg.dataDir}/compose.yaml"}
|
|
||||||
: > ${lib.escapeShellArg "${cfg.dataDir}/secrets/email-password"}
|
|
||||||
chmod 0600 ${lib.escapeShellArg "${cfg.dataDir}/secrets/email-password"}
|
|
||||||
|
|
||||||
metadata_xml="$(${pkgs.curl}/bin/curl -fsSL https://${cfg.authentikDomain}/application/saml/${cfg.authentikProviderSlug}/metadata/)"
|
|
||||||
saml_cert="$(printf '%s' "$metadata_xml" | ${pkgs.python3}/bin/python3 -c '
|
|
||||||
import xml.etree.ElementTree as ET, sys
|
|
||||||
xml = sys.stdin.read()
|
|
||||||
root = ET.fromstring(xml)
|
|
||||||
ns = {"ds": "http://www.w3.org/2000/09/xmldsig#"}
|
|
||||||
node = root.find(".//ds:X509Certificate", ns)
|
|
||||||
if node is None or not (node.text or "").strip():
|
|
||||||
raise SystemExit("missing X509 certificate in Authentik metadata")
|
|
||||||
print((node.text or "").strip())
|
|
||||||
')"
|
|
||||||
|
|
||||||
cat > ${lib.escapeShellArg "${cfg.dataDir}/compose.override.yaml"} <<EOF
|
|
||||||
secrets:
|
|
||||||
zulip__postgres_password:
|
|
||||||
file: ${cfg.postgresPasswordFile}
|
|
||||||
zulip__rabbitmq_password:
|
|
||||||
file: ${cfg.rabbitmqPasswordFile}
|
|
||||||
zulip__redis_password:
|
|
||||||
file: ${cfg.redisPasswordFile}
|
|
||||||
zulip__secret_key:
|
|
||||||
file: ${cfg.secretKeyFile}
|
|
||||||
zulip__email_password:
|
|
||||||
file: ${cfg.dataDir}/secrets/email-password
|
|
||||||
|
|
||||||
services:
|
|
||||||
zulip:
|
|
||||||
environment:
|
|
||||||
SETTING_EXTERNAL_HOST: "${cfg.domain}"
|
|
||||||
SETTING_ZULIP_ADMINISTRATOR: "${cfg.administratorEmail}"
|
|
||||||
TRUST_GATEWAY_IP: "True"
|
|
||||||
SETTING_SEND_LOGIN_EMAILS: "False"
|
|
||||||
ZULIP_AUTH_BACKENDS: "EmailAuthBackend,SAMLAuthBackend"
|
|
||||||
CONFIG_application_server__http_only: true
|
|
||||||
CONFIG_application_server__nginx_listen_port: ${toString cfg.port}
|
|
||||||
CONFIG_application_server__queue_workers_multiprocess: false
|
|
||||||
ZULIP_CUSTOM_SETTINGS: |
|
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
|
|
||||||
EMAIL_FILE_PATH = "/data/logs/emails"
|
|
||||||
SOCIAL_AUTH_SAML_ORG_INFO = {
|
|
||||||
"en-US": {
|
|
||||||
"displayname": "Burrow Zulip",
|
|
||||||
"name": "zulip",
|
|
||||||
"url": "https://${cfg.domain}",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
SOCIAL_AUTH_SAML_ENABLED_IDPS = {
|
|
||||||
"authentik": {
|
|
||||||
"entity_id": "https://${cfg.authentikDomain}",
|
|
||||||
"url": "https://${cfg.authentikDomain}/application/saml/${cfg.authentikProviderSlug}/sso/binding/redirect/",
|
|
||||||
"display_name": "burrow.net",
|
|
||||||
"x509cert": """$saml_cert""",
|
|
||||||
"attr_user_permanent_id": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
|
||||||
"attr_username": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
|
||||||
"attr_email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
|
||||||
"attr_first_name": "firstName",
|
|
||||||
"attr_last_name": "lastName",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.burrow-zulip = {
|
|
||||||
description = "Run Burrow Zulip with host-managed dependencies";
|
|
||||||
after = [
|
|
||||||
"burrow-zulip-runtime.service"
|
|
||||||
"network-online.target"
|
|
||||||
];
|
|
||||||
wants = [
|
|
||||||
"burrow-zulip-runtime.service"
|
|
||||||
"network-online.target"
|
|
||||||
];
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
path = [
|
|
||||||
pkgs.bash
|
|
||||||
pkgs.coreutils
|
|
||||||
pkgs.gawk
|
|
||||||
pkgs.gnugrep
|
|
||||||
pkgs.openssl
|
|
||||||
pkgs.podman
|
|
||||||
pkgs.podman-compose
|
|
||||||
];
|
|
||||||
restartTriggers = [
|
|
||||||
composeFile
|
|
||||||
cfg.postgresPasswordFile
|
|
||||||
cfg.rabbitmqPasswordFile
|
|
||||||
cfg.redisPasswordFile
|
|
||||||
cfg.secretKeyFile
|
|
||||||
];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
User = "root";
|
|
||||||
Group = "root";
|
|
||||||
WorkingDirectory = cfg.dataDir;
|
|
||||||
RemainAfterExit = true;
|
|
||||||
TimeoutStopSec = "20s";
|
|
||||||
ExecStop = "${pkgs.bash}/bin/bash -lc 'set -euo pipefail; if ${pkgs.podman}/bin/podman container exists burrow-zulip_zulip_1; then ${pkgs.podman}/bin/podman stop --ignore --time 10 burrow-zulip_zulip_1 >/dev/null || true; ${pkgs.podman}/bin/podman rm -f --ignore burrow-zulip_zulip_1 >/dev/null || true; fi'";
|
|
||||||
};
|
|
||||||
script = ''
|
|
||||||
set -euo pipefail
|
|
||||||
cd ${lib.escapeShellArg cfg.dataDir}
|
|
||||||
|
|
||||||
compose() {
|
|
||||||
${pkgs.podman-compose}/bin/podman-compose -p burrow-zulip "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_zulip_data_layout() {
|
|
||||||
local zulip_data_dir=${lib.escapeShellArg "${cfg.dataDir}/data"}
|
|
||||||
|
|
||||||
install -d -m 0755 "$zulip_data_dir/logs"
|
|
||||||
install -d -m 0755 "$zulip_data_dir/logs/emails"
|
|
||||||
install -d -m 0700 "$zulip_data_dir/secrets"
|
|
||||||
chown 1000:1000 "$zulip_data_dir/logs" "$zulip_data_dir/logs/emails" "$zulip_data_dir/secrets"
|
|
||||||
|
|
||||||
if [ ! -s "$zulip_data_dir/secrets/bootstrap-owner-password" ]; then
|
|
||||||
umask 077
|
|
||||||
openssl rand -base64 24 > "$zulip_data_dir/secrets/bootstrap-owner-password"
|
|
||||||
fi
|
|
||||||
chown 1000:1000 "$zulip_data_dir/secrets/bootstrap-owner-password"
|
|
||||||
chmod 0600 "$zulip_data_dir/secrets/bootstrap-owner-password"
|
|
||||||
}
|
|
||||||
|
|
||||||
bootstrap_realm_if_needed() {
|
|
||||||
local realm_exists
|
|
||||||
local attempts=0
|
|
||||||
while ! podman exec burrow-zulip_zulip_1 test -r /etc/zulip/zulip-secrets.conf >/dev/null 2>&1; do
|
|
||||||
attempts=$((attempts + 1))
|
|
||||||
if [ "$attempts" -ge 90 ]; then
|
|
||||||
echo "error: Zulip did not finish generating production secrets" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
realm_exists="$(
|
|
||||||
podman exec burrow-zulip_zulip_1 bash -lc \
|
|
||||||
"su zulip -c '/home/zulip/deployments/current/manage.py list_realms'" \
|
|
||||||
| awk '$NF == "https://${cfg.domain}" { print "yes" }'
|
|
||||||
)"
|
|
||||||
|
|
||||||
if [ -n "$realm_exists" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local realm_name=${lib.escapeShellArg cfg.realmName}
|
|
||||||
local admin_email=${lib.escapeShellArg cfg.administratorEmail}
|
|
||||||
local owner_name=${lib.escapeShellArg cfg.realmOwnerName}
|
|
||||||
local create_realm_cmd
|
|
||||||
|
|
||||||
printf -v create_realm_cmd '%q ' \
|
|
||||||
/home/zulip/deployments/current/manage.py \
|
|
||||||
create_realm \
|
|
||||||
--string-id= \
|
|
||||||
--password-file /data/secrets/bootstrap-owner-password \
|
|
||||||
--automated \
|
|
||||||
"$realm_name" \
|
|
||||||
"$admin_email" \
|
|
||||||
"$owner_name"
|
|
||||||
|
|
||||||
podman exec burrow-zulip_zulip_1 su zulip -c "$create_realm_cmd"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ ! -e .initialized ]; then
|
|
||||||
compose pull
|
|
||||||
compose run --rm -T zulip app:init
|
|
||||||
touch .initialized
|
|
||||||
fi
|
|
||||||
|
|
||||||
ensure_zulip_data_layout
|
|
||||||
compose up -d zulip
|
|
||||||
bootstrap_realm_if_needed
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -25,9 +25,4 @@ in
|
||||||
"secrets/infra/headscale-oidc-client-secret.age".publicKeys = burrowForgeRecipients;
|
"secrets/infra/headscale-oidc-client-secret.age".publicKeys = burrowForgeRecipients;
|
||||||
"secrets/infra/linear-scim-token.age".publicKeys = burrowForgeRecipients;
|
"secrets/infra/linear-scim-token.age".publicKeys = burrowForgeRecipients;
|
||||||
"secrets/infra/tailscale-oidc-client-secret.age".publicKeys = burrowForgeRecipients;
|
"secrets/infra/tailscale-oidc-client-secret.age".publicKeys = burrowForgeRecipients;
|
||||||
"secrets/infra/zulip-postgres-password.age".publicKeys = burrowForgeRecipients;
|
|
||||||
"secrets/infra/zulip-memcached-password.age".publicKeys = burrowForgeRecipients;
|
|
||||||
"secrets/infra/zulip-rabbitmq-password.age".publicKeys = burrowForgeRecipients;
|
|
||||||
"secrets/infra/zulip-redis-password.age".publicKeys = burrowForgeRecipients;
|
|
||||||
"secrets/infra/zulip-secret-key.age".publicKeys = burrowForgeRecipients;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
age-encryption.org/v1
|
|
||||||
-> ssh-ed25519 ux4N8Q x0r1UHgSibFIvKU34kP0+mnvQa5xXnac3P5fyqb7qFc
|
|
||||||
MfKnr5N0DV2NIoo4MFVFV0ULMayy0zzZqIq4FDzgDGc
|
|
||||||
-> ssh-ed25519 IrZmAg rzoR8knGrsTGuh9Hqg/NB0NQKI1vx1WI0ZRyrLIPwVY
|
|
||||||
7gV/d1slrIT+W0+iX5YK/uUWjHGJfee6vA+f9a35nEY
|
|
||||||
-> ssh-ed25519 0kWPgQ SyuEAfqmBAqLcuuQUHM5OzAv2hoquMMYtVdbKpBVhjI
|
|
||||||
7QqXens2363ln0euoormMh9a3Csh+nS2eBkHuQJmOWc
|
|
||||||
-> X25519 qDjNNkYBUhWTYyBhrw9tYl8a7G6TCkVZbR4aPcP+J0c
|
|
||||||
QF33V6hFUuYRj0B8Eo4jqyyvCpBbpD2ViVWoS8A8f3E
|
|
||||||
--- 1/Jb0nvWlcszMmxI0yVr6kfexDN0sSk1p+wsTUL4WvU
|
|
||||||
¨¤ãö9a¿ª5ÌIµÛÙƒçŽèV[fÀÁàç,Dàb \æv&§L½Z7õ!åû?4=JxFíÁeVÔ
|
|
||||||
Binary file not shown.
|
|
@ -1,11 +0,0 @@
|
||||||
age-encryption.org/v1
|
|
||||||
-> ssh-ed25519 ux4N8Q s1hLIWvkXmlIv/VeHXpDSCe+dh09mE+iZd7xJiQccy0
|
|
||||||
8WosTJQLGRPhTR06SIDjgtXNebcf+H/pFzY/lBCjXcs
|
|
||||||
-> ssh-ed25519 IrZmAg zBNlK+o/RCTCyp8BRkoAYqsDn//kIKtYk3SICkMu3BA
|
|
||||||
EhBQy8QdSnCZKkdGzQho7zEMmAbJVoU5jZOMPN6tHG0
|
|
||||||
-> ssh-ed25519 0kWPgQ hv06idPXqAATkLeUC5vILdEO2NXNWPczlWnwMFvOdkA
|
|
||||||
3EeajviunGlcfcF1QlRJrVA9bwPT+fJZFX0uneYVs0c
|
|
||||||
-> X25519 vm9rPYnQB16VSidi7+nr70lFaH0W/jIGY8zwUObZUV8
|
|
||||||
jFgPy/w4j0/p1USKGjQY+coo1OUFXiIjJ5apIZCrZVI
|
|
||||||
--- Cf2c6WzLYOi8xE/sIn7ZtUqBy5AToASDUNpAxyjrI9M
|
|
||||||
˜:Š,+!°¬›ÏÛöϨϬB4DÿŸmHè÷®|Ä(çŸø9ñ‹l9†LßPZ^ïzed=im¡óëz¢æ?øŸ
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
age-encryption.org/v1
|
|
||||||
-> ssh-ed25519 ux4N8Q DqDE3ZZlPUWUyyLA185xsOmfGi146SNk+hENMQXaiFY
|
|
||||||
D6FhZgynbdccPJQiFRJ18EYvCyDLz3cak0YuQa4f5p4
|
|
||||||
-> ssh-ed25519 IrZmAg lXgVeADmgjeHeVOOIS5oHqrhkN59ZWDemMOBJo3ubH8
|
|
||||||
AQ24P+DnxNoHEguNnLaROIW4/Sq96w/UxzzQwEOyGRc
|
|
||||||
-> ssh-ed25519 0kWPgQ 8x0pMohdACYueLY6jbNwg7MYVaZcjwBU4axthvDoFx4
|
|
||||||
SgUVnd6MK1MccWVYOu9R3PtoMCBBNGKQ7jt5MSA+KkI
|
|
||||||
-> X25519 UaO5huJPx8d8eMUnGhbI77tZjsFlIPWEffT4fgoO22w
|
|
||||||
DVz016ibRxJoa4TDmb2m0Qu9Dn8jpjWEBVtdm2TZx0c
|
|
||||||
--- 5+MHuvC26SjEBFSmRm0kXjiI27QnJGxvPl2w13EkMrw
|
|
||||||
Óí¯FžoQ˜˜ƒ]ÈŸ‹õ‘ˆ‚þš‰ûëeU³/óíÑ/ó®ÂnÀoø.XþGù£J•й|‡<>õ+ïž
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
age-encryption.org/v1
|
|
||||||
-> ssh-ed25519 ux4N8Q ml+kmLmuRb2nMXJyhKigby2+lPddxM/U7tjhGGQ/JGk
|
|
||||||
B3UCv/3+4GHeKR964o/m0CoicHwDgWQGEarPW94tb3I
|
|
||||||
-> ssh-ed25519 IrZmAg AO0ELOuGGj+WanDZFRkHKUEJyZqJYFdhWbqmUfwbpiM
|
|
||||||
5RZMxVBvW5+TzCBFnn66ry3o5V5cJykweyoYMVBgczY
|
|
||||||
-> ssh-ed25519 0kWPgQ gqQ/S33Re2OYLz1D9LoSAoqOKxuL4aUes8r6+NyAoXw
|
|
||||||
NHo2xFsxxJO1ZjnG9r3oxMuvjOUsCyyPvcar2ejZp9w
|
|
||||||
-> X25519 vUAjBCE197YsckVNM4SYVIPBEESTWnBPCWnUlEwYs1I
|
|
||||||
L3l85DXFoAVm2ssHfjBeqRpWGlo1UGbmcNkEgoUB9fM
|
|
||||||
--- X/2O8ufjbTGrt2zCm4gSRqqoxT5v6a+13XjH4dpRsHs
|
|
||||||
÷¼âM¬kÜf"¹¬ž(qëxöÅèF2Bd…ÖMRîYji ¯õˆÖ°Ü´Å<¿òøÒ‘¾™Ñí¯b_.!r+¸<ÀÞUsž²sëô<C3AB>Êæ<C38A>uí?g«ÉDçã\Öðú<C3B0>VaÊÓÄÓ¨mÇã(ȈÞ&.ñ&Àc/|½w˜²ƒ(Wð’¡ÁýïªHޝœ4rÑ ¾¸á°æ+ j"üñÙB §
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue