sync: auto-sync from GURU-5070 at 2026-06-05 08:06:47
Author: Mike Swanson Machine: GURU-5070 Timestamp: 2026-06-05 08:06:47
This commit is contained in:
@@ -24,8 +24,13 @@ Graph API permissions alone are NOT sufficient for privileged operations. The se
|
|||||||
**Roles assigned so far:**
|
**Roles assigned so far:**
|
||||||
- Valleywide Plastering (5c53ae9f...): User Administrator
|
- Valleywide Plastering (5c53ae9f...): User Administrator
|
||||||
- Dataforth (7dfa3ce8...): User Administrator, Exchange Administrator
|
- Dataforth (7dfa3ce8...): User Administrator, Exchange Administrator
|
||||||
|
- azcomputerguru.com (ce61461e...): full set assigned 2026-06-05 — Sec-Inv + Exch-Op = Exchange Administrator; Tenant Admin = Conditional Access Administrator; User Manager = User Administrator + Authentication Administrator.
|
||||||
|
|
||||||
**For new tenants:** After admin consent, manually assign roles via Entra portal > Roles and administrators. The app cannot self-assign directory roles.
|
**For new tenants:** `onboard-tenant.sh <domain>` assigns the directory roles programmatically (Tenant Admin tier) — no manual portal step needed. The app cannot self-assign; the Tenant Admin SP does it.
|
||||||
|
|
||||||
|
**GOTCHA — pre-2026-04-20 tenants have NO directory roles.** The directory-role assignment block was added to `onboard-tenant.sh` in commit cd50117a on **2026-04-20**. Before that, "onboarding" only did app consent + Graph/EXO API permissions. So any tenant onboarded before that date has full app permissions but **zero directory role assignments** — Graph reads work, but **Exchange REST (quarantine, Get-Mailbox, message trace) and other privileged ops 401** until you re-run `onboard-tenant.sh`. This is NOT a removal/breach — the roles were simply never assigned, and with no Entra ID P2 there's no PIM to auto-expire anything. ACG's own tenant hit exactly this on 2026-06-05 (EOP quarantine check 401'd). **Re-run `onboard-tenant.sh` on any tenant onboarded before 2026-04-20** — Valleywide, Dataforth, Cascades are prime candidates to verify proactively. Confirm actual state with `roleManagement/directory/roleAssignments?$filter=principalId%20eq%20'<sp-oid>'&$expand=roleDefinition` (tenant-admin token; classic endpoint, no P2 needed — the PIM `roleAssignmentSchedules` endpoints return `AadPremiumLicenseRequired` without P2).
|
||||||
|
|
||||||
|
**BUG (fixed 2026-06-05):** `onboard-tenant.sh role_assigned()` had an unencoded space in its `$filter` (`principalId eq '...'`), so the query always failed → function always returned false → script always printed "MISSING -> ASSIGNING" and leaned on the conflict-tolerant POST for idempotency (assignment still worked, but PRESENT/MISSING reporting was meaningless). Fixed to `%20`. The old TODO blaming PIM was a misdiagnosis.
|
||||||
|
|
||||||
### Exchange Online REST API
|
### Exchange Online REST API
|
||||||
|
|
||||||
|
|||||||
BIN
clients/dataforth/ExternalCodeReview.zip
Normal file
BIN
clients/dataforth/ExternalCodeReview.zip
Normal file
Binary file not shown.
257
clients/dataforth/power-monitor-demo/GATEWAY-SPEC.md
Normal file
257
clients/dataforth/power-monitor-demo/GATEWAY-SPEC.md
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
# Power Monitor Demo — Gateway & Publishing Spec
|
||||||
|
|
||||||
|
**Status:** DRAFT v0.1 (living document) · **Date:** 2026-06-05
|
||||||
|
**Owner:** Arizona Computer Guru (advisory) · **Implementer:** Georg Haubner (Dataforth)
|
||||||
|
**Subject:** Standing up a gated, proxied demo of the Power Monitor UI at `PWM.dataforth.com`
|
||||||
|
|
||||||
|
> ACG's role here is to **shape the design via feedback**, not to modify the application
|
||||||
|
> code. This document is the reference architecture we hand to Georg. The SPA itself
|
||||||
|
> ("ExternalCodeReview" build) is unchanged except where called out under
|
||||||
|
> **Feedback for Georg**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Purpose & Scope
|
||||||
|
|
||||||
|
Publish a demo of the Power Monitor dashboard so **technology partners and Dataforth
|
||||||
|
staff** can run feature-testing and feedback sessions. This is **not** the shipped
|
||||||
|
client product — it is a hosted, controlled preview.
|
||||||
|
|
||||||
|
In scope: hosting model, auth, the proxy/policy gateway, per-user permissioning,
|
||||||
|
and the consolidated code-review feedback. Out of scope: rebuilding the meter
|
||||||
|
firmware API or the SPA's internals.
|
||||||
|
|
||||||
|
**Delivery model:** the shipped product is a **computer-run application** that connects to
|
||||||
|
meters over the network and displays their data — it is **not** flashed onto the meter
|
||||||
|
(hardware limits). The demo is the hosted, browser-accessible version of that same app.
|
||||||
|
This corrects the "served from embedded flash" rationale in the source's own `project.md`
|
||||||
|
(see Feedback). Practically: the **shipped product = a local app on the customer's network**
|
||||||
|
(the hardened `dev_server.exe` lineage); the **demo = the hosted version of that app** behind
|
||||||
|
our gateway. Same proxy concept, two trust levels — which is why the SSRF/open-proxy concern
|
||||||
|
is **High for the public demo** but **much lower for the local product** (own LAN, own meters).
|
||||||
|
|
||||||
|
## 2. Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser ──HTTPS──> PWM.dataforth.com
|
||||||
|
│ (outbound tunnel; no meter is ever publicly exposed)
|
||||||
|
▼
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ Demo Gateway │ hosted INSIDE the Dataforth network
|
||||||
|
│ • serves static SPA │
|
||||||
|
│ • passwordless sessions │
|
||||||
|
│ • POLICY enforcement pt │
|
||||||
|
│ • proxies ONLY live mtrs │
|
||||||
|
└───────────┬──────────────┘
|
||||||
|
│ (allowlisted)
|
||||||
|
▼
|
||||||
|
Live DTF meters (LAN)
|
||||||
|
Simulated units = generated client-side in the browser; never touch the gateway.
|
||||||
|
```
|
||||||
|
|
||||||
|
Key insight from code review: **simulated units are produced client-side**; only
|
||||||
|
**live meters** generate network traffic (`/api/Data?ip=`). Therefore the gateway's
|
||||||
|
security boundary is precisely *"what may this user do to a real meter,"* and that
|
||||||
|
must be enforced **on the wire** at the gateway — never trusting the app's
|
||||||
|
(cosmetic) client-side button gating.
|
||||||
|
|
||||||
|
## 3. Device Classes (all three visible in the demo)
|
||||||
|
|
||||||
|
| Class | Source | Marker | Reaches gateway? |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Simulated** | baked into the build | **"SIMULATION" disclaimer** (see Feedback) | No — client-side only |
|
||||||
|
| **Live (curated)** | real DTF meters, fixed allowlist | live indicator | Yes — proxied |
|
||||||
|
| **Explorable** | user-entered IP within the meter subnet | — | Yes — internal users only |
|
||||||
|
|
||||||
|
The demo data Georg built into the executable **stays**. Live units **stay visible**
|
||||||
|
so staff can validate that real readings populate the dashboard correctly.
|
||||||
|
|
||||||
|
## 3.5 Demo Behavior & Build Approach (feedback for Georg)
|
||||||
|
|
||||||
|
### Show everything; simulate what the user can't really do
|
||||||
|
For a *feedback* demo, partners and staff must SEE the full feature set — firmware,
|
||||||
|
config, and IP screens included. So non-admins are **not** shown 403 errors and controls
|
||||||
|
are **not** hidden:
|
||||||
|
- Every screen and control renders for everyone.
|
||||||
|
- Capability held → the write is **real** (proxied to the meter).
|
||||||
|
- Capability absent → the write is routed to a **client-side simulation** returning a
|
||||||
|
believable result. Full interactive experience; no real meter touched.
|
||||||
|
|
||||||
|
(IP visibility for external users is the one exception — soft-hidden per the `see_ip`
|
||||||
|
nice-to-have.)
|
||||||
|
|
||||||
|
### Build it as an isolated DEMO MODULE — not a fork
|
||||||
|
The application is **actively evolving** and the point is to test *current* features.
|
||||||
|
- **Do NOT maintain a separate demo fork** — it drifts immediately; partners would test
|
||||||
|
stale features, and two codebases must be kept in sync. Wrong cost.
|
||||||
|
- **Do add a `demo` mode driven by an isolated module** (one scoped component, enabled by
|
||||||
|
a build flag / env). It applies the SIMULATION disclaimers, reads the user's
|
||||||
|
capabilities (gateway `/me` endpoint), and provides the write-simulation shim above.
|
||||||
|
The shipped product builds **without** the module, so none of it can activate in
|
||||||
|
production.
|
||||||
|
|
||||||
|
### A clean strip point for the production build (consideration)
|
||||||
|
Georg will eventually want a production build with **no demo/mock code** in it. Since this
|
||||||
|
is a **computer-run app, not meter firmware**, the motivation is **code hygiene + safety**
|
||||||
|
(not flash size): removing any path by which fabricated values could render on the shipped
|
||||||
|
product — the flip side of the "mock engine fused into the live data path" finding.
|
||||||
|
|
||||||
|
Two clean options, his call:
|
||||||
|
- **Stay buildless** (current style) -> draw the boundary at the **file level**: extract all
|
||||||
|
sim/mock/demo logic out of `w3js_main.js` into a standalone `demo_module.js`; the demo
|
||||||
|
`index.html` includes it, the production bundle omits it. A runtime `if (DEMO_MODE)` flag
|
||||||
|
is NOT a strip — the code still ships in the bundle.
|
||||||
|
- **Add a small build step** — now a real option, since the original "no Node on embedded
|
||||||
|
hardware" rationale no longer applies. A bundler can dead-code-eliminate a `DEMO_MODE`
|
||||||
|
flag at compile time.
|
||||||
|
|
||||||
|
Either way it's the same extraction that resolves the "simulation fused into the live path"
|
||||||
|
finding — do it once, serve both demo and product. Strictly necessary is Georg's judgment;
|
||||||
|
cheap now, costly to retrofit.
|
||||||
|
|
||||||
|
### Two layers, on purpose (defense in depth)
|
||||||
|
- **Demo module (in the app) = UX layer** — show-all, simulate writes, disclaimers.
|
||||||
|
Trusted only for presentation.
|
||||||
|
- **Gateway = hard security boundary** — independently blocks any real write a user isn't
|
||||||
|
entitled to, on the wire, regardless of what the browser does. Never trusts the app.
|
||||||
|
|
||||||
|
The two are independent: even with a bug in the demo module, the gateway still prevents a
|
||||||
|
non-privileged user from modifying a real meter.
|
||||||
|
|
||||||
|
## 4. Authentication — passwordless magic links
|
||||||
|
|
||||||
|
- Admin (internal) adds a user (name + permission checkboxes); the gateway mints a
|
||||||
|
**one-time magic link**. Admin delivers it however is convenient (email, Teams,
|
||||||
|
in person). Clicking it establishes a durable signed session cookie.
|
||||||
|
- **No passwords** are ever set, stored, rotated, or emailed.
|
||||||
|
- **Revoke** = set the user inactive; the session is invalidated.
|
||||||
|
- v1 needs **no email infrastructure** — the admin copies the link. Automated send
|
||||||
|
via Dataforth M365 SMTP can be added later without changing the model.
|
||||||
|
- **Session lifetime:** Georg's call (a sane default + optional auto-expiry for
|
||||||
|
external users after the feedback window).
|
||||||
|
|
||||||
|
## 5. Authorization — `internal` is the master gate
|
||||||
|
|
||||||
|
| | External user | Internal user |
|
||||||
|
|---|---|---|
|
||||||
|
| Simulated units | full (harmless) | full |
|
||||||
|
| Live meters | **read-only, IPs hidden** | read-only + (per checkbox) write |
|
||||||
|
| See / explore / add / modify IPs | **never** | yes (within meter subnet) |
|
||||||
|
| Destructive on real meters (config/reboot/firmware) | **never** | per-user checkbox |
|
||||||
|
| Manage user list (`is_admin`) | never | internal only (Georg + Mike) |
|
||||||
|
|
||||||
|
- **All IP-related capability is gated by `internal`** — external users never see,
|
||||||
|
explore, add, or modify anything IP-related.
|
||||||
|
- The per-user checkboxes (`write_config`, `reboot_restore`, `update_firmware`) are
|
||||||
|
finer control **among internal users**, applied to real meters.
|
||||||
|
- **Visibility != capability:** non-admins **see every option**; lacking a capability
|
||||||
|
means that action is **simulated**, not hidden or errored (see §3.5).
|
||||||
|
- **New-user defaults:** `internal=false`, `is_admin=false`, all checkboxes off →
|
||||||
|
read-only + simulated units only.
|
||||||
|
|
||||||
|
## 6. User Store (gateway-owned)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
user:
|
||||||
|
name: "Jane Partner"
|
||||||
|
login_handle: jane@partner.example # used to mint/deliver the magic link
|
||||||
|
internal: false # master gate for IP + destructive surface
|
||||||
|
is_admin: false # manage user list (internal only)
|
||||||
|
active: true # uncheck to revoke
|
||||||
|
capabilities: # only meaningful when internal=true
|
||||||
|
write_config: false
|
||||||
|
reboot_restore: false
|
||||||
|
update_firmware: false
|
||||||
|
# IP-related capability (see/explore/modify) is implied by internal=true, not a flag.
|
||||||
|
```
|
||||||
|
|
||||||
|
Flat file or SQLite the gateway owns. Managed via an internal-only `/admin` UI.
|
||||||
|
|
||||||
|
## 7. Gateway Enforcement
|
||||||
|
|
||||||
|
**Request flow:**
|
||||||
|
1. Browser → tunnel → gateway. No valid session → magic-link login.
|
||||||
|
2. Gateway serves the static SPA bundle.
|
||||||
|
3. App issues `/api…?ip=` or `/Control…`. Gateway intercepts every request:
|
||||||
|
a. Resolve user + capabilities from the session.
|
||||||
|
b. **Classify the target IP:** curated-live / explorable-subnet / out-of-range.
|
||||||
|
c. Apply policy (table below). Deny → `403 "not permitted in this demo."`
|
||||||
|
d. Allowed → proxy to the real meter; relay response.
|
||||||
|
|
||||||
|
**Policy (live-meter traffic only):**
|
||||||
|
|
||||||
|
| Action | Endpoints | Allowed for |
|
||||||
|
|---|---|---|
|
||||||
|
| Read telemetry/config | `GET /api/*`, `GET /Control` | everyone (curated-live); internal (explorable) |
|
||||||
|
| Target an explorable IP | any, with `?ip=` outside curated list | **internal only**, and **only within the meter subnet allowlist** |
|
||||||
|
| Write config | `PUT/POST /api/Config/*` | internal + `write_config` |
|
||||||
|
| Reboot / restore | `POST /Control/SystemRestart`, `/Control/RestoreToDefault` | internal + `reboot_restore` |
|
||||||
|
| Firmware update | `PUT /Control/FirmwareUpdate`, `POST /UpdateFile/*` | internal + `update_firmware` |
|
||||||
|
|
||||||
|
**SSRF prevention:** the gateway proxies **only** to IPs inside a configured **meter
|
||||||
|
subnet allowlist** — never arbitrary hosts. This structurally eliminates the open-proxy
|
||||||
|
behavior in the current `dev_server.py`. External users are further restricted to the
|
||||||
|
**curated** live list (a subset of the allowlist).
|
||||||
|
|
||||||
|
## 8. Hosting & Network
|
||||||
|
|
||||||
|
- **Host the gateway INSIDE the Dataforth network** (small VM/container on the meter
|
||||||
|
VLAN) so it can reach live meters directly.
|
||||||
|
- **Expose only the gateway** publicly via an **outbound tunnel** (Cloudflare Tunnel
|
||||||
|
or ACG NPM) → `PWM.dataforth.com`. No meter is ever directly reachable from the
|
||||||
|
internet. This is the correct answer to "need an internal IP available for an
|
||||||
|
external connection" — publish the *gateway*, not a meter IP.
|
||||||
|
- **TLS:** terminated at the tunnel/edge (Cloudflare or Let's Encrypt).
|
||||||
|
- **DNS:** `PWM.dataforth.com` → edge → tunnel → gateway.
|
||||||
|
|
||||||
|
## 9. Build / Publish
|
||||||
|
|
||||||
|
The SPA is static; "build" = produce a clean bundle:
|
||||||
|
- Remove dev/forensic artifacts: `search_transcript_*.py`, `query_all_devices.py`,
|
||||||
|
and the `original_ui/` duplicate tree (also leaks internal meter IPs + a developer
|
||||||
|
username — must not ship externally).
|
||||||
|
- Pin CDN dependencies (jQuery, Chart.js) **locally** for reliability and offline
|
||||||
|
operation (a customer-site app shouldn't depend on public CDNs).
|
||||||
|
- Gateway serves the bundle; live data flows through the policy proxy.
|
||||||
|
|
||||||
|
## 10. Feedback for Georg (consolidated from the code review)
|
||||||
|
|
||||||
|
**A. Demo-publish items**
|
||||||
|
1. **Simulation disclaimer** — mark simulated units with a clear "SIMULATION —
|
||||||
|
representative of real product data" badge. (Keep the demo data; just label it.)
|
||||||
|
2. **No open proxy** — the public demo must use the allowlisted gateway, never the
|
||||||
|
current `dev_server.py` (binds all interfaces, proxies arbitrary `ip=`).
|
||||||
|
3. **Strip dev/forensic artifacts** before any external handoff (see §9).
|
||||||
|
4. **Enforce permissions at the gateway**, not via client-side button-disabling
|
||||||
|
(which is cosmetic and bypassable).
|
||||||
|
|
||||||
|
**B. Product-ship items (for the eventual client product, not the demo)**
|
||||||
|
- On-device **TLS** (login/creds/SMTP currently traverse plain HTTP).
|
||||||
|
- **Firmware image signature verification** on the meter (the UI validates nothing).
|
||||||
|
- Confirm `RestoreToDefault` is server-authorized (callable without a session client-side).
|
||||||
|
- **Escape device-supplied strings** before `innerHTML` (stored DOM-XSS via TagName/name).
|
||||||
|
- Replace the **numeric session token** with an opaque/expiring token; add idle timeout.
|
||||||
|
- De-duplicate the hardcoded IP allowlists (copied across 5–6 files) and collapse the
|
||||||
|
`original_ui/` + `_v2` duplicate trees.
|
||||||
|
- Separate the client-side mock engine from the live data path so real meters never
|
||||||
|
show fabricated values.
|
||||||
|
|
||||||
|
## 11. Open Parameters (need Georg / Dataforth input)
|
||||||
|
|
||||||
|
1. **Curated live-meter allowlist** — which real meters are visible in the demo
|
||||||
|
(names + IPs), and which are external-visible vs internal-only.
|
||||||
|
2. **Meter subnet allowlist** — the IP range `explore_ip` (internal) may target.
|
||||||
|
3. **Session lifetime** — Georg's call (+ external auto-expiry?).
|
||||||
|
4. **Magic-link delivery** — manual copy for v1, or wire M365 SMTP now?
|
||||||
|
5. **Hosting specifics** — VM/container location on the DTF network; tunnel choice
|
||||||
|
(Cloudflare Tunnel vs ACG NPM).
|
||||||
|
|
||||||
|
## 12. Future / Out of Scope (noted, not required for the demo)
|
||||||
|
|
||||||
|
- **Opaque device handles** (gateway issues IDs; maps ID→IP) for *true* IP hiding
|
||||||
|
from external users — deferred; v1 uses simple UI-hiding (`see_ip` nice-to-have).
|
||||||
|
- On-device TLS, firmware signing, full session hardening — product-phase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Living draft — iterating with Mike before handoff to Georg.*
|
||||||
73
clients/dataforth/power-monitor-demo/QUESTIONS-FOR-GEORG.md
Normal file
73
clients/dataforth/power-monitor-demo/QUESTIONS-FOR-GEORG.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Power Monitor Demo — Questions for Georg
|
||||||
|
|
||||||
|
**Status:** DRAFT (for Mike's review before sending) · **Date:** 2026-06-05
|
||||||
|
Companion to `GATEWAY-SPEC.md`. Where we have a recommendation it's noted as **(our take)**
|
||||||
|
so Georg can confirm or redirect rather than start from scratch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A. Intent, audience, timeline
|
||||||
|
1. **Feedback format** — is a session a live demo you walk partners through, or self-service
|
||||||
|
access they poke at on their own? (Drives how much auth friction is acceptable.)
|
||||||
|
2. **Testers** — roughly how many, and the internal/external split? Any names/emails you
|
||||||
|
already know, so we can size the user list?
|
||||||
|
3. **Timeline** — when do you want `PWM.dataforth.com` reachable for the first round?
|
||||||
|
4. **Shipped product form** — is the eventual customer product a **packaged desktop app
|
||||||
|
they install** (the `dev_server.exe` lineage) or an **on-prem server** they browse to?
|
||||||
|
Doesn't block the demo; it shapes the product-side advice (install/update, where auth lives).
|
||||||
|
|
||||||
|
## B. Hosting & access (the "external connection" from your email)
|
||||||
|
5. Your *"need an internal IP available for an external connection"* note — what did you
|
||||||
|
picture for letting outside partners reach this? **(Our take:** host a small gateway
|
||||||
|
**inside** your network and tunnel it out to `PWM.dataforth.com` — no meter is ever
|
||||||
|
exposed publicly. Does that fit?)
|
||||||
|
6. Where can a small always-on box (VM/container) live **on the meter network** to host
|
||||||
|
that gateway?
|
||||||
|
7. Is `dataforth.com` on **Cloudflare** (we'd use a Cloudflare Tunnel), or should we front
|
||||||
|
it with **our reverse proxy / NPM** for the tunnel + TLS?
|
||||||
|
|
||||||
|
## C. Meters & demo data
|
||||||
|
8. **Which real meters** should appear in the demo (names + IPs)?
|
||||||
|
9. For `explore / add a meter by IP` (internal users only), **what IP range** should that
|
||||||
|
be limited to?
|
||||||
|
10. **Simulated units stay**, with a clear **"SIMULATION — representative data"** marker —
|
||||||
|
agreed? **(Our take:** yes — keep them, just label them.)
|
||||||
|
11. Should **external partners see live meter *data*** at all (IPs always hidden), or should
|
||||||
|
externals be **simulation-only**? You mentioned staff validating live data — confirming
|
||||||
|
whether that extends to outside partners.
|
||||||
|
|
||||||
|
## D. Users & permissions
|
||||||
|
12. Confirm the model: **simple self-managed user list** (no AD/SSO), **passwordless
|
||||||
|
magic-link** logins, **you + Mike as admins**. **(Our take:** yes.)
|
||||||
|
13. Per-internal-user destructive toggles we'd gate: **firmware update, config write,
|
||||||
|
reboot/restore**. Anything else you want as a switch (e.g. CSV/data export, clearing
|
||||||
|
event logs)?
|
||||||
|
14. Should **firmware update even be reachable** in the demo (as a *simulated* screen so
|
||||||
|
partners can give feedback on it), or **hidden entirely**?
|
||||||
|
15. **New external users default to read-only + simulated-only** — agreed?
|
||||||
|
|
||||||
|
## E. Code structure & build (so the demo doesn't fight the product)
|
||||||
|
16. Open to **extracting the simulation/mock logic out of `w3js_main.js` into a standalone
|
||||||
|
"demo module"** — so the same code drives the demo *and* can be cleanly omitted from a
|
||||||
|
production build? **(Our take:** yes; it also resolves the "mock data fused into the live
|
||||||
|
path" item.)
|
||||||
|
17. **Buildless** (include/omit the demo file) vs **adding a small build step** — preference?
|
||||||
|
Note: since this is **never getting flashed onto the meter**, the "no build tools"
|
||||||
|
rationale in your `project.md` no longer applies — a build step is back on the table if
|
||||||
|
you'd find it useful.
|
||||||
|
18. How are you iterating with **Antigravity** — should the demo **always track your latest
|
||||||
|
build**, or be a **pinned snapshot** per feedback round?
|
||||||
|
|
||||||
|
## F. What you want from ACG
|
||||||
|
19. Concretely, what's our role: **(a)** stand up + host the gated demo gateway, **(b)** a
|
||||||
|
formal external code-review writeup, **(c)** advise/shape only — or some combination?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Heads-up worth mentioning to Georg regardless
|
||||||
|
- `project.md` still justifies the whole vanilla-JS / zero-dependency design on *"serve from
|
||||||
|
embedded hardware flash."* If it's never flashed, that rationale is outdated — which quietly
|
||||||
|
**reopens technology choices** (a light framework or build step) he may have ruled out.
|
||||||
|
- The package as-sent contains dev/forensic artifacts (Gemini "Antigravity" transcript
|
||||||
|
scrapers with his local path, a script listing live internal meter IPs) and an
|
||||||
|
`original_ui/` duplicate — none of which should travel in an external deliverable.
|
||||||
132
session-logs/2026-06-05-session.md
Normal file
132
session-logs/2026-06-05-session.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Session Log — 2026-06-05
|
||||||
|
|
||||||
|
## User
|
||||||
|
- **User:** Mike Swanson (mike)
|
||||||
|
- **Machine:** GURU-5070
|
||||||
|
- **Role:** admin
|
||||||
|
|
||||||
|
## Session Summary
|
||||||
|
|
||||||
|
The session began as a request to check Exchange Online Protection for an email from "Georg" that Mike could not find. The EOP quarantine query against the ACG tenant (azcomputerguru.com) returned HTTP 401 on every Exchange REST adminapi call — including a known-good `Get-Mailbox` — proving the failure was not the quarantine cmdlet but the Security Investigator service principal lacking Exchange permissions on ACG's own tenant. Investigation showed the SP held zero directory role assignments. The Exchange Operator role had been assigned to its SP back on 2026-05-15, but the others had never been set.
|
||||||
|
|
||||||
|
Root-causing "what happened" to the permissions: directory-audit logs (30-day retention) contained zero "Remove member from role" events, and the PIM/schedule endpoints returned `AadPremiumLicenseRequired` — ACG has no Entra ID P2, so PIM cannot auto-expire role assignments. Git blame on `onboard-tenant.sh` showed the directory-role assignment block was added on 2026-04-20 (commit cd50117a). ACG was onboarded before that date, so it received app consent + API permissions but never the directory roles. Nothing was removed; the roles were simply never assigned. The fix was to run `onboard-tenant.sh azcomputerguru.com`, which assigned Exchange Administrator (Security Investigator + Exchange Operator), Conditional Access Administrator (Tenant Admin), and User Administrator + Authentication Administrator (User Manager). A secondary bug was found and fixed: `role_assigned()` had an unencoded space in its Graph `$filter`, so the query always failed and the function always returned false.
|
||||||
|
|
||||||
|
With Exchange access restored (via the Exchange Operator token, whose role had replicated since 5/15), the email hunt resumed. Georg Haubner (ghaubner@dataforth.com) sent Mike two emails on 6/4 ("Need internal IP address available for external connection"), both delivered to the Inbox and marked read. The email Mike was actually missing was a third reply at 6:30pm MST (01:30 UTC 6/5). The ACG inbound trace had no record of it, yet the Dataforth outbound trace showed it "Delivered." The Dataforth message-trace detail revealed it was routed by a transport rule ("INKY - Annotation - Recipient Not Group Member") to Mailprotector's outbound gateway (dataforth-com.outbound.emailservice.io / 52.3.213.180) and never re-emerged to ACG — it was held at Mailprotector. Mike released it manually. Dataforth layers INKY (annotation) + Mailprotector CloudFilter (outbound delivery) on top of Exchange Online.
|
||||||
|
|
||||||
|
Mike then asked whether Mailprotector has an API. Research established the live console/API is `emailservice.io` (`console.mailprotector.com` is dead; `api.mailprotector.com` is only a Postman doc mirror). Mike provided an API key; using his `MailProtectorAPI.pdf` (74-page image-based PDF, rendered via newly-installed poppler), the full REST surface was catalogued. The key was vaulted and a new `/mailprotector` skill was built (mirroring the packetdial pattern), code-reviewed (APPROVED), and committed.
|
||||||
|
|
||||||
|
The session closed on a larger thread: the `ExternalCodeReview.zip` Mike had saved in `clients/dataforth/` is Georg's evolving Power Monitor dashboard (an AI/Antigravity-built vanilla-JS SPA for Dataforth's power meters). Three analysis agents produced a full understanding (frontend, device API/tooling, security). Mike then specified a near-term goal: publish a gated, proxied demo at PWM.dataforth.com for partner/staff feedback. Across several iterations we shaped the architecture and wrote `GATEWAY-SPEC.md` and `QUESTIONS-FOR-GEORG.md`. This demo work is parked pending Mike's conversation with Georg later today.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
- **Diagnosed the permission gap as "never assigned," not "removed."** No removal events in audit retention + no Entra P2 (so no PIM expiry) + git blame showing the directory-role feature postdates ACG's onboarding = roles were never set. Avoided chasing a phantom breach/removal.
|
||||||
|
- **Used the Exchange Operator token (role replicated since 5/15) to read mail immediately** rather than waiting on the freshly-granted Security Investigator role to replicate (which had not completed even after ~35 min).
|
||||||
|
- **Switched to `Get-MessageTraceV2`** after `Get-MessageTrace` returned BadRequest (Microsoft retired the V1 cmdlet).
|
||||||
|
- **Vaulted the Mailprotector key and built a reusable skill** rather than a one-off script — the hunt-and-release is now ~2 API calls and works across every client on the CloudFilter stack.
|
||||||
|
- **Session log placed at root `session-logs/`** (multi-scope: ACG tenant + claudetools skill + Dataforth), so the wiki-compile phase was skipped.
|
||||||
|
- **Demo architecture (advisory only — not changing Georg's code):** gateway hosted INSIDE the Dataforth network + outbound tunnel to PWM.dataforth.com (no meter publicly exposed); passwordless admin-minted magic links; a simple self-managed user list (explicitly NOT AD/Entra); `internal` flag as the master gate for all IP-related + destructive surface; per-user capability checkboxes among internal users; new users default read-only + simulation-only.
|
||||||
|
- **Show-everything-simulate-the-rest UX:** non-admins see all controls; lacking a capability routes the write to a client-side simulation, not a 403 — the gateway remains the hard backstop.
|
||||||
|
- **Demo as an isolated module, not a fork** (app is actively evolving; a fork would drift). Corrected mid-thread: the product is a computer-run app, not flashed to the meter — so the "embedded flash / no build step" rationale in Georg's project.md is outdated, and the demo-strip motivation is hygiene/safety, not flash size.
|
||||||
|
|
||||||
|
## Problems Encountered
|
||||||
|
|
||||||
|
- **EOP quarantine 401 on ACG tenant** — Security Investigator SP had no directory roles. Resolved by running `onboard-tenant.sh azcomputerguru.com` (assigned all required roles).
|
||||||
|
- **`onboard-tenant.sh role_assigned()` always returned false** — unencoded space in `?$filter=principalId eq '...'`. Fixed to `%20`; verified live (unencoded → empty/error, encoded → n:1). Committed bf58675.
|
||||||
|
- **Fresh Security Investigator role would not replicate to Exchange RBAC** (~35+ min, still 401). Worked around by using the long-standing Exchange Operator role token instead.
|
||||||
|
- **Background quarantine poll task (biy6ecbqa) failed** — 27-min timeout waiting on replication; superseded by the Exchange Operator approach. No impact.
|
||||||
|
- **`Get-MessageTrace` BadRequest** — retired by Microsoft. Used `Get-MessageTraceV2`.
|
||||||
|
- **`api.mailprotector.com` and `console.mailprotector.com` dead-ends** — former is a Postman docs shell, latter does not resolve. Real API base is `emailservice.io/api/v1`.
|
||||||
|
- **PDF unreadable** — Read tool needs poppler (`pdftoppm`); `pytddf`/PyPDF2 absent; `py -` opened a REPL. Installed poppler via winget; PDF was image-based (74 bytes text from 74 pages) so rendered pages to PNG and read visually.
|
||||||
|
- **`/tmp` path mismatch on Windows** — extracted the zip to `C:\Users\guru\AppData\Local\Temp\ecr_work` so the Read tool and agents could reach it.
|
||||||
|
- **Self-correction:** claimed to have added the strip-point note to GATEWAY-SPEC.md before actually writing it; corrected and wrote it in the following turn.
|
||||||
|
|
||||||
|
## Configuration Changes
|
||||||
|
|
||||||
|
**claudetools repo:**
|
||||||
|
- Modified: `.claude/skills/remediation-tool/scripts/onboard-tenant.sh` — `role_assigned()` `%20` fix + corrected stale PIM-misdiagnosis comment. (commit bf58675)
|
||||||
|
- Created: `.claude/skills/mailprotector/SKILL.md`, `scripts/mp_client.py`, `scripts/mp.py`, `references/api.md` — new `/mailprotector` skill. (commit ce97448)
|
||||||
|
- Created: `clients/dataforth/power-monitor-demo/GATEWAY-SPEC.md`, `clients/dataforth/power-monitor-demo/QUESTIONS-FOR-GEORG.md` — demo shaping docs (committed by this session's sync).
|
||||||
|
- Note: `.claude/memory/feedback_365_remediation_tool.md` edit (ACG roles + onboarding-gap gotcha) was reverted by Mike/linter; not re-added.
|
||||||
|
|
||||||
|
**vault repo:**
|
||||||
|
- Created: `msp-tools/mailprotector.sops.yaml` — Mailprotector API key. (commit b30923c)
|
||||||
|
|
||||||
|
**System (GURU-5070):**
|
||||||
|
- Installed poppler via `winget install oschwartz10612.Poppler` (for PDF rendering; `pdftoppm`/`pdftotext` at `%LOCALAPPDATA%\Microsoft\WinGet\Packages\oschwartz10612.Poppler_*\poppler-25.07.0\Library\bin`).
|
||||||
|
|
||||||
|
**M365 — ACG tenant directory roles assigned (via onboard-tenant.sh):**
|
||||||
|
- Security Investigator SP (9d242c15-…) → Exchange Administrator
|
||||||
|
- Exchange Operator SP (83c225f1-…) → Exchange Administrator
|
||||||
|
- Tenant Admin SP (046a7f70-…) → Conditional Access Administrator
|
||||||
|
- User Manager SP (04020a4e-…) → User Administrator + Authentication Administrator
|
||||||
|
|
||||||
|
## Credentials & Secrets
|
||||||
|
|
||||||
|
- **Mailprotector CloudFilter API key:** `R-PTIbuYNwEgmGbaJly69t77zXg1hX-HRo4atjdaKMo`
|
||||||
|
- Vaulted at `msp-tools/mailprotector.sops.yaml` (field `credentials.api_key`).
|
||||||
|
- Bearer token. Per-manager-role key from `emailservice.io/profile` → role → "View API Key".
|
||||||
|
- Plaintext temp copy (`/tmp/mp-token`) was deleted after vaulting.
|
||||||
|
|
||||||
|
## Infrastructure & Servers
|
||||||
|
|
||||||
|
- **ACG M365 tenant:** azcomputerguru.com → `ce61461e-81a0-4c84-bb4a-7b354a9a356d`. No Entra ID P2 (PIM unavailable).
|
||||||
|
- **Dataforth M365 tenant:** dataforth.com → `7dfa3ce8-c496-4b51-ab8d-bd3dcd78b584`. ACG holds GDAP "Partner Technician" delegation into it.
|
||||||
|
- **ComputerGuru MSP app SPs (ACG tenant):** Security Investigator app `bfbc12a4-f0dd-4e12-b06d-997e7271e10c` / SP `9d242c15-6cd3-46ec-96d3-bcafaaaca333`; Exchange Operator app `b43e7342-5b4b-492f-890f-bb5a4f7f40e9` / SP `83c225f1-b38d-4063-9fdd-642b6b09ae8b`; Tenant Admin SP `046a7f70-bb08-4309-ae14-5fb5e9e78b62`; User Manager SP `04020a4e-4faf-4dda-b4f1-cd2151f00ed6`.
|
||||||
|
- **Exchange Administrator role template:** `29232cdf-9323-42fd-ade2-1d097af3e4de`.
|
||||||
|
- **Dataforth mail security stack:** INKY (annotation; `*.inkyphishfence.com`) + Mailprotector CloudFilter (outbound/inbound delivery; `dataforth-com.outbound.emailservice.io`, 52.3.213.180) layered on Exchange Online via transport rules + connectors. Outbound connector "Outbound-Mailprotector" (recipientDomains `*`). The held message was routed by transport rule "INKY - Annotation - Recipient Not Group Member".
|
||||||
|
- **Mailprotector:** console/login + API host `https://emailservice.io`; API base `https://emailservice.io/api/v1` (Bearer). `api.mailprotector.com` = Postman docs only; `console.mailprotector.com` = dead.
|
||||||
|
|
||||||
|
## Commands & Outputs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Resolve tenant + get EXO token (remediation-tool)
|
||||||
|
bash get-token.sh azcomputerguru.com investigator-exo # cert auth
|
||||||
|
|
||||||
|
# Assign Exchange Admin to an SP (Graph, tenant-admin token)
|
||||||
|
POST https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments
|
||||||
|
{"principalId":"<sp>","roleDefinitionId":"29232cdf-...","directoryScopeId":"/"}
|
||||||
|
|
||||||
|
# Full onboarding (assigns all directory roles, idempotent)
|
||||||
|
bash onboard-tenant.sh azcomputerguru.com # exit 0, all roles ASSIGNED
|
||||||
|
|
||||||
|
# role_assigned() bug proof
|
||||||
|
?$filter=principalId eq '<sp>' -> empty/error (unencoded space)
|
||||||
|
?$filter=principalId%20eq%20'<sp>' -> n:1 (fixed form)
|
||||||
|
|
||||||
|
# Exchange Online held-mail / trace (adminapi InvokeCommand)
|
||||||
|
Get-QuarantineMessage -RecipientAddress mike@azcomputerguru.com
|
||||||
|
Get-MessageTraceV2 -SenderAddress ghaubner@dataforth.com -RecipientAddress mike@azcomputerguru.com
|
||||||
|
Get-MessageTraceDetailV2 -MessageTraceId <id> # showed INKY rule -> emailservice.io
|
||||||
|
|
||||||
|
# Mailprotector API (validate + held mail + release)
|
||||||
|
GET https://emailservice.io/api/v1/domains # 200 (token valid)
|
||||||
|
GET https://emailservice.io/api/v1/{scope}/{id}/messages # list held/quarantined
|
||||||
|
POST https://emailservice.io/api/v1/messages/{id}/deliver # release one
|
||||||
|
POST https://emailservice.io/api/v1/{scope}/{id}/messages/deliver_many
|
||||||
|
|
||||||
|
# New skill smoke test
|
||||||
|
py mp.py status # HTTP 200, auth valid
|
||||||
|
py mp.py domains # 200, live domains
|
||||||
|
|
||||||
|
# PDF render (poppler)
|
||||||
|
pdftoppm -png -r 140 MailProtectorAPI.pdf pg # 74 pages -> PNG (text layer empty)
|
||||||
|
```
|
||||||
|
|
||||||
|
Held message MessageId: `<SN4PR0201MB3613C475FC8175669B30464AC2112@SN4PR0201MB3613.namprd02.prod.outlook.com>`
|
||||||
|
|
||||||
|
## Pending / Incomplete Tasks
|
||||||
|
|
||||||
|
- **Dataforth Power Monitor demo — PARKED** pending Mike↔Georg conversation (later today 2026-06-05). Deliverables ready: `clients/dataforth/power-monitor-demo/GATEWAY-SPEC.md` (living draft) + `QUESTIONS-FOR-GEORG.md` (opened in Notepad++ for Mike's edits).
|
||||||
|
- After Georg's answers: optionally produce a polished **Georg-facing** version of the questions + a clean external code-review writeup; fold any decisions back into GATEWAY-SPEC.md.
|
||||||
|
- **Open parameters for Georg** (GATEWAY-SPEC §11): curated live-meter allowlist, explore-IP subnet, session lifetime, magic-link delivery (manual vs M365 SMTP), hosting box + tunnel choice (Cloudflare vs ACG NPM), product form (desktop app vs on-prem server).
|
||||||
|
- **Mailprotector:** consider an allow-rule for ghaubner@dataforth.com / dataforth.com to prevent future legit-mail holds (API supports allow/block rules); optionally wire email-send for magic links later.
|
||||||
|
- **Other tenants still on the old deprecated app** (per gotchas: Valleywide, Dataforth, Cascades) — Dataforth confirmed this session to 401 on the new Security Investigator SP for Exchange; candidates for onboard-tenant.sh.
|
||||||
|
- Temp extractions left on disk: `C:\Users\guru\AppData\Local\Temp\ecr_work` and `/tmp/ecr` (can be cleaned).
|
||||||
|
|
||||||
|
## Reference Information
|
||||||
|
|
||||||
|
- **Commits — claudetools:** `bf58675` (onboard role_assigned %20 fix), `ce97448` (mailprotector skill). **Commit — vault:** `b30923c` (mailprotector key).
|
||||||
|
- **New skill:** `.claude/skills/mailprotector/` — `py mp.py messages domains <id> --sender <addr>` then `py mp.py release <msg_id> --confirm`. Read-only by default; writes gated behind `--confirm`. Release permission gotcha: entity config `permissions.messages.allow_spam_release` must be true.
|
||||||
|
- **Demo project source:** `clients/dataforth/ExternalCodeReview.zip` (115 files; vanilla-JS SPA + `dev_server.py` proxy; built by Google Antigravity/Gemini under `ghaubner`, 8000+ step transcript). Proposed demo domain: `PWM.dataforth.com`.
|
||||||
|
- **Mailprotector API doc:** `C:\Users\guru\Downloads\MailProtectorAPI.pdf` (74 pp). Public mirror: https://api.mailprotector.com/ ; help center: https://support.mailprotector.com/hc/en-us/articles/200156105-Mailprotector-API-Documentation
|
||||||
|
- **Georg Haubner:** ghaubner@dataforth.com (Dataforth).
|
||||||
Reference in New Issue
Block a user