From 35bd13de8be7729486ffd794d3e067d3674d0477 Mon Sep 17 00:00:00 2001 From: reachableceo Date: Wed, 9 Sep 2026 22:43:28 -0500 Subject: [PATCH] glpi: REST auth recipe (app_token + encrypted-at-rest trap), SSO script known-issue GLPI 11 encrypts api/app tokens at rest and decrypts before comparing; the DB ciphertext never authenticates. Document the working initSession?user_token+app_token recipe, per-user token minting via GLPIKey, REST account provisioning, and the singlesignon plugin callback breakage. https://projects.knownelement.com/issues/947#note-5519 --- skills/agent-provisioning/SKILL.md | 21 +++++-- .../scripts/glpi-sso-login.sh | 59 +++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) create mode 100755 skills/agent-provisioning/scripts/glpi-sso-login.sh diff --git a/skills/agent-provisioning/SKILL.md b/skills/agent-provisioning/SKILL.md index 1ba9d50..595ed72 100644 --- a/skills/agent-provisioning/SKILL.md +++ b/skills/agent-provisioning/SKILL.md @@ -143,7 +143,7 @@ scripts/discourse-sso-login.sh [vault-item] # /auth/oidc (+ CSRF + | System | SSO start point | Gotchas | |---|---|---| -| GLPI | `https://cmdb.knownelement.com/plugins/singlesignon/front/callback.php/provider/1?remember=1` | account auto-provisioned; no scripted flow yet | +| GLPI | `https://cmdb.knownelement.com/plugins/singlesignon/front/callback.php/provider/1?remember=1` | script exists (`glpi-sso-login.sh`) but the plugin callback is BROKEN on GLPI 11 (include-path + session/CSRF Access denied) — provision accounts via REST instead (recipe below) | | Gitea | `https://git.knownelement.com/user/oauth2/cloudron` | redirects to `client_id=-oidc`; gitea may NORMALIZE the username (e.g. `tsgstaff-coo-vpperf` → `vpperf`) — verify which login got created | | Discourse | `https://community.turnsys.com/auth/oidc` | hostname is community.**turnsys.com**; provider name is `oidc`, NOT openid_connect; needs `/session/csrf` token then a confirm-page authenticity_token POST | | Redmine | `https://projects.knownelement.com/oauth?oauth_provider=1` | the "bounce to /login" mystery = the plugin's GET form needs `oauth_provider=1` | @@ -188,9 +188,22 @@ centrally — no per-identity session needed: `/admin/users/list/active.json` first. Admin Api-Key minting uses the same model; admin key for vptechops is in `creds/vptechops-discourse` (`admin_api_key`). -- **GLPI**: DEFERRED on #947 — REST `user_token` auth 401s even for a - super-admin with api_token/personal_token set; do not burn time re-deriving - that failure. +- **GLPI** (WORKING as of 2026-09-09, #947 root-caused): REST needs BOTH + `user_token` AND `app_token`: + `GET /apirest.php/initSession?user_token=&app_token=<plaintext app_token>` + → `session_token`; then `Session-Token:` + `App-Token:` headers. THE TRAP: + GLPI 11 ENCRYPTS tokens at rest (`are_apiclients_tokens_encrypted` / + `are_users_tokens_encrypted`, sodium XChaCha20, key `/app/data/config/glpicrypt.key`) + and DECRYPTS before comparing — the ciphertext you see in + `glpi_users.api_token` / `glpi_apiclients.app_token` NEVER authenticates. + Plaintext values: app_token = `creds/glpi` password; glpi super-admin + api_token = `creds/glpi` username; per-user tokens are vaulted in each + identity's master item (`glpi_api_token`). To mint: `openssl rand -hex 20`, + encrypt with `GLPIKey('/app/data/config')->encrypt()` inside the + container, write back via SQL (plaintext legacy rows never authenticate). + Create accounts via REST as the glpi super-admin (`POST /User` with + name/email matching the Cloudron identity) — the SSO plugin's + auto-register is broken on GLPI 11 (#947). Store each key in the vault item immediately (schema below), and always verify the write (`sm get … --field …` readback) — `sm setfield` 400s diff --git a/skills/agent-provisioning/scripts/glpi-sso-login.sh b/skills/agent-provisioning/scripts/glpi-sso-login.sh new file mode 100755 index 0000000..db76898 --- /dev/null +++ b/skills/agent-provisioning/scripts/glpi-sso-login.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# GLPI SSO first-login via Cloudron OIDC (singlesignon plugin, callback.php/provider/1). +# usage: glpi-sso-login.sh <cloudron-username> [vault-item] +# Verifies a logged-in session at front/preference.php (HTTP 200 + no login redirect). +# KNOWN ISSUE 2026-09-09: the singlesignon plugin callback fails on GLPI 11 (include-path + +# session/CSRF Access denied) — interactive GLPI SSO broken; REST provisioning used instead +# (see SKILL.md GLPI recipe + Redmine #947). Script kept for when the plugin is fixed. +set -uo pipefail +U="${1:?usage: glpi-sso-login.sh <cloudron-username> [vault-item]}" +ITEM="${2:-$U Cloudron}" +BASE="https://cmdb.knownelement.com" +IDP="https://my.knownelement.com" +JAR="/tmp/glpi-jar-$U.txt"; rm -f "$JAR" +SM() { if [ "$(id -un)" = "TSGCOO" ]; then /data2/TSGCOO/.local/bin/sm "$@" </dev/null; else sudo -u TSGCOO /data2/TSGCOO/.local/bin/sm "$@" </dev/null; fi; } +PW=$(SM get "$ITEM" --field password) +[ -n "$PW" ] || { echo "FAIL: no password in vault item $ITEM" >&2; exit 1; } +SEED=$(SM get "$ITEM" --field totp_seed 2>/dev/null || true) + +# entry: the SSO plugin callback kickoff (provider id 1 = KNEL Cloud) +INT=$(curl -sk -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$BASE/plugins/singlesignon/front/callback.php/provider/1?remember=1") +case "$INT" in + */openid/interaction/*) : ;; + *) echo "FAIL: no interaction reached: $INT" >&2; exit 3 ;; +esac +UIDPATH=$(printf '%s' "$INT" | grep -o '/openid/interaction/[^?]*') + +BODY="{\"username\":\"$U\",\"password\":\"$PW\"" +if [ -n "$SEED" ]; then + TCODE=$(bash "$(dirname "$0")/totp.sh" "$SEED") + BODY="$BODY,\"totpToken\":\"$TCODE\"" +fi +BODY="$BODY}" +LOGIN_RESP=$(curl -sk -b "$JAR" -c "$JAR" -X POST "$IDP$UIDPATH/login" -H 'Content-Type: application/json' -d "$BODY") +unset BODY +case "$LOGIN_RESP" in + *redirectTo*) : ;; + *) echo "FAIL: login rejected: $LOGIN_RESP" >&2; exit 5 ;; +esac +RED=$(printf '%s' "$LOGIN_RESP" | sed -n 's/.*"redirectTo":"\([^"]*\)".*/\1/p') + +FINAL=$(curl -sk -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$RED") +case "$FINAL" in + */openid/interaction/*) + CUIDPATH=$(printf '%s' "$FINAL" | grep -o '/openid/interaction/[^?]*') + CLOC=$(curl -sk -b "$JAR" -c "$JAR" -D - -o /dev/null -X POST "$IDP$CUIDPATH/confirm" -d '' | grep -i '^location:' | head -1 | tr -d '\r' | sed 's/^[Ll]ocation: //') + [ -n "$CLOC" ] || { echo "FAIL: consent confirm produced no redirect" >&2; exit 6; } + CLOC=$(printf '%s' "$CLOC" | sed 's|^https//|https://|; s|^http//|http://|') + case "$CLOC" in http://*|https://*) : ;; /*) CLOC="$IDP$CLOC" ;; *) CLOC="$IDP/$CLOC" ;; esac + FINAL=$(curl -sk -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$CLOC") + ;; +esac + +CODE=$(curl -sk -b "$JAR" -o /dev/null -w '%{http_code}' "$BASE/front/preference.php") +if [ "$CODE" = "200" ]; then + echo "OK: $U logged into GLPI (session verified at preference.php)" + exit 0 +fi +echo "FAIL: no GLPI session (preference.php HTTP $CODE), landed: $FINAL" >&2 +exit 7