Redesign the role model from 3 roles (platform-admin, admin, member) to 4 clear personas: - owner (org role): full tenant control — billing, team, apps, deploy - operator (org role): app lifecycle + observability, no billing/team - viewer (org role): read-only observability - saas-vendor (global role, hosted only): cross-tenant platform admin Bootstrap changes: - Rename org roles: admin→owner, member→operator, add viewer - Remove platform-admin global role (moved to vendor-seed) - admin user gets owner role, camel user gets viewer role - Custom JWT maps: owner→server:admin, operator→server:operator, viewer→server:viewer, saas-vendor→server:admin New docker/vendor-seed.sh for hosted SaaS environments only. Remove sidebar user/logout link (TopBar handles logout). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
5.4 KiB
Bash
136 lines
5.4 KiB
Bash
#!/bin/sh
|
|
set -e
|
|
|
|
# Cameleer SaaS — Vendor Seed Script
|
|
# Creates the saas-vendor global role and vendor user.
|
|
# Run ONCE on the hosted SaaS environment AFTER standard bootstrap.
|
|
# NOT part of docker-compose.yml — invoked manually or by CI.
|
|
|
|
LOGTO_ENDPOINT="${LOGTO_ENDPOINT:-http://logto:3001}"
|
|
MGMT_API_RESOURCE="https://default.logto.app/api"
|
|
API_RESOURCE_INDICATOR="https://api.cameleer.local"
|
|
PG_HOST="${PG_HOST:-postgres}"
|
|
PG_USER="${PG_USER:-cameleer}"
|
|
PG_DB_LOGTO="logto"
|
|
|
|
# Vendor credentials (override via env vars)
|
|
VENDOR_USER="${VENDOR_USER:-vendor}"
|
|
VENDOR_PASS="${VENDOR_PASS:-vendor}"
|
|
VENDOR_NAME="${VENDOR_NAME:-SaaS Vendor}"
|
|
|
|
log() { echo "[vendor-seed] $1"; }
|
|
pgpass() { PGPASSWORD="${PG_PASSWORD:-cameleer_dev}"; export PGPASSWORD; }
|
|
|
|
# Install jq + curl
|
|
apk add --no-cache jq curl >/dev/null 2>&1
|
|
|
|
# ============================================================
|
|
# Get Management API token
|
|
# ============================================================
|
|
|
|
log "Reading M2M credentials from bootstrap file..."
|
|
BOOTSTRAP_FILE="/data/logto-bootstrap.json"
|
|
if [ ! -f "$BOOTSTRAP_FILE" ]; then
|
|
log "ERROR: Bootstrap file not found at $BOOTSTRAP_FILE — run standard bootstrap first"
|
|
exit 1
|
|
fi
|
|
|
|
M2M_ID=$(jq -r '.m2mClientId' "$BOOTSTRAP_FILE")
|
|
M2M_SECRET=$(jq -r '.m2mClientSecret' "$BOOTSTRAP_FILE")
|
|
|
|
if [ -z "$M2M_ID" ] || [ "$M2M_ID" = "null" ] || [ -z "$M2M_SECRET" ] || [ "$M2M_SECRET" = "null" ]; then
|
|
log "ERROR: M2M credentials not found in bootstrap file"
|
|
exit 1
|
|
fi
|
|
|
|
log "Getting Management API token..."
|
|
TOKEN_RESPONSE=$(curl -s -X POST "${LOGTO_ENDPOINT}/oidc/token" \
|
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
-d "grant_type=client_credentials&client_id=${M2M_ID}&client_secret=${M2M_SECRET}&resource=${MGMT_API_RESOURCE}&scope=all")
|
|
TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token' 2>/dev/null)
|
|
[ -z "$TOKEN" ] || [ "$TOKEN" = "null" ] && { log "ERROR: Failed to get token"; exit 1; }
|
|
log "Got Management API token."
|
|
|
|
api_get() { curl -s -H "Authorization: Bearer $TOKEN" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || echo "[]"; }
|
|
api_post() { curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$2" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || true; }
|
|
|
|
# ============================================================
|
|
# Create saas-vendor global role
|
|
# ============================================================
|
|
|
|
log "Checking for saas-vendor role..."
|
|
EXISTING_ROLES=$(api_get "/api/roles")
|
|
VENDOR_ROLE_ID=$(echo "$EXISTING_ROLES" | jq -r '.[] | select(.name == "saas-vendor" and .type == "User") | .id')
|
|
|
|
if [ -n "$VENDOR_ROLE_ID" ]; then
|
|
log "saas-vendor role exists: $VENDOR_ROLE_ID"
|
|
else
|
|
# Collect all API resource scope IDs
|
|
EXISTING_RESOURCES=$(api_get "/api/resources")
|
|
API_RESOURCE_ID=$(echo "$EXISTING_RESOURCES" | jq -r ".[] | select(.indicator == \"$API_RESOURCE_INDICATOR\") | .id")
|
|
ALL_SCOPE_IDS=$(api_get "/api/resources/$API_RESOURCE_ID/scopes" | jq '[.[].id]')
|
|
|
|
log "Creating saas-vendor role with all scopes..."
|
|
VENDOR_ROLE_RESPONSE=$(api_post "/api/roles" "{
|
|
\"name\": \"saas-vendor\",
|
|
\"description\": \"SaaS vendor — full platform control across all tenants\",
|
|
\"type\": \"User\",
|
|
\"scopeIds\": $ALL_SCOPE_IDS
|
|
}")
|
|
VENDOR_ROLE_ID=$(echo "$VENDOR_ROLE_RESPONSE" | jq -r '.id')
|
|
log "Created saas-vendor role: $VENDOR_ROLE_ID"
|
|
fi
|
|
|
|
# ============================================================
|
|
# Create vendor user
|
|
# ============================================================
|
|
|
|
log "Checking for vendor user '$VENDOR_USER'..."
|
|
VENDOR_USER_ID=$(api_get "/api/users?search=$VENDOR_USER" | jq -r ".[] | select(.username == \"$VENDOR_USER\") | .id")
|
|
|
|
if [ -n "$VENDOR_USER_ID" ]; then
|
|
log "Vendor user exists: $VENDOR_USER_ID"
|
|
else
|
|
log "Creating vendor user '$VENDOR_USER'..."
|
|
VENDOR_RESPONSE=$(api_post "/api/users" "{
|
|
\"username\": \"$VENDOR_USER\",
|
|
\"password\": \"$VENDOR_PASS\",
|
|
\"name\": \"$VENDOR_NAME\"
|
|
}")
|
|
VENDOR_USER_ID=$(echo "$VENDOR_RESPONSE" | jq -r '.id')
|
|
log "Created vendor user: $VENDOR_USER_ID"
|
|
fi
|
|
|
|
# Assign saas-vendor role
|
|
if [ -n "$VENDOR_ROLE_ID" ] && [ "$VENDOR_ROLE_ID" != "null" ]; then
|
|
api_post "/api/users/$VENDOR_USER_ID/roles" "{\"roleIds\": [\"$VENDOR_ROLE_ID\"]}" >/dev/null 2>&1
|
|
log "Assigned saas-vendor role."
|
|
fi
|
|
|
|
# ============================================================
|
|
# Add vendor to all existing organizations with owner role
|
|
# ============================================================
|
|
|
|
log "Adding vendor to all organizations..."
|
|
ORG_OWNER_ROLE_ID=$(api_get "/api/organization-roles" | jq -r '.[] | select(.name == "owner") | .id')
|
|
ORGS=$(api_get "/api/organizations")
|
|
ORG_COUNT=$(echo "$ORGS" | jq 'length')
|
|
|
|
for i in $(seq 0 $((ORG_COUNT - 1))); do
|
|
ORG_ID=$(echo "$ORGS" | jq -r ".[$i].id")
|
|
ORG_NAME=$(echo "$ORGS" | jq -r ".[$i].name")
|
|
api_post "/api/organizations/$ORG_ID/users" "{\"userIds\": [\"$VENDOR_USER_ID\"]}" >/dev/null 2>&1
|
|
if [ -n "$ORG_OWNER_ROLE_ID" ] && [ "$ORG_OWNER_ROLE_ID" != "null" ]; then
|
|
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
-d "{\"organizationRoleIds\": [\"$ORG_OWNER_ROLE_ID\"]}" \
|
|
"${LOGTO_ENDPOINT}/api/organizations/$ORG_ID/users/$VENDOR_USER_ID/roles" >/dev/null 2>&1
|
|
fi
|
|
log " Added to org '$ORG_NAME' ($ORG_ID) with owner role."
|
|
done
|
|
|
|
log ""
|
|
log "=== Vendor seed complete! ==="
|
|
log " Vendor user: $VENDOR_USER / $VENDOR_PASS"
|
|
log " Role: saas-vendor (global) + owner (in all orgs)"
|
|
log " This user has platform:admin scope and cross-tenant access."
|