authentik: cleanup unused resources + add invitation enrollment flow [ci skip]
Cleanup: - Deleted 5 unused flows (enrollment-inviation, headscale-auth/authz, default-enrollment, oauth-enrollment) - Deleted 8 orphaned stages bound only to deleted flows - Deleted authentik Read-only group and role (0 users) - Deleted 2 unbound policies (map github username, Map Google Attributes) Invitation enrollment: - Created invitation-enrollment flow with 5 stages (invitation validation, identification with social login, prompt, user write, auto-login) - Set all OAuth sources (Google/GitHub/Facebook) enrollment_flow to invitation-enrollment - New users can only sign up via single-use invitation links - Added authentik-invite.sh script for invitation management - Updated reference docs and authentik skill
This commit is contained in:
parent
af5f6a659b
commit
160fda882f
3 changed files with 295 additions and 13 deletions
|
|
@ -2,11 +2,12 @@
|
||||||
|
|
||||||
> Snapshot of applications, groups, users, and flows. Use `authentik` skill for management tasks.
|
> Snapshot of applications, groups, users, and flows. Use `authentik` skill for management tasks.
|
||||||
|
|
||||||
## Applications (9)
|
## Applications (10)
|
||||||
| Application | Provider Type | Auth Flow |
|
| Application | Provider Type | Auth Flow |
|
||||||
|-------------|--------------|-----------|
|
|-------------|--------------|-----------|
|
||||||
| Cloudflare Access | OAuth2/OIDC | explicit consent |
|
| Cloudflare Access | OAuth2/OIDC | explicit consent |
|
||||||
| Domain wide catch all | Proxy (forward auth) | implicit consent |
|
| Domain wide catch all | Proxy (forward auth) | implicit consent |
|
||||||
|
| Forgejo | OAuth2/OIDC | explicit consent |
|
||||||
| Grafana | OAuth2/OIDC | implicit consent |
|
| Grafana | OAuth2/OIDC | implicit consent |
|
||||||
| Headscale | OAuth2/OIDC | explicit consent |
|
| Headscale | OAuth2/OIDC | explicit consent |
|
||||||
| Immich | OAuth2/OIDC | explicit consent |
|
| Immich | OAuth2/OIDC | explicit consent |
|
||||||
|
|
@ -18,17 +19,17 @@
|
||||||
## Groups (9)
|
## Groups (9)
|
||||||
| Group | Parent | Superuser | Purpose |
|
| Group | Parent | Superuser | Purpose |
|
||||||
|-------|--------|-----------|---------|
|
|-------|--------|-----------|---------|
|
||||||
| Allow Login Users | — | No | Parent group for login-permitted users |
|
| Allow Login Users | -- | No | Parent group for login-permitted users |
|
||||||
| authentik Admins | — | Yes | Full admin access |
|
| authentik Admins | -- | Yes | Full admin access |
|
||||||
| authentik Read-only | — | No | Read-only access (has role) |
|
|
||||||
| Headscale Users | Allow Login Users | No | VPN access |
|
| Headscale Users | Allow Login Users | No | VPN access |
|
||||||
| Home Server Admins | Allow Login Users | No | Server admin access |
|
| Home Server Admins | Allow Login Users | No | Server admin access |
|
||||||
| Wrongmove Users | Allow Login Users | No | Real-estate app access |
|
| Wrongmove Users | Allow Login Users | No | Real-estate app access |
|
||||||
| kubernetes-admins | — | No | K8s cluster-admin RBAC |
|
| kubernetes-admins | -- | No | K8s cluster-admin RBAC |
|
||||||
| kubernetes-power-users | — | No | K8s power-user RBAC |
|
| kubernetes-power-users | -- | No | K8s power-user RBAC |
|
||||||
| kubernetes-namespace-owners | — | No | K8s namespace-owner RBAC |
|
| kubernetes-namespace-owners | -- | No | K8s namespace-owner RBAC |
|
||||||
|
| Task Submitters | -- | No | Task submission access |
|
||||||
|
|
||||||
## Users (7 real)
|
## Users (8 real)
|
||||||
| Username | Name | Type | Groups |
|
| Username | Name | Type | Groups |
|
||||||
|----------|------|------|--------|
|
|----------|------|------|--------|
|
||||||
| akadmin | authentik Default Admin | internal | authentik Admins, Home Server Admins, Headscale Users |
|
| akadmin | authentik Default Admin | internal | authentik Admins, Home Server Admins, Headscale Users |
|
||||||
|
|
@ -36,15 +37,73 @@
|
||||||
| emil.barzin@gmail.com | Emil Barzin | internal | Home Server Admins, Headscale Users |
|
| emil.barzin@gmail.com | Emil Barzin | internal | Home Server Admins, Headscale Users |
|
||||||
| ancaelena98@gmail.com | Anca Milea | external | Wrongmove Users, Headscale Users |
|
| ancaelena98@gmail.com | Anca Milea | external | Wrongmove Users, Headscale Users |
|
||||||
| vabbit81@gmail.com | GHEORGHE Milea | external | Headscale Users |
|
| vabbit81@gmail.com | GHEORGHE Milea | external | Headscale Users |
|
||||||
| valentinakolevabarzina@gmail.com | Валентина Колева-Барзина | internal | Headscale Users |
|
| valentinakolevabarzina@gmail.com | Valentina | internal | Headscale Users |
|
||||||
| anca.r.cristian10@gmail.com | — | internal | Wrongmove Users |
|
| anca.r.cristian10@gmail.com | -- | internal | Wrongmove Users |
|
||||||
| kadir.tugan@gmail.com | Kadir | internal | Wrongmove Users |
|
| kadir.tugan@gmail.com | Kadir | internal | Wrongmove Users |
|
||||||
|
|
||||||
## Login Sources
|
## Login Sources
|
||||||
- **Google** (OAuth) — user matching by identifier
|
- **Google** (OAuth) -- user matching by identifier
|
||||||
- **GitHub** (OAuth) — user matching by email_link
|
- **GitHub** (OAuth) -- user matching by email_link
|
||||||
- **Facebook** (OAuth) — user matching by email_link
|
- **Facebook** (OAuth) -- user matching by email_link
|
||||||
|
- All sources use `invitation-enrollment` as enrollment flow (new users require invitation)
|
||||||
|
|
||||||
## Authorization Flows
|
## Authorization Flows
|
||||||
- **Explicit consent** (`default-provider-authorization-explicit-consent`): Shows consent screen
|
- **Explicit consent** (`default-provider-authorization-explicit-consent`): Shows consent screen
|
||||||
- **Implicit consent** (`default-provider-authorization-implicit-consent`): Auto-redirects
|
- **Implicit consent** (`default-provider-authorization-implicit-consent`): Auto-redirects
|
||||||
|
|
||||||
|
## Invitation Enrollment Flow
|
||||||
|
Slug: `invitation-enrollment` | PK: `7d667321-2b02-4e16-8161-148078a8dac1`
|
||||||
|
|
||||||
|
New users can only sign up via invitation link. Admins generate single-use invite links.
|
||||||
|
|
||||||
|
### Stages (in order)
|
||||||
|
| Order | Stage | Type | Purpose |
|
||||||
|
|-------|-------|------|---------|
|
||||||
|
| 10 | invitation-validation | Invitation | Validates `?itoken=` parameter, blocks without valid token |
|
||||||
|
| 20 | enrollment-identification | Identification | Shows social login (Google/GitHub/Facebook) + passkey |
|
||||||
|
| 30 | enrollment-prompt | Prompt | Collects name and email (pre-filled from social login) |
|
||||||
|
| 40 | enrollment-user-write | User Write | Creates user in `Allow Login Users` group |
|
||||||
|
| 50 | enrollment-login | User Login | Auto-login after signup |
|
||||||
|
|
||||||
|
### Invitation Management
|
||||||
|
Script: `.claude/scripts/authentik-invite.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create invitation (single-use, no expiry)
|
||||||
|
./authentik-invite.sh create "Headscale Users"
|
||||||
|
|
||||||
|
# Create invitation with expiry
|
||||||
|
./authentik-invite.sh create "Wrongmove Users" --days 7
|
||||||
|
|
||||||
|
# Add user to group after enrollment
|
||||||
|
./authentik-invite.sh assign <username> "Headscale Users"
|
||||||
|
|
||||||
|
# List pending invitations
|
||||||
|
./authentik-invite.sh list
|
||||||
|
```
|
||||||
|
|
||||||
|
Invited users sign up via social login (Google/GitHub/Facebook) or passkey. No username/password enrollment.
|
||||||
|
|
||||||
|
## Cleanup Log (2026-03-13)
|
||||||
|
### Deleted Flows
|
||||||
|
- `enrollment-inviation` (typo) -- previous invitation attempt
|
||||||
|
- `headscale-authentication` -- not used by any provider
|
||||||
|
- `headscale-authorization` -- not used by any provider
|
||||||
|
- `default-enrollment-flow` -- password-based, unused
|
||||||
|
- `oauth-enrollment` -- replaced by invitation-enrollment
|
||||||
|
|
||||||
|
### Deleted Stages
|
||||||
|
- `enrollment-invitation`, `enrollment-invitation-write` (from old invitation flow)
|
||||||
|
- `invitation` (unbound)
|
||||||
|
- `default-enrollment-prompt-first`, `default-enrollment-prompt-second` (from default enrollment)
|
||||||
|
- `default-enrollment-user-write`, `default-enrollment-email-verification`, `default-enrollment-user-login`
|
||||||
|
|
||||||
|
### Deleted Groups
|
||||||
|
- `authentik Read-only` -- 0 users, unused role
|
||||||
|
|
||||||
|
### Deleted Policies
|
||||||
|
- `map github username to email` -- unbound
|
||||||
|
- `Map Google Attributes` -- unbound
|
||||||
|
|
||||||
|
### Deleted Roles
|
||||||
|
- `authentik Read-only` -- no group assignment
|
||||||
|
|
|
||||||
180
.claude/scripts/authentik-invite.sh
Executable file
180
.claude/scripts/authentik-invite.sh
Executable file
|
|
@ -0,0 +1,180 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Authentik Invitation Management Script
|
||||||
|
# Usage:
|
||||||
|
# ./authentik-invite.sh create "Group Name" # Single-use, no expiry
|
||||||
|
# ./authentik-invite.sh create "Group Name" --days 7 # Expires in 7 days
|
||||||
|
# ./authentik-invite.sh assign <username> "Group Name" # Add user to group
|
||||||
|
# ./authentik-invite.sh list # Show pending invitations
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
INFRA_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
API="https://authentik.viktorbarzin.me/api/v3"
|
||||||
|
FLOW_SLUG="invitation-enrollment"
|
||||||
|
|
||||||
|
get_token() {
|
||||||
|
grep authentik_api_token "$INFRA_DIR/terraform.tfvars" | cut -d'"' -f2
|
||||||
|
}
|
||||||
|
|
||||||
|
api_get() {
|
||||||
|
curl -sf -H "Authorization: Bearer $(get_token)" "$API/$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
api_post() {
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: Bearer $(get_token)" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"$API/$1" -d "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
api_patch() {
|
||||||
|
curl -sf -X PATCH \
|
||||||
|
-H "Authorization: Bearer $(get_token)" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"$API/$1" -d "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_create() {
|
||||||
|
local group_name="${1:?Usage: create <group-name> [--days N]}"
|
||||||
|
local days=""
|
||||||
|
|
||||||
|
shift
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--days) days="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Build invitation payload
|
||||||
|
# Get flow PK
|
||||||
|
local flow_pk
|
||||||
|
flow_pk=$(api_get "flows/instances/$FLOW_SLUG/" | python3 -c "import json,sys; print(json.load(sys.stdin)['pk'])")
|
||||||
|
|
||||||
|
local payload
|
||||||
|
payload=$(python3 -c "
|
||||||
|
import json, sys, re
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
slug = re.sub(r'[^a-z0-9-]', '-', '$group_name'.lower()).strip('-')
|
||||||
|
data = {
|
||||||
|
'name': 'invite-' + slug + '-' + datetime.now(timezone.utc).strftime('%Y%m%d-%H%M'),
|
||||||
|
'single_use': True,
|
||||||
|
'fixed_data': {'group': '$group_name'},
|
||||||
|
'flow': '$flow_pk'
|
||||||
|
}
|
||||||
|
|
||||||
|
days = '$days'
|
||||||
|
if days:
|
||||||
|
expires = datetime.now(timezone.utc) + timedelta(days=int(days))
|
||||||
|
data['expires'] = expires.isoformat()
|
||||||
|
|
||||||
|
print(json.dumps(data))
|
||||||
|
")
|
||||||
|
|
||||||
|
local result
|
||||||
|
result=$(api_post "stages/invitation/invitations/" "$payload")
|
||||||
|
local token
|
||||||
|
token=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin)['pk'])")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Invitation created for group: $group_name"
|
||||||
|
if [[ -n "$days" ]]; then
|
||||||
|
echo "Expires in: $days days"
|
||||||
|
else
|
||||||
|
echo "Expires: never"
|
||||||
|
fi
|
||||||
|
echo "Single-use: yes"
|
||||||
|
echo ""
|
||||||
|
echo "Share this link:"
|
||||||
|
echo " https://authentik.viktorbarzin.me/if/flow/$FLOW_SLUG/?itoken=$token"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_assign() {
|
||||||
|
local username="${1:?Usage: assign <username> <group-name>}"
|
||||||
|
local group_name="${2:?Usage: assign <username> <group-name>}"
|
||||||
|
|
||||||
|
# Find user PK
|
||||||
|
local user_pk
|
||||||
|
user_pk=$(api_get "core/users/?search=$username" | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
users = json.load(sys.stdin)['results']
|
||||||
|
if not users:
|
||||||
|
print('NOT_FOUND', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print(users[0]['pk'])
|
||||||
|
")
|
||||||
|
|
||||||
|
# Find group PK and current users
|
||||||
|
local group_data
|
||||||
|
group_data=$(api_get "core/groups/?search=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$group_name'))")" | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
groups = json.load(sys.stdin)['results']
|
||||||
|
matches = [g for g in groups if g['name'] == '$group_name']
|
||||||
|
if not matches:
|
||||||
|
print('NOT_FOUND', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
g = matches[0]
|
||||||
|
users = g.get('users', [])
|
||||||
|
print(json.dumps({'pk': g['pk'], 'users': users}))
|
||||||
|
")
|
||||||
|
|
||||||
|
local group_pk
|
||||||
|
group_pk=$(echo "$group_data" | python3 -c "import json,sys; print(json.load(sys.stdin)['pk'])")
|
||||||
|
|
||||||
|
# Add user to group
|
||||||
|
local updated_users
|
||||||
|
updated_users=$(echo "$group_data" | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
d = json.load(sys.stdin)
|
||||||
|
users = d['users']
|
||||||
|
uid = $user_pk
|
||||||
|
if uid not in users:
|
||||||
|
users.append(uid)
|
||||||
|
print(json.dumps(users))
|
||||||
|
")
|
||||||
|
|
||||||
|
api_patch "core/groups/$group_pk/" "{\"users\": $updated_users}" > /dev/null
|
||||||
|
|
||||||
|
echo "Added $username (pk=$user_pk) to group '$group_name'"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_list() {
|
||||||
|
api_get "stages/invitation/invitations/?page_size=50" | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
if not data['results']:
|
||||||
|
print('No pending invitations.')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
print(f\"{'Token (itoken)':<40} {'Name':<50} {'Single-Use':<12} {'Expires':<25} {'Group'}\")
|
||||||
|
print('-' * 160)
|
||||||
|
for inv in data['results']:
|
||||||
|
token = inv['pk']
|
||||||
|
name = inv.get('name', '')
|
||||||
|
single = 'yes' if inv.get('single_use') else 'no'
|
||||||
|
expires = inv.get('expires') or 'never'
|
||||||
|
if expires != 'never':
|
||||||
|
expires = expires[:19]
|
||||||
|
group = inv.get('fixed_data', {}).get('group', '—')
|
||||||
|
print(f'{token:<40} {name:<50} {single:<12} {expires:<25} {group}')
|
||||||
|
print(f\"\\nTotal: {data['pagination']['count']}\")
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-help}" in
|
||||||
|
create) shift; cmd_create "$@" ;;
|
||||||
|
assign) shift; cmd_assign "$@" ;;
|
||||||
|
list) cmd_list ;;
|
||||||
|
*)
|
||||||
|
echo "Authentik Invitation Manager"
|
||||||
|
echo ""
|
||||||
|
echo "Usage:"
|
||||||
|
echo " $0 create <group-name> [--days N] Create single-use invite link"
|
||||||
|
echo " $0 assign <username> <group-name> Add user to group"
|
||||||
|
echo " $0 list Show pending invitations"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
@ -237,6 +237,49 @@ To protect a service via Authentik + Traefik forward auth:
|
||||||
- `X-authentik-name`
|
- `X-authentik-name`
|
||||||
- `X-authentik-groups`
|
- `X-authentik-groups`
|
||||||
|
|
||||||
|
## Invitation Management
|
||||||
|
|
||||||
|
### Create Invitation
|
||||||
|
```bash
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: Bearer $AUTHENTIK_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"https://authentik.viktorbarzin.me/api/v3/stages/invitation/invitations/" \
|
||||||
|
-d '{
|
||||||
|
"name": "invite-slug-name",
|
||||||
|
"single_use": true,
|
||||||
|
"fixed_data": {"group": "Target Group Name"},
|
||||||
|
"flow": "<invitation-enrollment-flow-pk>"
|
||||||
|
}'
|
||||||
|
# Returns PK which is the itoken
|
||||||
|
# Link: https://authentik.viktorbarzin.me/if/flow/invitation-enrollment/?itoken=<pk>
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Invitations
|
||||||
|
```bash
|
||||||
|
curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
|
||||||
|
"https://authentik.viktorbarzin.me/api/v3/stages/invitation/invitations/?page_size=50"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Invitation
|
||||||
|
```bash
|
||||||
|
curl -s -X DELETE -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
|
||||||
|
"https://authentik.viktorbarzin.me/api/v3/stages/invitation/invitations/<pk>/"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Helper Script
|
||||||
|
Use `.claude/scripts/authentik-invite.sh` for invitation management:
|
||||||
|
```bash
|
||||||
|
./authentik-invite.sh create "Group Name" [--days N]
|
||||||
|
./authentik-invite.sh assign <username> "Group Name"
|
||||||
|
./authentik-invite.sh list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Important Notes
|
||||||
|
- OAuth source `enrollment_flow` is set to `invitation-enrollment` -- new social login users require invitation
|
||||||
|
- Source updates require Django ORM (PATCH not supported on `sources/oauth/<slug>/`)
|
||||||
|
- Invitation `name` field must be a slug (letters, numbers, hyphens, underscores)
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
1. **API pagination**: All list endpoints return paginated results. Use `?page_size=50` or check `pagination.next` for more pages.
|
1. **API pagination**: All list endpoints return paginated results. Use `?page_size=50` or check `pagination.next` for more pages.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue