Add Linear SCIM role sync
This commit is contained in:
parent
4d3257995b
commit
ebcfc4bf8d
7 changed files with 440 additions and 0 deletions
311
Scripts/authentik-sync-linear-scim.sh
Normal file
311
Scripts/authentik-sync-linear-scim.sh
Normal file
|
|
@ -0,0 +1,311 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
authentik_url="${AUTHENTIK_URL:-https://auth.burrow.net}"
|
||||||
|
bootstrap_token="${AUTHENTIK_BOOTSTRAP_TOKEN:-}"
|
||||||
|
application_slug="${AUTHENTIK_LINEAR_APPLICATION_SLUG:-linear}"
|
||||||
|
provider_name="${AUTHENTIK_LINEAR_SCIM_PROVIDER_NAME:-Linear SCIM}"
|
||||||
|
scim_url="${AUTHENTIK_LINEAR_SCIM_URL:-}"
|
||||||
|
scim_token_file="${AUTHENTIK_LINEAR_SCIM_TOKEN_FILE:-}"
|
||||||
|
user_identifier="${AUTHENTIK_LINEAR_SCIM_USER_IDENTIFIER:-email}"
|
||||||
|
owner_group="${AUTHENTIK_LINEAR_OWNER_GROUP:-linear-owners}"
|
||||||
|
admin_group="${AUTHENTIK_LINEAR_ADMIN_GROUP:-linear-admins}"
|
||||||
|
guest_group="${AUTHENTIK_LINEAR_GUEST_GROUP:-linear-guests}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: Scripts/authentik-sync-linear-scim.sh
|
||||||
|
|
||||||
|
Required environment:
|
||||||
|
AUTHENTIK_BOOTSTRAP_TOKEN
|
||||||
|
AUTHENTIK_LINEAR_SCIM_URL
|
||||||
|
AUTHENTIK_LINEAR_SCIM_TOKEN_FILE
|
||||||
|
|
||||||
|
Optional environment:
|
||||||
|
AUTHENTIK_URL
|
||||||
|
AUTHENTIK_LINEAR_APPLICATION_SLUG
|
||||||
|
AUTHENTIK_LINEAR_SCIM_PROVIDER_NAME
|
||||||
|
AUTHENTIK_LINEAR_SCIM_USER_IDENTIFIER
|
||||||
|
AUTHENTIK_LINEAR_OWNER_GROUP
|
||||||
|
AUTHENTIK_LINEAR_ADMIN_GROUP
|
||||||
|
AUTHENTIK_LINEAR_GUEST_GROUP
|
||||||
|
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
|
||||||
|
|
||||||
|
if [[ -z "$scim_url" ]]; then
|
||||||
|
echo "error: AUTHENTIK_LINEAR_SCIM_URL is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$scim_token_file" || ! -s "$scim_token_file" ]]; then
|
||||||
|
echo "error: AUTHENTIK_LINEAR_SCIM_TOKEN_FILE is required and must be readable" >&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
|
||||||
|
}
|
||||||
|
|
||||||
|
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_group_pk() {
|
||||||
|
local group_name="$1"
|
||||||
|
|
||||||
|
api GET "/api/v3/core/groups/?page_size=200&search=${group_name}" \
|
||||||
|
| jq -r --arg name "$group_name" '.results[]? | select(.name == $name) | .pk // empty' \
|
||||||
|
| head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_group() {
|
||||||
|
local group_name="$1"
|
||||||
|
local payload group_pk
|
||||||
|
|
||||||
|
payload="$(jq -cn --arg name "$group_name" '{name: $name}')"
|
||||||
|
group_pk="$(lookup_group_pk "$group_name")"
|
||||||
|
|
||||||
|
if [[ -n "$group_pk" ]]; then
|
||||||
|
api PATCH "/api/v3/core/groups/${group_pk}/" "$payload" >/dev/null
|
||||||
|
else
|
||||||
|
group_pk="$(
|
||||||
|
api POST "/api/v3/core/groups/" "$payload" \
|
||||||
|
| jq -r '.pk // empty'
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$group_pk" ]]; then
|
||||||
|
echo "error: could not reconcile Authentik group ${group_name}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$group_pk"
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup_application() {
|
||||||
|
api GET "/api/v3/core/applications/?page_size=200" \
|
||||||
|
| jq -c --arg slug "$application_slug" '.results[]? | select(.slug == $slug)' \
|
||||||
|
| head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup_scim_provider() {
|
||||||
|
api GET "/api/v3/providers/scim/?page_size=200" \
|
||||||
|
| jq -c \
|
||||||
|
--arg application_slug "$application_slug" \
|
||||||
|
--arg provider_name "$provider_name" \
|
||||||
|
'.results[]? | select(.assigned_backchannel_application_slug == $application_slug or .name == $provider_name)' \
|
||||||
|
| head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup_scim_mapping_pk() {
|
||||||
|
local managed_name="$1"
|
||||||
|
|
||||||
|
api GET "/api/v3/propertymappings/provider/scim/?page_size=200" \
|
||||||
|
| jq -r --arg managed "$managed_name" '.results[]? | select(.managed == $managed) | .pk // empty' \
|
||||||
|
| head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
reconcile_property_mapping() {
|
||||||
|
local name="$1"
|
||||||
|
local expression="$2"
|
||||||
|
local payload existing_pk
|
||||||
|
|
||||||
|
payload="$(
|
||||||
|
jq -n \
|
||||||
|
--arg name "$name" \
|
||||||
|
--arg expression "$expression" \
|
||||||
|
'{
|
||||||
|
name: $name,
|
||||||
|
expression: $expression
|
||||||
|
}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
existing_pk="$(
|
||||||
|
api GET "/api/v3/propertymappings/provider/scim/?page_size=200" \
|
||||||
|
| jq -r --arg name "$name" '.results[]? | select(.name == $name) | .pk // empty' \
|
||||||
|
| head -n1
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -n "$existing_pk" ]]; then
|
||||||
|
api PATCH "/api/v3/propertymappings/provider/scim/${existing_pk}/" "$payload" >/dev/null
|
||||||
|
printf '%s\n' "$existing_pk"
|
||||||
|
else
|
||||||
|
api POST "/api/v3/propertymappings/provider/scim/" "$payload" \
|
||||||
|
| jq -r '.pk // empty'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_object() {
|
||||||
|
local provider_pk="$1"
|
||||||
|
local model="$2"
|
||||||
|
local object_id="$3"
|
||||||
|
|
||||||
|
api POST "/api/v3/providers/scim/${provider_pk}/sync/object/" "$(
|
||||||
|
jq -cn \
|
||||||
|
--arg model "$model" \
|
||||||
|
--arg object_id "$object_id" \
|
||||||
|
'{
|
||||||
|
sync_object_model: $model,
|
||||||
|
sync_object_id: $object_id,
|
||||||
|
override_dry_run: false
|
||||||
|
}'
|
||||||
|
)" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_authentik
|
||||||
|
|
||||||
|
group_mapping_pk="$(lookup_scim_mapping_pk "goauthentik.io/providers/scim/group")"
|
||||||
|
case "$user_identifier" in
|
||||||
|
email)
|
||||||
|
user_mapping_expression=$'# Some implementations require givenName and familyName to be set\ngivenName, familyName = request.user.name, " "\nformatted = request.user.name + " "\nif " " in request.user.name:\n givenName, _, familyName = request.user.name.partition(" ")\n formatted = request.user.name\n\navatar = request.user.avatar\nphotos = None\nif "://" in avatar:\n photos = [{"value": avatar, "type": "photo"}]\n\nlocale = request.user.locale()\nif locale == "":\n locale = None\n\nemails = []\nif request.user.email != "":\n emails = [{\n "value": request.user.email,\n "type": "other",\n "primary": True,\n }]\n\nidentifier = request.user.email\nif identifier == "":\n identifier = request.user.username\n\nreturn {\n "userName": identifier,\n "name": {\n "formatted": formatted,\n "givenName": givenName,\n "familyName": familyName,\n },\n "displayName": request.user.name,\n "photos": photos,\n "locale": locale,\n "active": request.user.is_active,\n "emails": emails,\n}'
|
||||||
|
;;
|
||||||
|
username)
|
||||||
|
user_mapping_expression=$'# Some implementations require givenName and familyName to be set\ngivenName, familyName = request.user.name, " "\nformatted = request.user.name + " "\nif " " in request.user.name:\n givenName, _, familyName = request.user.name.partition(" ")\n formatted = request.user.name\n\navatar = request.user.avatar\nphotos = None\nif "://" in avatar:\n photos = [{"value": avatar, "type": "photo"}]\n\nlocale = request.user.locale()\nif locale == "":\n locale = None\n\nemails = []\nif request.user.email != "":\n emails = [{\n "value": request.user.email,\n "type": "other",\n "primary": True,\n }]\nreturn {\n "userName": request.user.username,\n "name": {\n "formatted": formatted,\n "givenName": givenName,\n "familyName": familyName,\n },\n "displayName": request.user.name,\n "photos": photos,\n "locale": locale,\n "active": request.user.is_active,\n "emails": emails,\n}'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "error: unsupported AUTHENTIK_LINEAR_SCIM_USER_IDENTIFIER value: ${user_identifier}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
user_mapping_pk="$(reconcile_property_mapping "Burrow Linear SCIM User" "$user_mapping_expression")"
|
||||||
|
|
||||||
|
if [[ -z "$user_mapping_pk" || -z "$group_mapping_pk" ]]; then
|
||||||
|
echo "error: could not resolve managed Authentik SCIM property mappings" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
owner_group_pk="$(ensure_group "$owner_group")"
|
||||||
|
admin_group_pk="$(ensure_group "$admin_group")"
|
||||||
|
guest_group_pk="$(ensure_group "$guest_group")"
|
||||||
|
|
||||||
|
provider_payload="$(
|
||||||
|
jq -n \
|
||||||
|
--arg name "$provider_name" \
|
||||||
|
--arg url "$scim_url" \
|
||||||
|
--arg token "$(tr -d '\r\n' < "$scim_token_file")" \
|
||||||
|
--arg user_mapping_pk "$user_mapping_pk" \
|
||||||
|
--arg group_mapping_pk "$group_mapping_pk" \
|
||||||
|
--arg owner_group_pk "$owner_group_pk" \
|
||||||
|
--arg admin_group_pk "$admin_group_pk" \
|
||||||
|
--arg guest_group_pk "$guest_group_pk" \
|
||||||
|
'{
|
||||||
|
name: $name,
|
||||||
|
url: $url,
|
||||||
|
token: $token,
|
||||||
|
auth_mode: "token",
|
||||||
|
verify_certificates: true,
|
||||||
|
compatibility_mode: "default",
|
||||||
|
property_mappings: [$user_mapping_pk],
|
||||||
|
property_mappings_group: [$group_mapping_pk],
|
||||||
|
group_filters: [
|
||||||
|
$owner_group_pk,
|
||||||
|
$admin_group_pk,
|
||||||
|
$guest_group_pk
|
||||||
|
],
|
||||||
|
dry_run: false
|
||||||
|
}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
existing_provider="$(lookup_scim_provider)"
|
||||||
|
if [[ -n "$existing_provider" ]]; then
|
||||||
|
provider_pk="$(printf '%s\n' "$existing_provider" | jq -r '.pk')"
|
||||||
|
api PATCH "/api/v3/providers/scim/${provider_pk}/" "$provider_payload" >/dev/null
|
||||||
|
else
|
||||||
|
provider_pk="$(
|
||||||
|
api POST "/api/v3/providers/scim/" "$provider_payload" \
|
||||||
|
| jq -r '.pk // empty'
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${provider_pk:-}" ]]; then
|
||||||
|
echo "error: Linear SCIM provider did not return a primary key" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
application="$(lookup_application)"
|
||||||
|
if [[ -z "$application" ]]; then
|
||||||
|
echo "error: could not resolve Authentik application ${application_slug}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
application_pk="$(printf '%s\n' "$application" | jq -r '.pk')"
|
||||||
|
application_payload="$(
|
||||||
|
printf '%s\n' "$application" \
|
||||||
|
| jq \
|
||||||
|
--arg provider_pk "$provider_pk" \
|
||||||
|
'{
|
||||||
|
name: .name,
|
||||||
|
slug: .slug,
|
||||||
|
provider: .provider,
|
||||||
|
backchannel_providers: ((.backchannel_providers // []) + [($provider_pk | tonumber)] | unique),
|
||||||
|
open_in_new_tab: .open_in_new_tab,
|
||||||
|
meta_launch_url: .meta_launch_url,
|
||||||
|
policy_engine_mode: .policy_engine_mode
|
||||||
|
}'
|
||||||
|
)"
|
||||||
|
api PATCH "/api/v3/core/applications/${application_pk}/" "$application_payload" >/dev/null
|
||||||
|
|
||||||
|
group_pks_json="$(jq -cn --arg owner "$owner_group_pk" --arg admin "$admin_group_pk" --arg guest "$guest_group_pk" '[$owner, $admin, $guest]')"
|
||||||
|
user_pks_json="$(
|
||||||
|
api GET "/api/v3/core/users/?page_size=200" \
|
||||||
|
| jq -c \
|
||||||
|
--argjson group_pks "$group_pks_json" \
|
||||||
|
'[.results[]?
|
||||||
|
| select(
|
||||||
|
([((.groups // [])[] | tostring)] as $user_groups
|
||||||
|
| ($group_pks | map(. as $wanted | ($user_groups | index($wanted)) != null) | any))
|
||||||
|
)
|
||||||
|
| .pk]'
|
||||||
|
)"
|
||||||
|
|
||||||
|
while IFS= read -r group_pk; do
|
||||||
|
[[ -z "$group_pk" ]] && continue
|
||||||
|
sync_object "$provider_pk" "authentik.core.models.Group" "$group_pk"
|
||||||
|
done < <(printf '%s\n' "$group_pks_json" | jq -r '.[]')
|
||||||
|
|
||||||
|
while IFS= read -r user_pk; do
|
||||||
|
[[ -z "$user_pk" ]] && continue
|
||||||
|
sync_object "$provider_pk" "authentik.core.models.User" "$user_pk"
|
||||||
|
done < <(printf '%s\n' "$user_pks_json" | jq -r '.[]')
|
||||||
|
|
||||||
|
status_json="$(api GET "/api/v3/providers/scim/${provider_pk}/sync/status/")"
|
||||||
|
if ! printf '%s\n' "$status_json" | jq -e '.task_count >= 0' >/dev/null 2>&1; then
|
||||||
|
echo "error: could not read Linear SCIM sync status for provider ${provider_pk}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Synced Authentik Linear SCIM provider ${provider_name} (${provider_pk}) with groups ${owner_group}, ${admin_group}, ${guest_group}."
|
||||||
|
|
@ -2,6 +2,11 @@
|
||||||
groups = {
|
groups = {
|
||||||
users = "burrow-users";
|
users = "burrow-users";
|
||||||
admins = "burrow-admins";
|
admins = "burrow-admins";
|
||||||
|
linear = {
|
||||||
|
owners = "linear-owners";
|
||||||
|
admins = "linear-admins";
|
||||||
|
guests = "linear-guests";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
identities = {
|
identities = {
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ across vendor-native Google auth flows when Burrow already operates an IdP.
|
||||||
- Add Authentik-managed SAML applications for:
|
- Add Authentik-managed SAML applications for:
|
||||||
- Zulip at `chat.burrow.net`
|
- Zulip at `chat.burrow.net`
|
||||||
- Linear using Burrow's claimed domains and Authentik metadata
|
- Linear using Burrow's claimed domains and Authentik metadata
|
||||||
|
- Add an Authentik-managed SCIM backchannel for Linear so Burrow can push
|
||||||
|
role groups declaratively instead of hand-maintaining workspace roles.
|
||||||
- Add an Authentik-managed OIDC application for 1Password Business under the
|
- Add an Authentik-managed OIDC application for 1Password Business under the
|
||||||
Burrow team sign-in address.
|
Burrow team sign-in address.
|
||||||
- Treat Zulip and Linear as downstream applications of the same identity
|
- Treat Zulip and Linear as downstream applications of the same identity
|
||||||
|
|
@ -66,6 +68,10 @@ 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.
|
||||||
|
- 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
|
||||||
|
configuration should map that intent onto the Linear push group without a
|
||||||
|
second manual roster.
|
||||||
- Model 1Password according to the vendor's actual integration contract:
|
- Model 1Password according to the vendor's actual integration contract:
|
||||||
- OIDC Authorization Code Flow with PKCE
|
- OIDC Authorization Code Flow with PKCE
|
||||||
- public client rather than a confidential client
|
- public client rather than a confidential client
|
||||||
|
|
@ -82,6 +88,8 @@ across vendor-native Google auth flows when Burrow already operates an IdP.
|
||||||
- Linear SAML must not become Burrow's only admin recovery path. At least one
|
- Linear SAML must not become Burrow's only admin recovery path. At least one
|
||||||
owner login path outside the enforced SAML flow should remain available until
|
owner login path outside the enforced SAML flow should remain available until
|
||||||
rollout is proven.
|
rollout is proven.
|
||||||
|
- Linear SCIM group push should be role-scoped and explicit. Burrow should
|
||||||
|
avoid blanket ownership mapping unless that intent is recorded in the repo.
|
||||||
- 1Password Owners cannot be forced onto Unlock with SSO during initial setup.
|
- 1Password Owners cannot be forced onto Unlock with SSO during initial setup.
|
||||||
Burrow should preserve the owner recovery path and treat OIDC rollout as a
|
Burrow should preserve the owner recovery path and treat OIDC rollout as a
|
||||||
scoped migration for non-owner users first.
|
scoped migration for non-owner users first.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
let
|
let
|
||||||
contributors = import ../../../contributors.nix;
|
contributors = import ../../../contributors.nix;
|
||||||
identities = contributors.identities;
|
identities = contributors.identities;
|
||||||
|
linearGroups = contributors.groups.linear;
|
||||||
stripNewline = value: lib.replaceStrings [ "\n" ] [ "" ] value;
|
stripNewline = value: lib.replaceStrings [ "\n" ] [ "" ] value;
|
||||||
authentikPasswordSecretPath = identity:
|
authentikPasswordSecretPath = identity:
|
||||||
if identity ? authentikPasswordSecret
|
if identity ? authentikPasswordSecret
|
||||||
|
|
@ -15,6 +16,7 @@ let
|
||||||
name = identity.displayName;
|
name = identity.displayName;
|
||||||
email = identity.canonicalEmail;
|
email = identity.canonicalEmail;
|
||||||
isAdmin = identity.isAdmin or false;
|
isAdmin = identity.isAdmin or false;
|
||||||
|
groups = lib.optionals (identity.isAdmin or false) [ linearGroups.owners ];
|
||||||
passwordFile = authentikPasswordSecretPath identity;
|
passwordFile = authentikPasswordSecretPath identity;
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -111,6 +113,12 @@ in
|
||||||
group = "root";
|
group = "root";
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
};
|
};
|
||||||
|
age.secrets.burrowLinearScimToken = {
|
||||||
|
file = ../../../secrets/infra/linear-scim-token.age;
|
||||||
|
owner = "root";
|
||||||
|
group = "root";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
age.secrets.burrowAuthentikGoogleClientId = {
|
age.secrets.burrowAuthentikGoogleClientId = {
|
||||||
file = ../../../secrets/infra/authentik-google-client-id.age;
|
file = ../../../secrets/infra/authentik-google-client-id.age;
|
||||||
owner = "root";
|
owner = "root";
|
||||||
|
|
@ -210,6 +218,12 @@ in
|
||||||
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";
|
||||||
linearDefaultRelayState = "https://linear.app/auth/sso/d0ca13dc-ac41-4824-8aab-e0ca352fc3de";
|
linearDefaultRelayState = "https://linear.app/auth/sso/d0ca13dc-ac41-4824-8aab-e0ca352fc3de";
|
||||||
|
linearScimUrl = "https://api.linear.app/auth/scim/d0ca13dc-ac41-4824-8aab-e0ca352fc3de";
|
||||||
|
linearScimTokenFile = config.age.secrets.burrowLinearScimToken.path;
|
||||||
|
linearScimUserIdentifier = "email";
|
||||||
|
linearOwnerGroupName = linearGroups.owners;
|
||||||
|
linearAdminGroupName = linearGroups.admins;
|
||||||
|
linearGuestGroupName = linearGroups.guests;
|
||||||
};
|
};
|
||||||
|
|
||||||
services.burrow.headscale = {
|
services.burrow.headscale = {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ let
|
||||||
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;
|
||||||
linearSamlSyncScript = ../../Scripts/authentik-sync-linear-saml.sh;
|
linearSamlSyncScript = ../../Scripts/authentik-sync-linear-saml.sh;
|
||||||
|
linearScimSyncScript = ../../Scripts/authentik-sync-linear-scim.sh;
|
||||||
googleSourceSyncScript = ../../Scripts/authentik-sync-google-source.sh;
|
googleSourceSyncScript = ../../Scripts/authentik-sync-google-source.sh;
|
||||||
tailnetAuthFlowSyncScript = ../../Scripts/authentik-sync-tailnet-auth-flow.sh;
|
tailnetAuthFlowSyncScript = ../../Scripts/authentik-sync-tailnet-auth-flow.sh;
|
||||||
authentikBlueprint = pkgs.writeText "burrow-authentik-blueprint.yaml" ''
|
authentikBlueprint = pkgs.writeText "burrow-authentik-blueprint.yaml" ''
|
||||||
|
|
@ -209,6 +210,42 @@ in
|
||||||
description = "Optional Linear relay state or login URL for IdP-initiated launches.";
|
description = "Optional Linear relay state or login URL for IdP-initiated launches.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
linearScimUrl = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Linear SCIM base connector URL.";
|
||||||
|
};
|
||||||
|
|
||||||
|
linearScimTokenFile = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Host-local file containing the Linear SCIM bearer token.";
|
||||||
|
};
|
||||||
|
|
||||||
|
linearScimUserIdentifier = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "email";
|
||||||
|
description = "Linear SCIM unique identifier field for users.";
|
||||||
|
};
|
||||||
|
|
||||||
|
linearOwnerGroupName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "linear-owners";
|
||||||
|
description = "Authentik group name that should map to Linear owners.";
|
||||||
|
};
|
||||||
|
|
||||||
|
linearAdminGroupName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "linear-admins";
|
||||||
|
description = "Authentik group name that should map to Linear admins.";
|
||||||
|
};
|
||||||
|
|
||||||
|
linearGuestGroupName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "linear-guests";
|
||||||
|
description = "Authentik group name that should map to Linear guests.";
|
||||||
|
};
|
||||||
|
|
||||||
forgejoClientId = lib.mkOption {
|
forgejoClientId = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "git.burrow.net";
|
default = "git.burrow.net";
|
||||||
|
|
@ -871,6 +908,59 @@ EOF
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
systemd.services.burrow-authentik-linear-scim = lib.mkIf (
|
||||||
|
cfg.linearScimUrl != null && cfg.linearScimTokenFile != null
|
||||||
|
) {
|
||||||
|
description = "Reconcile the Burrow Authentik Linear SCIM provider";
|
||||||
|
after = [
|
||||||
|
"burrow-authentik-ready.service"
|
||||||
|
"burrow-authentik-directory.service"
|
||||||
|
"burrow-authentik-linear-saml.service"
|
||||||
|
"network-online.target"
|
||||||
|
];
|
||||||
|
wants = [
|
||||||
|
"burrow-authentik-ready.service"
|
||||||
|
"burrow-authentik-directory.service"
|
||||||
|
"burrow-authentik-linear-saml.service"
|
||||||
|
"network-online.target"
|
||||||
|
];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
restartTriggers = [
|
||||||
|
linearScimSyncScript
|
||||||
|
cfg.envFile
|
||||||
|
cfg.linearScimTokenFile
|
||||||
|
];
|
||||||
|
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_LINEAR_APPLICATION_SLUG=${lib.escapeShellArg cfg.linearProviderSlug}
|
||||||
|
export AUTHENTIK_LINEAR_SCIM_PROVIDER_NAME="Linear SCIM"
|
||||||
|
export AUTHENTIK_LINEAR_SCIM_URL=${lib.escapeShellArg cfg.linearScimUrl}
|
||||||
|
export AUTHENTIK_LINEAR_SCIM_TOKEN_FILE=${lib.escapeShellArg cfg.linearScimTokenFile}
|
||||||
|
export AUTHENTIK_LINEAR_SCIM_USER_IDENTIFIER=${lib.escapeShellArg cfg.linearScimUserIdentifier}
|
||||||
|
export AUTHENTIK_LINEAR_OWNER_GROUP=${lib.escapeShellArg cfg.linearOwnerGroupName}
|
||||||
|
export AUTHENTIK_LINEAR_ADMIN_GROUP=${lib.escapeShellArg cfg.linearAdminGroupName}
|
||||||
|
export AUTHENTIK_LINEAR_GUEST_GROUP=${lib.escapeShellArg cfg.linearGuestGroupName}
|
||||||
|
|
||||||
|
${pkgs.bash}/bin/bash ${linearScimSyncScript}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
services.caddy.virtualHosts."${cfg.domain}".extraConfig = ''
|
services.caddy.virtualHosts."${cfg.domain}".extraConfig = ''
|
||||||
encode gzip zstd
|
encode gzip zstd
|
||||||
reverse_proxy 127.0.0.1:${toString cfg.port}
|
reverse_proxy 127.0.0.1:${toString cfg.port}
|
||||||
|
|
|
||||||
|
|
@ -23,5 +23,6 @@ in
|
||||||
"secrets/infra/forgejo-nsc-dispatcher-config.age".publicKeys = burrowForgeRecipients;
|
"secrets/infra/forgejo-nsc-dispatcher-config.age".publicKeys = burrowForgeRecipients;
|
||||||
"secrets/infra/forgejo-nsc-token.age".publicKeys = burrowForgeRecipients;
|
"secrets/infra/forgejo-nsc-token.age".publicKeys = burrowForgeRecipients;
|
||||||
"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/tailscale-oidc-client-secret.age".publicKeys = burrowForgeRecipients;
|
"secrets/infra/tailscale-oidc-client-secret.age".publicKeys = burrowForgeRecipients;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
secrets/infra/linear-scim-token.age
Normal file
11
secrets/infra/linear-scim-token.age
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
age-encryption.org/v1
|
||||||
|
-> ssh-ed25519 ux4N8Q 6LanICpiWi1sozNr5HJDWCGb6QFBktRQ0dH2wfFSu2g
|
||||||
|
jc83UfFoFvxAXcu4O/b6KC+1AyZq/k9IHzx6fL8DHoQ
|
||||||
|
-> ssh-ed25519 IrZmAg r1ggts4fiWOGHoD7IY+cVEgECOUFaulJ1ATSX6/wB2Q
|
||||||
|
NnKRd8FNKXpCrANK2q2mFJjWYccqInzGNHjK7oJNNS0
|
||||||
|
-> ssh-ed25519 0kWPgQ G3i+VXIhED5crwLZoF8cTcaljYENq7K0DAy5mTHsNkk
|
||||||
|
+eJThDXro6DpNghlcziQv64rg8j0mcm3UfGVHcctI6w
|
||||||
|
-> X25519 2yw5RabY1hp/of6RLpKI2ao0AwBOzNdeOR4M9YRwmhY
|
||||||
|
vCe9r9ayAsDcLkyt4/c9EBZpU/DrkGKj8KLbSF9YCHo
|
||||||
|
--- Lgi0Th/QpSFhDP7JK+jenEIvI0aQfQ3oQ6sl2homLu4
|
||||||
|
礽?-d<>:橥備<E6A9A5>濙咩怄蘖腨强*狺獯
|
||||||
Loading…
Add table
Add a link
Reference in a new issue