toga-ai 1.0.935 → 1.0.937
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.
- package/knowledge/1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md +11 -7
- package/knowledge/1.0/apps/togadesk/INDEX.md +2 -1
- package/knowledge/1.0/apps/togadesk/architecture.md +12 -4
- package/knowledge/1.0/apps/togadesk/features/staff-login-authentication.md +17 -3
- package/knowledge/1.0/apps/togadesk/features/staff-sso-login.md +72 -0
- package/knowledge/1.0/apps/tools/features/saml-sso-auth.md +8 -3
- package/knowledge/1.0/apps/worker/INDEX.md +1 -0
- package/knowledge/1.0/apps/worker/workflows/netsuite-addressbook-location-mapping.md +51 -0
- package/knowledge/1.0/apps/worker/workflows/reprocessing-one-netsuite-order.md +3 -2
- package/knowledge/2.0/apps/saml/features/acs-assertion-trust-gaps.md +16 -3
- package/knowledge/2.0/apps/saml/features/downstream-integration-contract.md +18 -3
- package/knowledge/2.0/apps/worker2/features/netsuite-transferorder-outbound-push.md +3 -2
- package/knowledge/2.0/apps/worker2/features/service-request-sales-order-generation.md +1 -1
- package/knowledge/2.0/apps/worker2/features/talos-transcript-ingestion.md +3 -8
- package/knowledge/2.0/standards/framework-rules.md +15 -12
- package/knowledge/INDEX.md +3 -3
- package/knowledge/clients/nychh/INDEX.md +1 -0
- package/knowledge/clients/nychh/features/location-shipping-addresses.md +16 -2
- package/knowledge/clients/nychh/features/netsuite-inventory-adjustment-fulfillment-link.md +8 -5
- package/knowledge/clients/nychh/features/netsuite-transfer-order-import.md +27 -16
- package/knowledge/clients/nychh/features/transfer-order-inventory-quantities.md +14 -54
- package/knowledge/clients/nychh/features/transfer-order-netsuite-push.md +4 -6
- package/knowledge/clients/nychh/profile.md +7 -3
- package/knowledge/clients/nychh/workflows/po-availability-triage.md +56 -0
- package/knowledge/clients/true/profile.md +6 -2
- package/package.json +1 -1
|
@@ -6,13 +6,14 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-02
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
13
13
|
- library/app/api/netsuite/rest.php
|
|
14
14
|
- dbchanges2/_modules/netsuite/2026-09-24a - TransferOrderDownstreamPoBridgeAcl.sql
|
|
15
15
|
- worker/crons/toga2/netsuite/process_netsuite_order.php
|
|
16
|
+
- test/@jeff/netsuite/test_match_transfer_order_purchase_order_lines.php
|
|
16
17
|
related:
|
|
17
18
|
- ../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
|
|
18
19
|
- ../../worker/features/netsuite-togasupply-per-client-sync.md
|
|
@@ -39,14 +40,15 @@ Callers:
|
|
|
39
40
|
- `linkTransferOrderPurchaseOrders()` — kept as a thin public wrapper (old callers).
|
|
40
41
|
|
|
41
42
|
Expected sets, from NetSuite only:
|
|
42
|
-
- **Upstream:** `otherRefNum` split on `' + '` → `findCustomerPurchaseOrderByNumber` under `$cfg['importCustomerPurchaseOrdersOnVendor']`. Missing customer PO → **created** (header + lines).
|
|
43
|
+
- **Upstream:** `otherRefNum` split on `' + '` → `findCustomerPurchaseOrderByNumber` under `$cfg['importCustomerPurchaseOrdersOnVendor']`. Missing customer PO → **created** (header + lines). A new PO line is created only when the TO item is on NO line of that PO. A new PO line adds NO stock-in (stock-in = linked stocking SO's received adjustments; see [NYCHH quantities](../../../clients/nychh/features/transfer-order-inventory-quantities.md)).
|
|
43
44
|
- **Downstream:** `App_Api_Netsuite_Rest::listPurchaseOrderIdsCreatedFromOrder($orderId)` (SuiteQL on `transactionline.createdfrom`). A vendor PO not yet in Toga is skipped here; the PO sync imports it later and reconciles again.
|
|
44
45
|
|
|
45
|
-
Line matching (
|
|
46
|
-
1.
|
|
47
|
-
2.
|
|
48
|
-
3.
|
|
49
|
-
4.
|
|
46
|
+
Line matching — `matchTransferOrderItemsToPurchaseOrderItems()` (public static, options array; helpers `chooseReusablePurchaseOrderLine`, `chooseUnclaimedPurchaseOrderLine`, `getStockedPurchaseOrderItemUuids`):
|
|
47
|
+
1. Candidates = same item `uuid`, then `partNumber` (exact — no normalization; `210-BFZY-A` ≠ `210-BFZY`).
|
|
48
|
+
2. A TO line with NetSuite `createdPo` pairs only inside that PO.
|
|
49
|
+
3. **Upstream (customer PO, 325/329)** — rank: existing link (unless it is unstocked while a stocked line exists) > stocked line (has a `PurchaseOrderItems_SalesOrderItems` row) > not yet used by this TO > equal qty > lowest line. A PO line MAY be reused by several TO lines (soft claim). A moved link logs `RELINK` to error_log.
|
|
50
|
+
4. **Downstream (vendor PO, 326/330)** — unchanged: keep valid existing link, prefer equal qty, each PO line used once.
|
|
51
|
+
5. Empty/orphan PO lines are never auto-deleted.
|
|
50
52
|
|
|
51
53
|
Deletes are logged: grep `reconcileTransferOrderPurchaseOrderLinks` in the worker log.
|
|
52
54
|
|
|
@@ -57,7 +59,9 @@ Deletes are logged: grep `reconcileTransferOrderPurchaseOrderLinks` in the worke
|
|
|
57
59
|
- **ACL first.** 326/330 had no API-role ACL anywhere → 403 `EZ-1`. `_modules/netsuite/2026-09-24a - TransferOrderDownstreamPoBridgeAcl.sql` seeds the 4-layer chain + field grants 2214-2216 (326) and 2230-2232 (330), roles by NAME (`API` = id 3 in all 37 prod client DBs). Must run **before** the library deploy.
|
|
58
60
|
- **Repair model = re-process, not SQL.** Fix the code so a re-run heals, then reset cursors / replay ([per-client sync](../../worker/features/netsuite-togasupply-per-client-sync.md)) or run [process_netsuite_order.php](../../worker/workflows/reprocessing-one-netsuite-order.md) for one order. No data-repair SQL.
|
|
59
61
|
- **Pre-2026-09-24 upstream rows were polluted.** The old `linkPurchaseOrderToTransferOrder` path (PO `createdFrom` a TO) and old `retireStale` wrote vendor POs into 325/329. `Client_Nychh` before fix: ~533/1,593 rows in 325 and 3,981/5,893 in 329 were vendor POs; GroWrk/Elite a few. Example: NYCHH TO id 11 (NS SO 71759 / internal 1582638) had vendor POs 49338 + 49324 upstream and its real customer PO EIT0005971 was never imported. After re-process: EIT0005971 created (PO id 1005, 20 lines, customer 28908) upstream; 49324 lines 1-15 + 49338 lines 1-5 downstream; 0 vendor rows upstream.
|
|
62
|
+
- **Old upstream matcher picked wrong lines (fixed 2026-10-02, `1bc4de52`).** It took the first same-item line (often an unstocked line created from a TO) and its "each PO line once" rule made new empty PO lines when a TO had the same item twice. Fix = upstream ranking above; downstream kept strict. Heal by re-processing the TO. Tests: `test/@jeff/netsuite/test_match_transfer_order_purchase_order_lines.php`.
|
|
60
63
|
- Stale test: `test/@Bala/tests/netsuite_transferorder_payload_tests.php` "PO number from the column" case fails since the `TransferOrders.purchaseOrderId` drop ([transfer-orders page](../../../2.0/apps/toga25-supply/features/transfer-orders-page.md)).
|
|
61
64
|
|
|
62
65
|
## Change history
|
|
66
|
+
- 2026-10-02 — Upstream line matching: stocked-line ranking + reuse; new PO line only when item absent; RELINK log; downstream unchanged. CTO alternative (soft claim, upstream only) adopted. (jcardinal)
|
|
63
67
|
- 2026-09-24 — Created: two-direction reconcile replaces the upstream-only linker; fixed vendor POs written upstream; 326/330 now live + ACL'd. Prod-verified. (jcardinal)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Summary |
|
|
4
4
|
|-----|---------|
|
|
5
|
-
| [TOGa Desk Architecture](architecture.md) |
|
|
5
|
+
| [TOGa Desk Architecture](architecture.md) | 1.0 (App_) support desk + field-service platform — staff at `/desk/`, public/client portal at `/`; open for the request pipeline, domain-class layer, MSP data m |
|
|
6
6
|
| [Email-to-Ticket Intake (crons/tickets.php)](features/email-to-ticket-intake.md) | TOGa Desk email→ticket intake via IMAP poller (`crons/tickets.php`) + postfix pipe (`crons/pipe.php`), both into `Ticket::emailToTicket()`; open for routing/thr |
|
|
7
7
|
| [Field-Service Dispatch (central / repair orders)](features/field-service-dispatch.md) | TOGa Desk field-service dispatch (the **central** domain): repair-order lifecycle, technician scheduling, onsite/depot, parts, and UPS shipment tracking; open f |
|
|
8
8
|
| [Managed Service Order Create Flow (New MSO modal → tickets/add)](features/managed-service-order-create.md) | TOGa Desk Managed Service Order (MSO) create flow: New MSO modal → `tickets/add` builds a `ManagedServiceOrder` + assets + a `RepairOrder` per asset; open for t |
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
| [REST API (RPC-over-POST) & API-Key Auth](features/rest-api.md) | TOGa Desk programmatic API at `desk/api/` — an RPC-over-POST dispatcher (not verb/URL REST) with API-key auth; open for the request shape, status codes, auth fl |
|
|
12
12
|
| [SMB Contract Editing & the clientMspId Corruption Trap](features/smb-contract-editing.md) | TOGa Desk SMB contracts modal (`/desk/?route=toga/smbcontracts`) and the `clientMspId` corruption trap that made MSP portal users see "You have no assigned clie |
|
|
13
13
|
| [Staff Login Authentication (signIn) — AD vs. local password](features/staff-login-authentication.md) | TOGa Desk staff login via `signIn()` — AD branch (live) vs. |
|
|
14
|
+
| [Staff SSO Login (Sso class via saml.togahub.com)](features/staff-sso-login.md) | TOGa Desk staff "Sign in with SSO" via saml.togahub.com (TRUE-78064, `Sso` class, Tools pattern); open before touching SSO login, the `?saml=` return, the `[sam |
|
|
14
15
|
| [Ticket "Assign To" Lists (who can be assigned a ticket)](features/ticket-assign-to-lists.md) | How the ticket "Assign To" pickers are built (new/edit/merge modals, ticket page) and the hardcoded client exclusion list (`ASSIGN_TO_EXCLUDED_CLIENTS`, IM Hous |
|
|
15
16
|
| [Ticket Lifecycle (class.ticket.php)](features/ticket-lifecycle.md) | TOGa Desk ticket creation + reply handling via `Ticket` (class.ticket.php): the four creation/reply doors, `deriveCustomerId()`, and the `addReply()` status swi |
|
|
16
17
|
| [Reverting a TOGa Desk _production Deploy (revert-forward)](workflows/production-revert-forward.md) | How to cleanly roll back one ticket's changes from the togadesk `_production` branch via `git revert -m 1` of its two-parent merge, verified with `git patch-id` |
|
|
@@ -6,9 +6,11 @@ project: TOGa Desk
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-02
|
|
10
10
|
owners: ["mhammontree", "jcardinal"]
|
|
11
11
|
files:
|
|
12
|
+
- index.php
|
|
13
|
+
- includes/loader.php
|
|
12
14
|
- desk/index.php
|
|
13
15
|
- desk/includes/loader.php
|
|
14
16
|
- desk/config.php
|
|
@@ -33,17 +35,20 @@ related:
|
|
|
33
35
|
- 1.0/apps/togadesk/features/email-to-ticket-intake.md
|
|
34
36
|
- 1.0/apps/togadesk/features/notifications.md
|
|
35
37
|
- 1.0/apps/togadesk/workflows/standalone-test-scripts.md
|
|
38
|
+
- 1.0/apps/togadesk/features/staff-sso-login.md
|
|
36
39
|
---
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
1.0 (App_) support desk + field-service platform — staff at `/desk/`, public/client portal at `/`; open for the request pipeline, domain-class layer, MSP data model, crons/integrations, or system topology.
|
|
39
42
|
|
|
40
43
|
## Summary
|
|
41
44
|
TOGa Desk is the staff-facing support desk **and field-service platform** (analysts work at `/desk/`). Framework 1.0 (`App_`) app bootstrapped from the `library` core (`_.php`). Beyond ticketing it covers asset/license/inventory management, repair-order dispatch (depot + onsite technicians), a knowledge base, host/service monitoring, RBAC, a REST-ish API, and IMAP email intake. Primary database `db_togadesk` (schema `TOGaDeskSupport`); also `db_common` and per-client databases.
|
|
42
45
|
|
|
43
|
-
**Critical rules:**
|
|
46
|
+
**Critical rules:** there are **two live trees** — `desk/` (staff app) and the repo root (`index.php` → `includes/loader.php`, the public/client portal served at `/`; docroot = repo root). Do not assume root code is dead; but root `includes/classes/` holds stale variants (e.g. root `class.ticket.php`) — live domain classes are in `desk/includes/classes/`. Per-client host restriction (`getRestrictedClient()`) is an **entry/UI gate only** — record-level tenancy comes from the session `clientid`, not the host. TOGa Desk **shares the `TOGaDeskSupport` database with TOGa View** — a change to ticket data semantics affects both apps. `ENVIRONMENT` defaults to `worker` when unset → CLI scripts read the wrong config file.
|
|
44
47
|
|
|
45
48
|
## Entry points & request pipeline
|
|
46
|
-
- **Web:** `desk/index.php` → `desk/includes/loader.php`.
|
|
49
|
+
- **Web, staff:** `desk/index.php` → `desk/includes/loader.php`.
|
|
50
|
+
- **Web, public/client portal:** repo-root `index.php` → root `includes/loader.php`, served at `/` (docroot = repo root). Clients (incl. custom-URL hosts) and staff sign in here; staff are then redirected to `/desk/`. The root loader requires `desk/includes/functions.php`, so `signIn()` etc. are the desk versions. Root page templates live in `template/pages/` (e.g. the login card in `template/pages/dashboard.php`).
|
|
51
|
+
- **Staff SSO** (TRUE-78064, prod only): `?route=signin&sso=1` and the gateway's `?saml=` return route through `general.php` → `Sso`. See [staff SSO login](features/staff-sso-login.md).
|
|
47
52
|
- **Bootstrap order** (`loader.php`): local `functions.php` → `config.php` → register the two custom autoloaders `vendorClassAutoload` (`desk/vendor/classes/`) and `appClassAutoload` (`desk/includes/classes/`) → Composer autoload → `require _.php` + `App_Framework_TOGaDesk::initialize()` (starts session with `HttpOnly`/`Secure`, inits Sentry, XSS-protects `$_REQUEST`/`$_GET`/`$_POST`) → `App_Page::validateCrossSiteRequestForgery()` → Medoo `$database` instantiated from config → resolve logged-in user `$liu` via `people.sessionid` → unserialize the user's role `perms`.
|
|
48
53
|
- **`$route`** comes from `$_GET['route']` (defaults to `dashboard`) and is sanitized against path traversal.
|
|
49
54
|
- **Sequential controller pipeline**, each stage gated on a query param:
|
|
@@ -136,12 +141,15 @@ Invariants:
|
|
|
136
141
|
- `ENVIRONMENT` defaults to `worker` when unset → CLI scripts read the wrong config file.
|
|
137
142
|
- Heavy **hardcoded client-ID branching** in `central` dispatch and `tickets/add` (Walmart, WJE, Compass/ODP, AIG, RUMCSI) — verify the client arm before changing shared logic.
|
|
138
143
|
- **Two `class.ticket.php` files** — only `desk/includes/classes/` is live; the root variant is dead (random ticket numbers, stale schema).
|
|
144
|
+
- **Autoloader path differs local vs prod.** `appClassAutoload()` (`desk/includes/functions.php` ~L418) appends `/desk` only when `$scriptpath` does NOT end in `desk`. A local checkout at `.../togadesk` ends in `desk`, so the root tree loads **root** `includes/classes` locally but `desk/includes/classes` in prod (`/var/app/current`). Root pages needing a desk class should `require_once` it explicitly (see `template/pages/dashboard.php` for `Sso`). Not fixed.
|
|
145
|
+
- **⚠ `config.*.ini` may be web-readable.** Docroot is the repo root and there is no `FilesMatch` deny for `*.ini`; `config.prod.ini` is committed with plaintext DB/SMTP creds (and, with SSO, Core + Client_True creds). Use read-only DB users / EB env vars. Not fixed — see [staff SSO login](features/staff-sso-login.md).
|
|
139
146
|
- The API has **no pagination** — `get` returns full result sets; Medoo `filters` pass straight to the WHERE clause.
|
|
140
147
|
- The prod O365 OAuth secret used by `crons/tickets_prod.php` has a **hard expiry (~2026-12-20)**.
|
|
141
148
|
- **Cross-tenant login hole in the per-client host restriction (pre-existing, NOT fixed).** The password-login access condition in `desk/includes/functions.php` (~L627-631) is a chain of ORs; two clauses grant access regardless of which client's subdomain is in use, letting an IM Houston or Fordham user sign in on another client's branded host. Post-login filtering keys off the session `clientid` so it is not an immediate record leak, but it defeats the restriction and contradicts the comment directly above it. Deserves its own ticket.
|
|
142
149
|
- **Sessions are host-scoped, so a domain change logs everyone out.** `ebs/setup_phpini.php` sets `session.save_handler=rediscluster` but no `session.cookie_domain`, and there is no `session_set_cookie_params()` anywhere. Combined with `people.sessionid` being a **single column**, logging in on a second domain invalidates the session on the first.
|
|
143
150
|
|
|
144
151
|
## Related
|
|
152
|
+
- [Staff SSO login](features/staff-sso-login.md)
|
|
145
153
|
- [TOGa View architecture](../togaview/architecture.md)
|
|
146
154
|
- [Ticket lifecycle](features/ticket-lifecycle.md)
|
|
147
155
|
- [REST API](features/rest-api.md)
|
|
@@ -6,13 +6,14 @@ project: TOGa Desk
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-02
|
|
10
10
|
owners: ["mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- desk/includes/functions.php
|
|
13
13
|
- desk/includes/controllers/general.php
|
|
14
14
|
- desk/includes/controllers/actions/user/loginAsAnother.php
|
|
15
15
|
related:
|
|
16
|
+
- 1.0/apps/togadesk/features/staff-sso-login.md
|
|
16
17
|
- 1.0/apps/togadesk/features/per-client-host-restriction.md
|
|
17
18
|
- 1.0/apps/togaview/features/login-flows.md
|
|
18
19
|
- 1.0/apps/togadesk/architecture.md
|
|
@@ -24,6 +25,8 @@ TOGa Desk staff login via `signIn()` — AD branch (live) vs. the armed email-on
|
|
|
24
25
|
|
|
25
26
|
TOGa Desk staff login runs through `signIn()` in `desk/includes/functions.php` (~L458), reached from `desk/includes/controllers/general.php:8`. It has **two mutually exclusive branches** selected by the feature flag `App_Feature::ACTIVE_DIRECTORY_LOGIN`. The AD branch is the live production path; the non-AD `else` branch is an **armed landmine** whose password check is commented out (see Gotchas).
|
|
26
27
|
|
|
28
|
+
Staff **SSO** (TRUE-78064, prod only) is a separate path that does not go through `signIn()` — see [staff SSO login](staff-sso-login.md). The password form below stays for clients, staff fallback, and the dev bypass.
|
|
29
|
+
|
|
27
30
|
`signIn()` is also the **only** place `people.lastlogin` is written — which makes that column useless for measuring TOGa View portal usage.
|
|
28
31
|
|
|
29
32
|
## How it works
|
|
@@ -32,7 +35,8 @@ TOGa Desk staff login runs through `signIn()` in `desk/includes/functions.php` (
|
|
|
32
35
|
2. `signIn()` branches on `App_Feature::ACTIVE_DIRECTORY_LOGIN`. **Verified in production (2026-08-21):** `Vision.FeatureFlags` id 7, `framework = 'App_Framework_TOGaDesk'`, `isActive = 1`, created 2020-08-10 — so the AD branch has been the live path for five years.
|
|
33
36
|
3. **AD branch.** Attempts an Active Directory check against Agilant's directory (`db_common`). Emails outside Agilant's AD — every client user — fail that check and fall through to the **local credential check** at ~L611-620: `people.email LIKE … AND people.password LIKE sha1($password) AND people.adPeopleId IS NULL`.
|
|
34
37
|
4. **Client-restriction gate (~L631).** After a successful local check, access is gated by an OR-chain keyed on `getRestrictedClient()`. Client constants live in the same file (~L1488-1531), e.g. `CLIENT_RUMCSI = 146`, `CLIENT_DEPARTMENT_RUMCSI = 231`. The per-client branded host is resolved by `getRestrictedClient()` (~L1558) — see [per-client hostname restriction](per-client-host-restriction.md).
|
|
35
|
-
5. On success `signIn()` writes `people.lastlogin` (`desk/includes/functions.php:588, 635, 672, 689`). `desk/includes/controllers/actions/user/loginAsAnother.php:17-18`
|
|
38
|
+
5. On success `signIn()` writes `people.lastlogin` (`desk/includes/functions.php:588, 635, 672, 689`). `desk/includes/controllers/actions/user/loginAsAnother.php:17-18` and `Sso::consume()` are the only other writers.
|
|
39
|
+
6. **Dev bypass.** `devLoginBypassAllowed()` (~L468) skips the AD password check (~L550, ~L685) when in dev **or** test mode.
|
|
36
40
|
|
|
37
41
|
### Data model
|
|
38
42
|
`TOGaDeskSupport.people` — `email` (UNIQUE), `password` (bare SHA-1), `adPeopleId`, `ldap_user`, `clientid`, `roleid`, `type`, `approved`, `isActive`, `lastlogin`. `Vision.FeatureFlags` — the `ACTIVE_DIRECTORY_LOGIN` row (id 7).
|
|
@@ -47,12 +51,22 @@ It is **not reachable in production** — `ACTIVE_DIRECTORY_LOGIN` has been on s
|
|
|
47
51
|
### `people.lastlogin` is written by togadesk ONLY — never by togaview
|
|
48
52
|
TOGa View's `?hash=` handoff block and `mvc/login/post.php` populate the session and never touch `lastlogin`. So:
|
|
49
53
|
- A NULL / stale `lastlogin` does **not** mean the account is unused — a portal user may log in daily and never move the column.
|
|
50
|
-
- `lastlogin` measures **TOGa Desk staff** activity, full stop. Any query that uses it to size portal adoption is wrong. (This invalidated a mid-session conclusion during TRUE-81051; an easy analysis trap to fall into twice.)
|
|
54
|
+
- `lastlogin` measures **TOGa Desk staff** activity (password or SSO), full stop. Any query that uses it to size portal adoption is wrong. (This invalidated a mid-session conclusion during TRUE-81051; an easy analysis trap to fall into twice.)
|
|
51
55
|
|
|
52
56
|
### Client-user logins are local-password logins even with AD on
|
|
53
57
|
The AD branch's name suggests client users authenticate against a directory. They do not — they land on the local SHA-1 check. The AD path serves Agilant staff (`clientId = 2`) only.
|
|
54
58
|
|
|
59
|
+
### Dev login bypass is on in every `test_mode=1` environment
|
|
60
|
+
`devLoginBypassAllowed()` = dev OR test mode, so alpha/beta/stage/demo/hotfix all accept a staff login without the AD password. Pre-existing. Confirm those hosts are not public.
|
|
61
|
+
|
|
62
|
+
### Password login still bypasses Entra MFA
|
|
63
|
+
With SSO live, staff can still sign in with AD/local password on the main host and skip MFA. Possible later step: reject password login for AD-linked staff in prod. Not done.
|
|
64
|
+
|
|
55
65
|
## Related
|
|
66
|
+
- [Staff SSO login](staff-sso-login.md)
|
|
56
67
|
- [Per-client hostname restriction](per-client-host-restriction.md)
|
|
57
68
|
- [TOGa View login flows](../../togaview/features/login-flows.md)
|
|
58
69
|
- [TOGa Desk architecture](../architecture.md)
|
|
70
|
+
|
|
71
|
+
## Change history
|
|
72
|
+
- 2026-10-02 — Linked the new staff SSO path (TRUE-78064); recorded dev-bypass-in-test-mode and MFA-bypass gotchas (mhammontree)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Staff SSO Login (Sso class via saml.togahub.com)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: togadesk
|
|
5
|
+
project: TOGa Desk
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: draft
|
|
9
|
+
updated: 2026-10-02
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- desk/includes/classes/class.sso.php
|
|
13
|
+
- desk/includes/loader.php
|
|
14
|
+
- desk/includes/controllers/general.php
|
|
15
|
+
- desk/template/signin.php
|
|
16
|
+
- template/pages/dashboard.php
|
|
17
|
+
- config.prod.ini
|
|
18
|
+
- dbchanges2/Core/2026-10-02a - TogaDeskSsoDomain.sql
|
|
19
|
+
related:
|
|
20
|
+
- ./staff-login-authentication.md
|
|
21
|
+
- ../architecture.md
|
|
22
|
+
- ../../tools/features/saml-sso-auth.md
|
|
23
|
+
- ../../library/features/app-sso-initiation.md
|
|
24
|
+
- ../../../2.0/apps/saml/features/downstream-integration-contract.md
|
|
25
|
+
- ../../../2.0/apps/saml/features/acs-assertion-trust-gaps.md
|
|
26
|
+
- ../../../clients/true/profile.md
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
TOGa Desk staff "Sign in with SSO" via saml.togahub.com (TRUE-78064, `Sso` class, Tools pattern); open before touching SSO login, the `?saml=` return, the `[saml]` config, or the TogaDesk Core.Domains row.
|
|
30
|
+
|
|
31
|
+
## Summary
|
|
32
|
+
TRUE-78064 replaces Active Directory for **TOGA staff** with SSO through `saml.togahub.com`, copying the Tools `App_Sso` pattern. Status 2026-10-02: written + locally tested, **not committed or deployed**; the return leg can only be fully tested in prod (needs the prod Core.Domains row + prod key).
|
|
33
|
+
|
|
34
|
+
Decisions (Mark + Brandon/call center, 2026-10-02):
|
|
35
|
+
- Staff only. Matched to an existing `TOGaDeskSupport.people` row by email. SSO **never creates** people rows — no row = cannot log in.
|
|
36
|
+
- **Prod only.** Only `config.prod.ini` has `[saml]`; beta/stage/dev have no SSO.
|
|
37
|
+
- No SSO-only page and no auto-redirect: a **"Sign in with SSO" button** on the normal login. TogaDesk cannot detect a Microsoft session itself; staff already signed in to Microsoft pass straight through after one click (same as Tools).
|
|
38
|
+
- Clients have **no** SSO — only custom-URL (host-based) portals. Their login does not change. Password form stays for clients, staff fallback, and the dev bypass.
|
|
39
|
+
- Prod host: `togadesk.togatech.com`.
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
1. **Start:** button → `/desk/?route=signin&sso=1` → `general.php` → `Sso::initiate()`: stores `{nonce,time}` in `$_SESSION`, calls `App_Sso::initiate()` with `[saml]` config + the current `API_SECRET_ACCESS_TOKEN` read from 2.0 `Core.Parameters` over `db_toga2core`; nonce goes in `urlParameters`.
|
|
43
|
+
2. **Return:** gateway redirects to `<Core.Domains.domain>?saml=<payload>&urlParameters=<nonce>`. `loader.php` sets `$route = "sso"` when `?saml=` is present, `route` is empty, and `Sso::isAvailable()`; route `sso` skips `isSignedIn()` and the `$liu` lookup.
|
|
44
|
+
3. **Consume** (`Sso::consume()`):
|
|
45
|
+
- nonce check: `hash_equals`, single use, 300 s max age (= gateway TTL); payload length cap 8192; `is_string` guards.
|
|
46
|
+
- base64 decode (a `+` arrives as a space → `strtr` back).
|
|
47
|
+
- decrypt client + user with current → previous key. Check IV length first: `App_String::decryptWithKey` warns on short input, and a 1.0 warning ends the request.
|
|
48
|
+
- client uuid must equal `[saml] true_client_uuid`; load `Client_True.Users` email by uuid, `isActive=1` (`db_true`).
|
|
49
|
+
- email must match **exactly one** `db_common` AD People row (`EmailAddresses`) — the same staff test the AD login uses.
|
|
50
|
+
- find `TOGaDeskSupport.people` by any AD alias OR `adPeopleId`, `isActive=1`, `clientid` NULL or not Office Depot; prefer `@togatech.com`, then lowest id. Set `adPeopleId` only if no other row holds it.
|
|
51
|
+
- `session_regenerate_id` (fail if it cannot), write `people.sessionid` / `lastlogin` by id, set `userLabel`, `logSystem`, `Referrer-Policy: no-referrer`, redirect `?route=dashboard`.
|
|
52
|
+
- Any failure: `logSystem` the reason (never the payload), `setStatus(1200)`, redirect `?route=signin`, `exit`.
|
|
53
|
+
4. **Safe to send headers:** unlike Tools, `App_FrameworkIndex_TOGaDesk::render()` emits nothing, so `general.php` runs before any output. No `headers_sent()` workaround needed.
|
|
54
|
+
5. **`Sso::isAvailable()`** = every `[saml]` key set (`idp_single_sign_on_service_url`, `idp_entity_identifier`, `assertion_consumer_service_url`, `domain_uuid`, `true_client_uuid`, `return_url`), `[database_true]` + `[database_toga2core]` hostnames set, and `HTTP_HOST` == host of `[saml] return_url`. It does **not** call `getRestrictedClient()` (the root portal also calls it). So client custom-URL hosts never show the button.
|
|
55
|
+
6. **Buttons** on both the root portal login card (`template/pages/dashboard.php` — the page staff land on at `/`) and `/desk/?route=signin` (where failures return).
|
|
56
|
+
|
|
57
|
+
### Core.Domains row (`dbchanges2/Core/2026-10-02a - TogaDeskSsoDomain.sql`)
|
|
58
|
+
`uuid d1e526b9-e2fb-4462-935f-ed449e8f5986` (= `[saml] domain_uuid`), `clientId 1` (True), `appId 18` (togadesk-1, from a historic seed — **confirm in prod before running**), `environmentId 1`, `domain https://togadesk.togatech.com/desk/`. Unqualified `Domains`, guarded `WHERE NOT EXISTS` on uuid OR (clientId, appId, environmentId). Domain must have **no query string** — see the [gateway contract](../../../2.0/apps/saml/features/downstream-integration-contract.md).
|
|
59
|
+
|
|
60
|
+
### Kill switch
|
|
61
|
+
Set `[saml] domain_uuid = ""` in `config.prod.ini` and deploy → button hidden, SSO off, password login unaffected.
|
|
62
|
+
|
|
63
|
+
## Gotchas
|
|
64
|
+
- **Root-tree class loading differs local vs prod.** The root portal page needs `Sso`, but the autoloader quirk (see [architecture](../architecture.md)) loads root `includes/classes` locally. `template/pages/dashboard.php` does `if (!class_exists('Sso')) require_once __DIR__ . '/../../desk/includes/classes/class.sso.php';` — keep it.
|
|
65
|
+
- **⚠ SSO inherits the gateway's forged-login hole.** `/acs` does not verify the IdP signature, so anyone can forge a login for any staff email. Same exposure as Tools. See [acs trust gaps](../../../2.0/apps/saml/features/acs-assertion-trust-gaps.md). Not fixed; Mark to raise with Jeff/Rohan.
|
|
66
|
+
- **Handoff replay.** The payload has no issue time or single-use id. The nonce blocks login CSRF, but a leaked `?saml=` URL can be replayed by someone who starts their own SSO inside the window. App fix: a used-payload table; gateway fix: nonce + iat + MAC inside the payload. Not fixed.
|
|
67
|
+
- **⚠ `config.prod.ini` may be web-readable.** Docroot is the repo root and there is no `FilesMatch` deny for `*.ini`. The committed file holds plaintext DB/SMTP creds and now also `[database_true]` and `[database_toga2core]` — the latter can read the master SSO key for every app. Use read-only DB users / EB env vars. Not fixed.
|
|
68
|
+
- **SSO does not force MFA.** Staff can still use AD/local password on the main host and skip Entra MFA. Possible later step: reject password login for AD-linked staff in prod.
|
|
69
|
+
- **Local testing ends at the gateway.** Gateway "Invalid SAMLResponse (Code = 2)" locally = local `Core_2.Parameters` key differs from prod. Expected; not a code bug.
|
|
70
|
+
|
|
71
|
+
## Change history
|
|
72
|
+
- 2026-10-02 — Created: staff SSO button + `Sso` class, prod-only, staff matched by email, no auto-create (TRUE-78064, not yet deployed) (mhammontree)
|
|
@@ -6,8 +6,8 @@ project: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [jcardinal, ajean]
|
|
9
|
+
updated: 2026-10-02
|
|
10
|
+
owners: [jcardinal, ajean, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- tools/_/app/auth.php
|
|
13
13
|
- tools/mvc/sso/initiate/get.php
|
|
@@ -24,6 +24,7 @@ related:
|
|
|
24
24
|
- ../../library/features/app-sso-initiation.md
|
|
25
25
|
- ../../../2.0/apps/saml/features/downstream-integration-contract.md
|
|
26
26
|
- ../../../clients/true/features/users-personas-data-model.md
|
|
27
|
+
- ../../togadesk/features/staff-sso-login.md
|
|
27
28
|
---
|
|
28
29
|
|
|
29
30
|
Tools `App_Auth`: consumes the 2.0 SAML gateway `?saml=` handoff, establishes its own session, and gates pages by Client_True persona; open for the SSO legs, the `Core.Parameters` runtime key sourcing, or the headers-already-sent/persona-snapshot gotchas.
|
|
@@ -67,7 +68,7 @@ The shared secret is **rotated regularly**, so any static `config.*.ini [saml]`
|
|
|
67
68
|
`mvc/login/post.php` permits **email-only** login **only** when config `[internal] dev_mode` is truthy **and** `App_Registry::inDevMode()` — both must hold. When used it writes a `SECURITY` line to `error_log`.
|
|
68
69
|
|
|
69
70
|
### One-step sign-in
|
|
70
|
-
Home route `/` (`mvc/get.php`), when not logged in, does `App_MVC::routeTo('/login'); return
|
|
71
|
+
Home route `/` (`mvc/get.php`), when not logged in, does `App_MVC::routeTo('/login'); return;`. `/login` (`mvc/login/get.php`) renders only a **"Sign in with SSO" button** (+ the dev bypass form in dev). Tools does **not** auto-detect an existing Microsoft session — the user clicks once; if already signed in to Microsoft they pass straight through. Verified on `origin/_production` 2026-10-02. TOGa Desk copies this model — see [TOGa Desk staff SSO](../../togadesk/features/staff-sso-login.md).
|
|
71
72
|
|
|
72
73
|
### Crypto format (`App_String::encryptWithKey` / `decryptWithKey`)
|
|
73
74
|
`library/app/string.php:1075-1094`, AES-256-CBC: `encryptWithKey` returns `base64( iv . openssl_encrypt(..., flag 0) )` — i.e. a base64 envelope whose body is the raw IV followed by openssl's own base64 ciphertext; `decryptWithKey` reverses it. **Interoperable byte-for-byte with the 2.0 `_String::decryptWithKey()`** that the SAML gateway uses to produce the handoff token. The 2026-06-30 login failure decoded to the exact expected iv+base64 shape, proving the issue was a stale **key**, not a cipher/format mismatch.
|
|
@@ -95,3 +96,7 @@ The platform's rotating key/value parameter store, on the **2.0 PROD Core DB** (
|
|
|
95
96
|
- [App_Sso initiation (library)](../../library/features/app-sso-initiation.md)
|
|
96
97
|
- [SAML downstream integration contract (2.0)](../../../2.0/apps/saml/features/downstream-integration-contract.md)
|
|
97
98
|
- [True users/personas data model](../../../clients/true/features/users-personas-data-model.md)
|
|
99
|
+
- [TOGa Desk staff SSO (same pattern)](../../togadesk/features/staff-sso-login.md)
|
|
100
|
+
|
|
101
|
+
## Change history
|
|
102
|
+
- 2026-10-02 — Clarified sign-in is one click on an SSO button; no auto-detect of a Microsoft session (mhammontree)
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
| [Walmart Daily Sales Report (send_toga_walmart_report.php)](features/walmart-daily-sales-report.md) | The two Walmart sales workbooks (summary + details) built from `TOGA_Walmart` (in-store) and `TOGA_WalmartEComm` (.COM); open for the pseudo-store 1472 channel |
|
|
22
22
|
| [Diagnosing frozen 1.0 worker cron check-ins (Sentry "missed" flood)](workflows/diagnosing-frozen-cron-checkins.md) | Ordered path to diagnose frozen 1.0 worker cron timestamps + a Sentry `worker1` "missed" flood; open when check-ins stall and you suspect a wedged lock. |
|
|
23
23
|
| [isFulfillable Multi-Client Backfill (all togasupply clients)](workflows/isfulfillable-multi-client-backfill.md) | One-time backfill catching up `Items.isFulfillable` on existing items across all 17 togasupply clients; open when running/re-running it or deciding whether rema |
|
|
24
|
+
| [Mapping NetSuite customer address-book entries to Toga Locations (process_netsuite_addressbook_mapping.php)](workflows/netsuite-addressbook-location-mapping.md) | Operator CLI that maps a client customer's NetSuite address-book entry ids to Toga Locations (`Locations_Addresses.c_netsuiteInternalAddressbookId`), so TO impo |
|
|
24
25
|
| [Deduplicating NetSuite duplicate Items across client DBs (recursive MERGE)](workflows/netsuite-duplicate-items-dedup.md) | Per-client cleanup collapsing duplicate `Items` rows (same `partNumber`) to one canonical via recursive MERGE; open before running it — and read the catalog-bli |
|
|
25
26
|
| [Onboarding a Client to the NetSuite TOGa Supply Sync](workflows/onboarding-client-to-netsuite-togasupply-sync.md) | How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer; open when onboarding a client, and read the vendor-seed / ACL / Parameters-see |
|
|
26
27
|
| [Re-processing NetSuite orders, fulfillments, receipts and inventory adjustments (process_netsuite_*.php)](workflows/reprocessing-one-netsuite-order.md) | Run the real sync for given NetSuite ids on ONE client, without touching cursors: one order (+ its vendor POs), or a list of fulfillments/receipts/inventory adj |
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Mapping NetSuite customer address-book entries to Toga Locations (process_netsuite_addressbook_mapping.php)"
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: worker
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-02
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- worker/crons/toga2/netsuite/process_netsuite_addressbook_mapping.php
|
|
13
|
+
- library/app/api/netsuite/rest.php
|
|
14
|
+
- _underscore/Trait/Netsuite/Locations/Address.php
|
|
15
|
+
- dbchanges2/_modules/netsuite/2026-10-01a - LocationsAddressesNetsuiteAddressbookId.sql
|
|
16
|
+
- dbchanges2/_modules/netsuite/2026-10-01b - LocationsAddressesAddressbookIdCustomField.sql
|
|
17
|
+
related:
|
|
18
|
+
- ../../../../clients/nychh/features/netsuite-transfer-order-import.md
|
|
19
|
+
- ../../../../clients/nychh/features/location-shipping-addresses.md
|
|
20
|
+
- ./reprocessing-one-netsuite-order.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
Operator CLI that maps a client customer's NetSuite address-book entry ids to Toga Locations (`Locations_Addresses.c_netsuiteInternalAddressbookId`), so TO import resolves the destination by entry id; open to propose/apply or extend the mapping.
|
|
24
|
+
|
|
25
|
+
## Summary
|
|
26
|
+
- Manual CLI, never scheduled. The sync only READS the mapping; it never writes it (no self-learning — CTO AGREE).
|
|
27
|
+
- Two passes: `--propose` writes a CSV for review; `--apply` writes the reviewed CSV.
|
|
28
|
+
- Column `Locations_Addresses.c_netsuiteInternalAddressbookId` (INT UNSIGNED, unique) is all-NetSuite-client (`_modules/netsuite/`); declared via `_Trait_Netsuite_Locations_Address` on every NetSuite client's `Model/<Client>/Locations/Address.php` (22 clients; Compass USA = `Model/Compass/Usa/Locations/Address.php`). Undeclared → /v2 EV-8.
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
From `worker/`:
|
|
32
|
+
```
|
|
33
|
+
php crons/toga2/netsuite/process_netsuite_addressbook_mapping.php <client> --propose=<csv> [--id=<entry id>]
|
|
34
|
+
php crons/toga2/netsuite/process_netsuite_addressbook_mapping.php <client> --apply=<csv> [--id=<entry id>]
|
|
35
|
+
```
|
|
36
|
+
Entries come from `App_Api_Netsuite_Rest::listCustomerAddressbookEntries()`. Each entry gets a reason (NetSuite decides; Toga only as fallback):
|
|
37
|
+
1. `BILLING` — skipped: entry is any site's default billing.
|
|
38
|
+
2. `AGILANT` — skipped (Agilant's own address).
|
|
39
|
+
3. `ADDRESSEE` — addressee names exactly one site.
|
|
40
|
+
4. `NETSUITE` — exactly one site's own NetSuite address book has the same street + zip.
|
|
41
|
+
5. `NETSUITE_DEFAULT_SHIPPING` — tie broken by which site has it as default shipping.
|
|
42
|
+
6. `TOGA` — fallback by Toga location address (for stale customer ids).
|
|
43
|
+
Anything else stays unmapped → import falls back to text match.
|
|
44
|
+
|
|
45
|
+
## Gotchas
|
|
46
|
+
- NYCHH 2026-10-02: 61 entries → 18 sites. Unmapped by design: shared buildings and the shared 462 1st Ave dock — list on [NYCHH location addresses](../../../../clients/nychh/features/location-shipping-addresses.md).
|
|
47
|
+
- Stale customer ids (NYCHH Bellevue 29275, Jacobi 31917 do not exist in NetSuite) only map via `TOGA`.
|
|
48
|
+
- After apply, re-process affected TOs (`process_netsuite_order.php <client> --tranid=a,b`) to move their destination.
|
|
49
|
+
|
|
50
|
+
## Change history
|
|
51
|
+
- 2026-10-02 — Created; applied for NYCHH. (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-02
|
|
10
10
|
owners: ["jcardinal", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/process_netsuite_order.php
|
|
@@ -37,7 +37,7 @@ From the `worker/` directory:
|
|
|
37
37
|
php crons/toga2/netsuite/process_netsuite_order.php <client> --tranid=<NS order #>
|
|
38
38
|
php crons/toga2/netsuite/process_netsuite_order.php <client> --id=<NS internal id>
|
|
39
39
|
```
|
|
40
|
-
`<client>` = the wrapper suffix of `sync_togasupply_<client>.php` (e.g. `hh`, `growrk`, `elite`). Pass exactly one of `--tranid` / `--id`.
|
|
40
|
+
`<client>` = the wrapper suffix of `sync_togasupply_<client>.php` (e.g. `hh`, `growrk`, `elite`). Pass exactly one of `--tranid` / `--id`. Both accept a comma list (`--id=a,b`, `--tranid=a,b`): runs in order, stops on the first error.
|
|
41
41
|
|
|
42
42
|
Steps the script runs:
|
|
43
43
|
1. Refuses if `sync_togasupply_<client>.php` is running (`ps -ef`) — two writers can duplicate links.
|
|
@@ -73,6 +73,7 @@ php crons/toga2/netsuite/process_netsuite_inventory_adjustment.php <client> --id
|
|
|
73
73
|
- Needs `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS` true in the wrapper for TO routing; otherwise the order syncs as a sales order.
|
|
74
74
|
|
|
75
75
|
## Change history
|
|
76
|
+
- 2026-10-02 — `process_netsuite_order.php` accepts comma lists for `--id` / `--tranid` (`a72f17f5`). (jcardinal)
|
|
76
77
|
- 2026-09-30 — Added `process_netsuite_item_fulfillment.php` + `process_netsuite_inventory_adjustment.php`. (jcardinal)
|
|
77
78
|
- 2026-09-30 — Added `process_netsuite_records.php` (fulfillments/receipts by id); Closed routing follows the closed-order rule (v3: line-level). (rgirish)
|
|
78
79
|
- 2026-09-24 — Created; prod-verified on NYCHH TO 11. (jcardinal)
|
|
@@ -6,8 +6,8 @@ project: SAML SSO Gateway
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["rgirish"]
|
|
9
|
+
updated: 2026-10-02
|
|
10
|
+
owners: ["rgirish", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- saml/Controller/Index.php
|
|
13
13
|
- _underscore/String.php
|
|
@@ -49,6 +49,11 @@ Git history: the block arrived in commit `4bcee38` "Final changes for Compass SA
|
|
|
49
49
|
**email only**, with no issuer check. So any IdP the gateway already trusts (Compass, Prudential,
|
|
50
50
|
Office Depot) can assert a `@togatech.com` email and be handed the internal user.
|
|
51
51
|
|
|
52
|
+
**Affected consumers:** every app that trusts a True login from the gateway — Tools and TOGa Desk
|
|
53
|
+
staff SSO ([Tools](../../../1.0/apps/tools/features/saml-sso-auth.md),
|
|
54
|
+
[TOGa Desk](../../../1.0/apps/togadesk/features/staff-sso-login.md)). A forged assertion for any
|
|
55
|
+
staff email signs that person in to both.
|
|
56
|
+
|
|
52
57
|
The per-client IdP verification certs already exist in source as base64 blobs (Compass, Office
|
|
53
58
|
Depot, Prudential, RUMCSI, Agilant) — they are what a re-enabled verifier would load. Verification
|
|
54
59
|
is per-client; each IdP's signing cert must match its own client.
|
|
@@ -63,7 +68,14 @@ stop replay on its own. There is also **no nonce and no single-use marking** —
|
|
|
63
68
|
presented any number of times.
|
|
64
69
|
|
|
65
70
|
**Fix direction:** AES-256-GCM, or an HMAC verified with `hash_equals()` **before** decrypting,
|
|
66
|
-
plus a server-side single-use nonce.
|
|
71
|
+
plus a server-side single-use nonce.
|
|
72
|
+
|
|
73
|
+
### Gap 3 — the `?saml=` handoff is replayable
|
|
74
|
+
|
|
75
|
+
The handoff payload carries no issue time and no single-use id. TOGa Desk's session nonce blocks
|
|
76
|
+
login CSRF, but a leaked `?saml=` URL can be replayed by someone who starts their own SSO inside
|
|
77
|
+
the window. App-side fix: a used-payload table. Gateway-side fix: nonce + iat + MAC inside the
|
|
78
|
+
payload. Changing `encryptWithKey()` affects every caller — it is also
|
|
67
79
|
the `?saml=` handoff and the api2 encrypted-user-uuid auth, so this is not a local change.
|
|
68
80
|
|
|
69
81
|
## Gotchas
|
|
@@ -77,4 +89,5 @@ the `?saml=` handoff and the api2 encrypted-user-uuid auth, so this is not a loc
|
|
|
77
89
|
citing this doc as sign-off.
|
|
78
90
|
|
|
79
91
|
## Change history
|
|
92
|
+
- 2026-10-02 — Added Tools + TOGa Desk as affected consumers; recorded handoff replay (Gap 3) (mhammontree)
|
|
80
93
|
- 2026-09-25 — Created; recorded that `/acs` verifies no IdP signature and no Audience/Destination/InResponseTo/NotBefore/NotOnOrAfter, and that RelayState uses unauthenticated AES-256-CBC with an attacker-supplied IV (rgirish)
|
|
@@ -6,8 +6,8 @@ project: SAML SSO Gateway
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [jcardinal]
|
|
9
|
+
updated: 2026-10-02
|
|
10
|
+
owners: [jcardinal, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- saml/Controller/Index.php
|
|
13
13
|
- _underscore/Model/Core/ClientAuthentication.php
|
|
@@ -16,6 +16,9 @@ related:
|
|
|
16
16
|
- ../architecture.md
|
|
17
17
|
- ../../_underscore/architecture.md
|
|
18
18
|
- ../../../clients/rate/features/saml-sso.md
|
|
19
|
+
- ./acs-assertion-trust-gaps.md
|
|
20
|
+
- ../../../1.0/apps/tools/features/saml-sso-auth.md
|
|
21
|
+
- ../../../1.0/apps/togadesk/features/staff-sso-login.md
|
|
19
22
|
---
|
|
20
23
|
|
|
21
24
|
Integration recipe for a downstream TOGa app to authenticate users via the SAML gateway (`saml.togahub.com`); open when wiring a new SSO-consuming app.
|
|
@@ -45,7 +48,14 @@ where the JSON is:
|
|
|
45
48
|
"payload": { "client": "encryptWithKey(client.uuid)", "user": "encryptWithKey(user.uuid)" }
|
|
46
49
|
}
|
|
47
50
|
```
|
|
48
|
-
all encrypted under the Core `API_SECRET_ACCESS_TOKEN`.
|
|
51
|
+
all encrypted under the Core `API_SECRET_ACCESS_TOKEN`. `urlParameters` from RelayState is forwarded back.
|
|
52
|
+
|
|
53
|
+
Exact build (`saml/Controller/Index.php` ~L459): `$domain->domain . '?saml=' . $authPayload . '&urlParameters=' . …`. So:
|
|
54
|
+
- The `Core.Domains.domain` value **must have no query string** — the gateway appends `?saml=` blindly.
|
|
55
|
+
- The payload is base64 but **not urlencoded** — a `+` arrives as a space; the consumer must `strtr(' ', '+')` before decoding.
|
|
56
|
+
- `Core.Domains` has UNIQUE(clientId, appId, environmentId) and a unique `domain`, so an app gets **one return URL per client + app + environment**.
|
|
57
|
+
|
|
58
|
+
Known 1.0 consumers (all True staff): [Tools](../../../1.0/apps/tools/features/saml-sso-auth.md), [TOGa Desk](../../../1.0/apps/togadesk/features/staff-sso-login.md).
|
|
49
59
|
|
|
50
60
|
The downstream app then base64-decodes the `saml` param, JSON-parses it, decrypts the client and user UUIDs with `_String::decryptWithKey()`, loads the user, and **establishes its own session**. The gateway holds no session — session ownership is entirely the downstream app's.
|
|
51
61
|
|
|
@@ -59,9 +69,14 @@ The downstream app then base64-decodes the `saml` param, JSON-parses it, decrypt
|
|
|
59
69
|
|
|
60
70
|
- **Lazy-transaction write-drop.** `_Database::register()` auto-starts a lazy transaction (since Apr 2026). Any `/acs` write path (e.g. user auto-creation) must call `_Database::transactionCommit()` before exit or the writes are silently rolled back.
|
|
61
71
|
- **Gateway holds no session** — every downstream app must build and own its own session from the decrypted handoff.
|
|
72
|
+
- **"Invalid SAMLResponse (Code = 2)"** from the gateway = RelayState could not be decrypted with Core `API_SECRET_ACCESS_TOKEN` / `_PREVIOUS`. Seen when testing from local: local `Core_2.Parameters` key ≠ prod key. A full return-leg test needs the prod `Core.Domains` row + a deploy.
|
|
73
|
+
- **The handoff is not replay-safe** — no issue time or single-use id. See [acs trust gaps](./acs-assertion-trust-gaps.md).
|
|
62
74
|
|
|
63
75
|
## Related
|
|
64
76
|
|
|
65
77
|
- [saml Architecture](../architecture.md)
|
|
66
78
|
- [_underscore Architecture](../../_underscore/architecture.md)
|
|
67
79
|
- [Rate SAML SSO](../../../clients/rate/features/saml-sso.md)
|
|
80
|
+
|
|
81
|
+
## Change history
|
|
82
|
+
- 2026-10-02 — Documented exact return-URL build (no query string in domain, unencoded base64), one domain per client+app+env, Code 2 meaning, TOGa Desk as consumer (mhammontree)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-02
|
|
10
10
|
owners: ["bala", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Netsuite/SalesOrder.php
|
|
@@ -78,7 +78,7 @@ Nothing beyond this list. A transfer order has no shipping cost, ship method, te
|
|
|
78
78
|
| `tranDate` | `dateOrder`, date only; **omitted** when null |
|
|
79
79
|
| `otherRefNum` | the PO number (developer's explicit requirement) |
|
|
80
80
|
| `memo` | `TransferOrders.description` — the NYCHH custom form renders it as **"ORDER DESCRIPTION"** |
|
|
81
|
-
| `shippingAddress` | the **destination** Location's `primaryLocationAddress.address` (added 2026-09-09, live 2026-09-14). Key **omitted** when the destination has no address — and **omitting it is a bug, not a safe default**: NetSuite then ships to the customer's default site. |
|
|
81
|
+
| `shippingAddress` | the TO's own `shipToAddress` (`TransferOrders.shipToAddressId`, 2026-10-02) when set; else the **destination** Location's `primaryLocationAddress.address` (added 2026-09-09, live 2026-09-14). Key **omitted** when the destination has no address — and **omitting it is a bug, not a safe default**: NetSuite then ships to the customer's default site. |
|
|
82
82
|
| `shippingAddress.attention` | the TOGA delivery contact (`transferOrder.contact`): `firstName + ' ' + lastName` (empty parts dropped), fallback `contact._name`. Set only when both an address and a name exist; no contact → attention untouched. Name only, no phone (developer decision). `resolveTransferOrderContactName()`; the address is **cloned** first so the Location's address object is not mutated. Live in prod 2026-09-25. |
|
|
83
83
|
| `externalId` | `toga-to-<transfer order uuid>` |
|
|
84
84
|
| line `item` / `quantity` | each transfer-order item |
|
|
@@ -260,6 +260,7 @@ When the payload omits the ship-to address the create **succeeds**, so it looks
|
|
|
260
260
|
- **`git diff --stat` after any programmatic full-file write to `_underscore`.** A full-file write with an editor default flipped CRLF → LF on `_underscore/Model/Nychh/TransferOrder.php` — content identical, git reported it modified with no content hunks. Restored with `git checkout --`. Standing rule: [2.0 backend PHP standards](../../standards/backend-php.md).
|
|
261
261
|
|
|
262
262
|
## Change history
|
|
263
|
+
- 2026-10-02 — Push sends the TO's own `shipToAddress` when set (new `TransferOrders.shipToAddressId`, RecordField 2682); destination Location address is the fallback. (jcardinal)
|
|
263
264
|
- 2026-09-24 — `resolvePurchaseOrderNumber()` bridge-only (column 2143 dropped); multiple POs joined with `' + '`. (jcardinal)
|
|
264
265
|
- 2026-09-25 — delivery contact name sent as `shippingAddress.attention`. (jcardinal)
|
|
265
266
|
|
|
@@ -72,7 +72,7 @@ An item with no active vendor is absent from the result and produces no PO line.
|
|
|
72
72
|
|
|
73
73
|
`c_netsuiteInternalVendorId` is present in **22 of 35 tenant databases — 13 do not have it**. Referencing it blind fails with MySQL *"Unknown column"* for those 13. So the worker runs a runtime `information_schema.COLUMNS` probe (`TABLE_SCHEMA` = client DB, `TABLE_NAME = 'Vendors'`) and only splices the ranking clause into `ORDER BY` when the column exists.
|
|
74
74
|
|
|
75
|
-
Generalizes: `c_` columns are
|
|
75
|
+
Generalizes: `c_` columns are **per-tenant schema**, so any shared multi-tenant query touching one must probe for it or be scoped to tenants known to have it. See the `c_`-column convention in `2.0/standards/framework-rules.md`.
|
|
76
76
|
|
|
77
77
|
### Verifying the whole chain — `test_elite_chain_toga2.php`
|
|
78
78
|
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-02
|
|
10
10
|
owners: [jcardinal, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Team/Transcripts.php
|
|
@@ -419,13 +419,7 @@ here by section/account, never by value.
|
|
|
419
419
|
`_ApiRequest::execute()`** (reconnect/re-register before the post-call save) and affects every
|
|
420
420
|
framework caller, so it is deferred pending architecture review. Do **not** work around it in
|
|
421
421
|
`Transcripts.php`.
|
|
422
|
-
-
|
|
423
|
-
`c_transcriptAiModelId` / `c_s3*` / `c_trueAiModelId` columns are read off raw `_Query` rows and
|
|
424
|
-
written/filtered as `_Model` array keys **without** being declared in the model class and with **no**
|
|
425
|
-
`Core.RecordFields` / ACL entries (matches the existing `c_clickupUserIdentifier` precedent).
|
|
426
|
-
Consequence: the `_underscore`/model layer required **zero** changes for these columns (Phase 2
|
|
427
|
-
verified NO-OP). See the framework standard
|
|
428
|
-
[`c_`-prefixed custom columns](../../standards/framework-rules.md).
|
|
422
|
+
- **The `c_` columns here never go through /v2 — that is why they needed no model change.** `c_transcriptAiModelId` / `c_s3*` (worker2 `Worker/Team/Transcripts.php`), `c_trueAiModelId` (`tools/mvc/talos/vocabulary/post.php`) and the older `c_clickupUserIdentifier` (worker2 `Worker/Netsuite/Opportunity.php`) are read/written only by hand-written SQL / raw `_Query` rows, which return any column. Through /v2 or a `_Model` instance a `c_` column must be declared — see [`c_`-prefixed custom columns](../../standards/framework-rules.md).
|
|
429
423
|
- **Cross-ACCOUNT S3.** `togaiq` (us-east-1) approved/archive writes use the `[talos]` key;
|
|
430
424
|
`CopyObject` across accounts is impossible, so archive is get(in-memory)+put. (The old toga-private
|
|
431
425
|
cross-account read is no longer in the loop.)
|
|
@@ -445,6 +439,7 @@ here by section/account, never by value.
|
|
|
445
439
|
read will clean every transcript with the merged/global set instead of the routed model's copy.
|
|
446
440
|
|
|
447
441
|
## Change history
|
|
442
|
+
- 2026-10-02 — Corrected the `c_` column note: these columns are raw-SQL only, not proof that /v2 is dynamic. (jcardinal)
|
|
448
443
|
- 2026-09-25 — renamed product references from "TOGa IQ" to "TOGa Talos" (rebrand; TOGa IQ is now the separate `toga25-iq` app) (jcardinal)
|
|
449
444
|
|
|
450
445
|
## Related
|