- 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
121 lines
4.1 KiB
Bash
Executable File
121 lines
4.1 KiB
Bash
Executable File
#!/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
|