toga-ai 1.0.906 → 1.0.908
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-28
|
|
10
10
|
owners: ["dfranks", "jcardinal", "mhammontree", "ajean", "bala", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Error.php
|
|
@@ -687,6 +687,10 @@ read you feed it — `_Error` promotes a *"read property on int"* warning into a
|
|
|
687
687
|
|
|
688
688
|
## Gotchas
|
|
689
689
|
|
|
690
|
+
- **`Logs.Issue` starts 2026-08-03 on prod — there are no rows before it.** The table went live with
|
|
691
|
+
that migration, so **any 500 older than 2026-08-03 has no Issue row at all**: looking its
|
|
692
|
+
`error.id` up returns nothing, and the `Logs.Api` row is the only record you get. An empty result
|
|
693
|
+
for an old error is the expected state, not a capture failure.
|
|
690
694
|
- **⚠ A missing `Logs.Issue` column silently disables ALL error capture on that environment** —
|
|
691
695
|
see the open defect above. Symptom: `identifiers.captureFailure` in the API response and nothing
|
|
692
696
|
anywhere else.
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-28
|
|
10
10
|
owners: [bala, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/Users/DelegatorUser.php
|
|
@@ -122,6 +122,19 @@ paging. Two hard rules in
|
|
|
122
122
|
- **api2 does log 4xx rows** (59 × 403 on client-sandbox in the last 30 days), so a missing 403 in
|
|
123
123
|
`Logs_<Client>.Api` means the call never happened or hit another environment — check the host
|
|
124
124
|
before blaming the log.
|
|
125
|
+
- **⚠ `POST /v2/auth/delegator` 500s on a PUBLIC (logged-out) token — known, deliberately not
|
|
126
|
+
fixed.** `V2.php` L896 checks `property_exists($this->jwtPayload->id, 'delegator')`; when the
|
|
127
|
+
claim is absent the `else` reads `$this->jwtPayload->id->client->user->uuid` at **L903** with no
|
|
128
|
+
check that `user` exists. A public token's `id.client` is `{uuid, slug, roles, api}` and carries
|
|
129
|
+
**no `user` key**, so PHP raises *"Undefined property: stdClass::$user"* and the caller gets a
|
|
130
|
+
**500 `EO-1`** instead of a clean **401 `EN-3`**. The same file already guards this exact shape
|
|
131
|
+
~50 lines lower (L952, the expired-token `elseif`: `property_exists($this->jwtPayload->id->client,
|
|
132
|
+
'user')`) — L903 is the one place that does not. Reachable **with no login**. Evidence:
|
|
133
|
+
`Logs.Issue` reference **T2**, 6 occurrences in 11 s on 2026-09-04 and none since; the failing
|
|
134
|
+
token decoded out of `Logs.Api.requestHeaders` was slug `Compass_Usa`, roles `[1,3]`, `api`
|
|
135
|
+
present, no `user`. **Decision (2026-09-28, bala): leave it.** Dormant and low impact; add the
|
|
136
|
+
one-line guard returning `DEFINED_MESSAGE_ERROR_INVALID_ACCESS_TOKEN` next time someone is in
|
|
137
|
+
that block — do not open a ticket for it.
|
|
125
138
|
- **Role 3 (API) cannot reach this record.** Its grant for record 362 was written into `Core`, but
|
|
126
139
|
`aclDatabase = CLIENT` means Core rows are never consulted — so no integration can read or write
|
|
127
140
|
`user-delegator-users` through api2 until the grant is mirrored into each `Client_*` DB.
|
|
@@ -154,6 +167,8 @@ paging. Two hard rules in
|
|
|
154
167
|
|
|
155
168
|
## Change history
|
|
156
169
|
|
|
170
|
+
- 2026-09-28 — Recorded the `/auth/delegator` public-token 500 at `V2.php` L903 (no `user` guard,
|
|
171
|
+
`Logs.Issue` T2) and the decision to leave it unfixed. (bala)
|
|
157
172
|
- 2026-09-22 — Front end now switches the token, so the gate is live: recorded that
|
|
158
173
|
`originalUserId` is still NULL on client-sandbox during a real Order For session, that the gate
|
|
159
174
|
also returns 403 EZ-1 for admin View As of anyone off the list, and that api2 does log 4xx rows.
|
|
@@ -6,7 +6,7 @@ project: SAML SSO Gateway
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-28
|
|
10
10
|
owners: ["rgirish", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- saml/index.php
|
|
@@ -18,6 +18,8 @@ files:
|
|
|
18
18
|
- _underscore/Cloud.php
|
|
19
19
|
related:
|
|
20
20
|
- 2.0/apps/_underscore/architecture.md
|
|
21
|
+
- features/gateway-environments-and-redirect-safety.md
|
|
22
|
+
- features/acs-assertion-trust-gaps.md
|
|
21
23
|
---
|
|
22
24
|
|
|
23
25
|
Gateway internals of the `saml` SAML 2.0 SP; open when working on `/acs`, `/meta`, cert/decryption, or deploy of the SSO gateway.
|
|
@@ -69,7 +71,7 @@ Identity mapping lives in `_underscore/Model/<ClientIdentifier>/ClientAuthentica
|
|
|
69
71
|
|
|
70
72
|
- **PHP 8.1+ in production**, on Amazon Linux with **Apache/httpd fronting PHP-FPM**. `composer.json` pins `platform.php` to **`7.2.33` for dependency resolution only** — that is not the runtime version; do not treat it as the target.
|
|
71
73
|
- **Config files are `Config/<environment>.ini`, selected by `_Environment::$name`.** Production sets `time_limit = 300`, `debug_mode = 0`. Many per-developer/per-stage configs exist (`alpha`, `beta`, `gamma`, `dev-*`, `rohan-mac`).
|
|
72
|
-
- **SP host per environment is a framework constant**, not local config: `_Model_Core_ClientAuthentication::SAML_ASSERTION_CONSUMER_SERVICE_HOST_URL_BY_ENVIRONMENT` — `dev → http://saml`, `
|
|
74
|
+
- **SP host per environment is a framework constant**, not local config: `_Model_Core_ClientAuthentication::SAML_ASSERTION_CONSUMER_SERVICE_HOST_URL_BY_ENVIRONMENT` — `dev → http://saml`, `sandbox-client → https://saml.client.sandbox.togahub.com`, `production → https://saml.togahub.com`. This map drives **both** the outbound ACS URL **and** what `/meta` publishes, so a wrong entry cross-wires two environments. `beta` currently points at production, so `saml.beta.togahub.com/meta` advertises production's entityID and ACS; the fix (`beta → https://saml.beta.togahub.com`) is on `TRUE-81993` / `_sandbox-client` and **not yet merged to `_production`**. Read the map through `samlHostUrl($slug)`, which throws — the bare array index broke 23 of the 27 `Core.Environments` rows. Three gateway hosts exist, one per AWS account — see [gateway environments & redirect safety](features/gateway-environments-and-redirect-safety.md).
|
|
73
75
|
- **Apache `.platform` configs (keep these):** `enforce_https.conf` redirects any request with `X-Forwarded-Proto != https` to HTTPS — **SAML must run over TLS only; do not weaken it**. `long_gateway_timeout.conf` sets `ProxyTimeout 1800` / `Timeout 1800` (same override as `apiproxy`). `php_include_underscore.config` adds `/var/www/html/_underscore` to PHP `include_path`.
|
|
74
76
|
|
|
75
77
|
## Deployment
|
|
@@ -93,7 +95,8 @@ Identity mapping lives in `_underscore/Model/<ClientIdentifier>/ClientAuthentica
|
|
|
93
95
|
- **SAML signature verification is commented out — repo's top security priority.** The IdP X509 cert verification block in `/acs` is disabled, so the gateway accepts any well-formed status-success `SAMLResponse` without proving it was signed by the expected IdP. The only remaining gate is the encrypted RelayState, which authenticates the *originating TOGa request* — not the *asserting IdP*. Threat: an attacker who can craft/replay a `SAMLResponse` (or a malicious/compromised IdP) can forge an identity assertion for any user. Re-enabling per-IdP X509 signature verification is the single highest-impact security fix. The per-client IdP verification certs already exist in source as base64 blobs (Compass, Office Depot, Prudential, RUMCSI, Agilant) — they are what a re-enabled verifier would load; verification is per-client and each IdP's signing cert must match.
|
|
94
96
|
- **Plaintext secrets in source — move to AWS SSM Parameter Store.** Checked-in credentials, all live and to be rotated: `saml/Config/production.ini` holds a **DB password, an AWS access key id + secret, and NetSuite credentials** in cleartext; `saml/Controller/Index.php` hardcodes the **Sentry DSN**; `.ebextensions/git.json` carries a **GitHub PAT** (same leak class as `apiproxy`). Migrate to AWS SSM Parameter Store (SecureString), load at runtime; source tree should reference parameter names only, never literal credential values.
|
|
95
97
|
- **Stale test scaffolding in `/acs` — remove it.** A large commented-out block in `saml/Controller/Index.php` hardcodes captured real `SAMLResponse` payloads and a synthetic `RelayState` for Office Depot / Compass testing. It is inert (commented out) but captured assertions living in source are a confidentiality/hygiene risk — delete it.
|
|
96
|
-
- **RelayState
|
|
98
|
+
- **RelayState is still not replay-proof, even after the TTL fix.** The check was written backwards (`$relayState->time <= time() + MAX_TIME_TO_LIVE`, true for every past timestamp) and is now `abs(time() - $relayState->time) <= MAX_TIME_TO_LIVE` on `TRUE-81993` / `_sandbox-client`. **That is hygiene, not a control:** `_String::encryptWithKey()` is AES-256-CBC with **no MAC and an attacker-supplied IV**, so the `time` field can be rewritten without the key, and there is no nonce or single-use marking. Fix direction is AES-256-GCM or an HMAC checked with `hash_equals()` before decrypt. See [/acs assertion trust gaps](features/acs-assertion-trust-gaps.md).
|
|
99
|
+
- **`/acs` must redirect only to a URL rebuilt by `safeRedirectUrl()`.** `_Page::headerLocationRedirect()` is a bare `header('Location: ')`, and the `?saml=` payload exchanges for real tokens at `POST /auth/encrypted-user-uuid` — so the destination is a credential delivery target. Redirect to the **return value**, never to the stored `Core.Domains.domain`: appending `'?saml='` to the stored string lets a stored path or query steal the handoff. A `Core.Domains` row (owned by the right client + environment) **is** the authorization — there is deliberately no host allowlist; do not add one. On `TRUE-81993` / `_sandbox-client`, not yet on `_production`.
|
|
97
100
|
- **`/meta` cert lines must be `trim()`-ed (CRLF).** The `.crt` files in `_underscore/Assets/ssl` are CRLF-encoded; cert-format code does `explode("\n", ...)` and indents each line, so without a per-line `trim()` a stray `\r` ends up on every base64 line inside `<ds:X509Certificate>`. Trim each line in both signing and encryption blocks.
|
|
98
101
|
- **SP encryption certs load from Secrets Manager at request time.** `/acs` holds no encryption key material on disk (`_underscore/Assets/ssl/` no longer carries the SP encryption key). Rotation = edit the `ENCRYPTION_SECRET_NAMES` list + the secrets themselves, not a redeploy of on-disk `.crt`/`.key` files.
|
|
99
102
|
- **Non-prod SAML depends on prod KMS/Secrets Manager; one SP key serves every env** — a non-prod leak is a prod key leak. `ENCRYPTION_SECRET_NAMES` must hold full ARNs (short names resolve only in the caller's account).
|
package/package.json
CHANGED