feat(it-ops): live inventory, drift API, Keycloak IT role, portal sync hint
- Add scripts/it-ops (Proxmox collector, IPAM drift, export orchestrator) - Add sankofa-it-read-api stub with optional CORS and refresh - Add systemd examples for read API, weekly inventory export, timer - Add live-inventory-drift GitHub workflow (dispatch + weekly) - Add IT controller spec, runbooks, Keycloak ensure-it-admin-role script - Note IT_READ_API env on portal sync completion output Made-with: Cursor
This commit is contained in:
120
scripts/deployment/keycloak-sankofa-ensure-it-admin-role.sh
Executable file
120
scripts/deployment/keycloak-sankofa-ensure-it-admin-role.sh
Executable file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
# Create Keycloak realm role sankofa-it-admin if missing (IT operations portal /it gate).
|
||||
# Runs Admin API against http://127.0.0.1:8080 inside the Keycloak CT (same pattern as
|
||||
# keycloak-sankofa-ensure-client-redirects-via-proxmox-pct.sh).
|
||||
#
|
||||
# After the role exists, assign it to IT staff in Keycloak Admin (Users → Role mapping)
|
||||
# or map it to a group and add a token mapper if you rely on group claims.
|
||||
#
|
||||
# Env: KEYCLOAK_ADMIN_PASSWORD in repo .env; optional KEYCLOAK_REALM (default master),
|
||||
# KEYCLOAK_CT_VMID (7802), PROXMOX_HOST.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/deployment/keycloak-sankofa-ensure-it-admin-role.sh [--dry-run]
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
# shellcheck source=/dev/null
|
||||
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
|
||||
if [ -f "$PROJECT_ROOT/.env" ]; then
|
||||
set +u
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
source "$PROJECT_ROOT/.env" 2>/dev/null || true
|
||||
set +a
|
||||
set -u
|
||||
fi
|
||||
|
||||
PROXMOX_HOST="${PROXMOX_HOST:-${PROXMOX_HOST_R630_01:-192.168.11.11}}"
|
||||
KEYCLOAK_CT_VMID="${KEYCLOAK_CT_VMID:-${SANKOFA_KEYCLOAK_VMID:-7802}}"
|
||||
REALM="${KEYCLOAK_REALM:-master}"
|
||||
ADMIN_USER="${KEYCLOAK_ADMIN:-admin}"
|
||||
ADMIN_PASS="${KEYCLOAK_ADMIN_PASSWORD:-}"
|
||||
ROLE_NAME="${SANKOFA_IT_ADMIN_ROLE_NAME:-sankofa-it-admin}"
|
||||
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15)
|
||||
|
||||
DRY=0
|
||||
[[ "${1:-}" == "--dry-run" ]] && DRY=1
|
||||
|
||||
if [ -z "$ADMIN_PASS" ]; then
|
||||
echo "KEYCLOAK_ADMIN_PASSWORD is not set in .env" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$DRY" = 1 ]; then
|
||||
echo "[dry-run] Would ssh root@${PROXMOX_HOST} pct exec ${KEYCLOAK_CT_VMID} -- python3 (ensure realm role ${ROLE_NAME} in realm ${REALM})"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "root@${PROXMOX_HOST}" \
|
||||
"pct exec ${KEYCLOAK_CT_VMID} -- env KC_PASS=\"${ADMIN_PASS}\" ADMUSER=\"${ADMIN_USER}\" REALM=\"${REALM}\" ROLE_NAME=\"${ROLE_NAME}\" python3 -u -" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
base = "http://127.0.0.1:8080"
|
||||
realm = os.environ["REALM"]
|
||||
role_name = os.environ["ROLE_NAME"]
|
||||
admin_user = os.environ["ADMUSER"]
|
||||
password = os.environ["KC_PASS"]
|
||||
|
||||
|
||||
def post_form(url: str, data: dict) -> dict:
|
||||
body = urllib.parse.urlencode(data).encode()
|
||||
req = urllib.request.Request(url, data=body, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
return json.loads(resp.read().decode())
|
||||
|
||||
|
||||
tok = post_form(
|
||||
f"{base}/realms/master/protocol/openid-connect/token",
|
||||
{
|
||||
"grant_type": "password",
|
||||
"client_id": "admin-cli",
|
||||
"username": admin_user,
|
||||
"password": password,
|
||||
},
|
||||
)
|
||||
access = tok.get("access_token")
|
||||
if not access:
|
||||
raise SystemExit(f"token failed: {tok}")
|
||||
|
||||
headers = {"Authorization": f"Bearer {access}"}
|
||||
role_url = f"{base}/admin/realms/{realm}/roles/{urllib.parse.quote(role_name, safe='')}"
|
||||
req_get = urllib.request.Request(role_url, headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req_get, timeout=60) as resp:
|
||||
if resp.getcode() in (200, 204):
|
||||
print(f"Realm role {role_name!r} already exists in {realm!r}.", flush=True)
|
||||
raise SystemExit(0)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code != 404:
|
||||
err = e.read().decode() if e.fp else str(e)
|
||||
raise SystemExit(f"GET role failed HTTP {e.code}: {err}") from e
|
||||
|
||||
payload = json.dumps(
|
||||
{
|
||||
"name": role_name,
|
||||
"description": "Sankofa IT operations (portal /it, inventory read API consumers)",
|
||||
"clientRole": False,
|
||||
}
|
||||
).encode()
|
||||
req_post = urllib.request.Request(
|
||||
f"{base}/admin/realms/{realm}/roles",
|
||||
data=payload,
|
||||
headers={**headers, "Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req_post, timeout=120) as resp:
|
||||
if resp.getcode() not in (200, 201):
|
||||
raise SystemExit(f"create role unexpected HTTP {resp.getcode()}")
|
||||
except urllib.error.HTTPError as e:
|
||||
err = e.read().decode() if e.fp else str(e)
|
||||
raise SystemExit(f"POST role failed HTTP {e.code}: {err}") from e
|
||||
|
||||
print(f"Created realm role {role_name!r} in realm {realm!r}. Assign it to IT users in Admin Console.", flush=True)
|
||||
PY
|
||||
@@ -106,5 +106,6 @@ echo "✅ Done. Verify:"
|
||||
echo " curl -sS http://${IP_SANKOFA_PORTAL:-192.168.11.51}:3000/ | head -c 120"
|
||||
echo " curl -sSI https://portal.sankofa.nexus/api/auth/signin | head -n 15"
|
||||
echo " https://portal.sankofa.nexus/ (via NPM; corporate apex is sankofa.nexus → IP_SANKOFA_PUBLIC_WEB)"
|
||||
echo " IT /it console: set IT_READ_API_URL (and optional IT_READ_API_KEY) in CT ${CT_APP_DIR}/.env — see portal/.env.example"
|
||||
echo ""
|
||||
echo "Legacy apex auth URL only if needed: SANKOFA_PORTAL_NEXTAUTH_URL=https://sankofa.nexus $0"
|
||||
|
||||
Reference in New Issue
Block a user