when: event: manual clone: git: image: woodpeckerci/plugin-git settings: attempts: 5 backoff: 10s steps: - name: validate-inputs image: alpine commands: - | if [ -z "$USERNAME" ] || [ -z "$EMAIL" ]; then echo "ERROR: USERNAME and EMAIL variables are required" echo "Trigger with: POST /api/repos/1/pipelines {branch:master, variables:{USERNAME:x, EMAIL:y}}" exit 1 fi # Validate username: lowercase alphanumeric + dash/underscore, 2-63 chars if ! echo "$USERNAME" | grep -qE '^[a-z0-9][a-z0-9_-]{0,61}[a-z0-9]$'; then echo "ERROR: USERNAME must be 2-63 chars, lowercase alphanumeric/dash/underscore" exit 1 fi # Validate email: basic format check if ! echo "$EMAIL" | grep -qE '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; then echo "ERROR: EMAIL must be a valid email address" exit 1 fi echo "Provisioning user: $USERNAME ($EMAIL)" echo "export PROVISION_USERNAME='$USERNAME'" > .provision-env echo "export PROVISION_EMAIL='$EMAIL'" >> .provision-env - name: prepare image: alpine commands: - "apk update && apk add jq curl git git-crypt" # git-crypt for secrets/ directory - | curl -k https://10.0.20.100:6443/api/v1/namespaces/woodpecker/configmaps/git-crypt-key \ -H "Authorization:Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ | jq -r .data.key | base64 -d > /tmp/key - "git-crypt unlock /tmp/key; rm -f /tmp/key" # Vault: authenticate via K8s service account JWT - | SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) VAULT_TOKEN=$(curl -s -X POST http://vault-active.vault.svc.cluster.local:8200/v1/auth/kubernetes/login \ -d "{\"role\":\"ci\",\"jwt\":\"$SA_TOKEN\"}" | jq -r .auth.client_token) echo "export VAULT_TOKEN=$VAULT_TOKEN" > .vault-env echo "export VAULT_ADDR=http://vault-active.vault.svc.cluster.local:8200" >> .vault-env - name: update-vault-kv image: alpine commands: - "apk update && apk add jq curl" # Read current platform secret - | . .provision-env && . .vault-env CURRENT=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" \ "$VAULT_ADDR/v1/secret/data/platform" | jq -r '.data.data') # Parse current k8s_users (stored as JSON string) CURRENT_USERS=$(echo "$CURRENT" | jq -r '.k8s_users') # Check if user already exists if echo "$CURRENT_USERS" | jq -e --arg u "$PROVISION_USERNAME" '.[$u]' >/dev/null 2>&1; then echo "User $PROVISION_USERNAME already exists in k8s_users — skipping Vault KV update" exit 0 fi # Add new user with convention defaults UPDATED_USERS=$(echo "$CURRENT_USERS" | jq --arg u "$PROVISION_USERNAME" --arg e "$PROVISION_EMAIL" \ '. + {($u): {"role":"namespace-owner","email":$e,"namespaces":[$u],"domains":[],"quota":{"cpu_requests":"2","memory_requests":"4Gi","memory_limits":"8Gi","pods":"20"}}}') # Write back full platform secret with updated k8s_users (as JSON string) PAYLOAD=$(echo "$CURRENT" | jq --arg users "$UPDATED_USERS" '.k8s_users = $users') curl -s -X POST -H "X-Vault-Token: $VAULT_TOKEN" \ "$VAULT_ADDR/v1/secret/data/platform" \ -d "{\"data\": $PAYLOAD}" | jq . echo "Added $PROVISION_USERNAME to k8s_users in Vault" - name: create-authentik-groups image: alpine commands: - "apk update && apk add jq curl" - | source .provision-env && source .vault-env # Get Authentik API token from Vault AUTHENTIK_TOKEN=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" \ "$VAULT_ADDR/v1/secret/data/viktor" | jq -r '.data.data.authentik_api_token') AUTHENTIK_URL="https://authentik.viktorbarzin.me" # Create sops-USERNAME group if it doesn't exist SOPS_GROUP="sops-$PROVISION_USERNAME" EXISTING=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \ "$AUTHENTIK_URL/api/v3/core/groups/?name=$SOPS_GROUP" | jq -r '.results | length') if [ "$EXISTING" = "0" ]; then GROUP_PAYLOAD=$(jq -n --arg name "$SOPS_GROUP" '{"name": $name, "is_superuser": false}') GROUP_PK=$(curl -s -X POST -H "Authorization: Bearer $AUTHENTIK_TOKEN" \ -H "Content-Type: application/json" \ "$AUTHENTIK_URL/api/v3/core/groups/" \ -d "$GROUP_PAYLOAD" | jq -r '.pk') echo "Created Authentik group $SOPS_GROUP (pk=$GROUP_PK)" else GROUP_PK=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \ "$AUTHENTIK_URL/api/v3/core/groups/?name=$SOPS_GROUP" | jq -r '.results[0].pk') echo "Authentik group $SOPS_GROUP already exists (pk=$GROUP_PK)" fi # Find the user by username USER_PK=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \ "$AUTHENTIK_URL/api/v3/core/users/?username=$PROVISION_USERNAME" | jq -r '.results[0].pk') if [ "$USER_PK" = "null" ] || [ -z "$USER_PK" ]; then echo "WARNING: User $PROVISION_USERNAME not found in Authentik — group assignment skipped" echo "The user may not have signed up yet. Groups will need manual assignment." exit 0 fi # Add user to sops group CURRENT_MEMBERS=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \ "$AUTHENTIK_URL/api/v3/core/groups/$GROUP_PK/" | jq -r '.users') UPDATED_MEMBERS=$(echo "$CURRENT_MEMBERS" | jq --argjson uid "$USER_PK" '. + [$uid] | unique') curl -s -X PATCH -H "Authorization: Bearer $AUTHENTIK_TOKEN" \ -H "Content-Type: application/json" \ "$AUTHENTIK_URL/api/v3/core/groups/$GROUP_PK/" \ -d "{\"users\": $UPDATED_MEMBERS}" | jq . echo "Added user $PROVISION_USERNAME (pk=$USER_PK) to group $SOPS_GROUP" - name: notify-apply-needed image: curlimages/curl commands: - | . .provision-env echo "User $PROVISION_USERNAME added to Vault KV and Authentik sops group." echo "Manual step needed: apply vault + rbac + woodpecker stacks." echo " cd stacks/vault && ../../scripts/tg apply --non-interactive" echo " cd stacks/rbac && ../../scripts/tg apply --non-interactive" echo " cd stacks/woodpecker && ../../scripts/tg apply --non-interactive" - name: slack image: curlimages/curl commands: - | . .provision-env 2>/dev/null || true curl -s -X POST -H 'Content-type: application/json' \ --data "{\"channel\":\"general\",\"text\":\"Woodpecker CI: User provisioned — $PROVISION_USERNAME added to Vault KV + Authentik. Run: cd stacks/vault && ../../scripts/tg apply --non-interactive && cd ../rbac && ../../scripts/tg apply --non-interactive\"}" \ "$SLACK_WEBHOOK" || true environment: SLACK_WEBHOOK: from_secret: slack_webhook when: status: [success, failure]