Files
OSIT-AE-App-Svelte/documentation/AE__Permissions_and_Security.md
Scott Idem b8e6bcaf03 fix(idaa): strip API calls from all +page.ts/+layout.ts, gate loading in $effect
SvelteKit load functions fire during link prefetch before Novi auth completes;
`if (browser)` guards do not prevent this. Moving all IDAA data fetching into
$effect hooks gated on `novi_verified || trusted_access` closes the IDB
pre-population race across archives, bb/[post_id], and recovery_meetings/[event_id].

Also documents the Auth-Before-Cache rule and per-route status in
AE__Permissions_and_Security.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 18:49:47 -04:00

208 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Aether — Permissions and Security
**Last updated:** 2026-02-27
**Source of truth:** `src/lib/ae_utils/ae_utils__perm_checks.ts`, `src/lib/stores/ae_stores.ts`
---
## Access Level Hierarchy
Highest to lowest. Each level **inherits all access from every level below it**.
| Level | `access_type` string | Typical Use |
| --- | --- | --- |
| Super | `super` | OSIT internal — full system access |
| Manager | `manager` | Account managers |
| Administrator | `administrator` | Event/account admins |
| Trusted | `trusted` | **Onsite staff** — site passcode or AE login |
| Public | `public` | Site-wide passcode granted |
| Authenticated | `authenticated` | Identity verified (e.g. IDAA Novi UUID) |
| Anonymous | `anonymous` | Default — not signed in |
> **Note on Public vs Authenticated:** `public` is a *site-wide* unlock (anyone with the passcode). `authenticated` verifies a *specific identity*. In the hierarchy, public outranks authenticated because it implies broader site access.
---
## `$ae_loc` Store — Permission Flags
`$ae_loc` is a `persisted()` store (backed by localStorage). Key fields:
```typescript
$ae_loc.access_type // string: current access type ('anonymous', 'trusted', etc.)
// Cumulative boolean flags (true = "you have AT LEAST this level")
$ae_loc.anonymous_access // always true
$ae_loc.authenticated_access // true from authenticated and above
$ae_loc.public_access // true from public and above
$ae_loc.trusted_access // true from trusted and above ← most-used gate
$ae_loc.administrator_access // true from administrator and above
$ae_loc.manager_access // true from manager and above
$ae_loc.super_access // true only at super
// Exclusive check flags (true = "you are EXACTLY this level")
$ae_loc.trusted_check // true only if access_type === 'trusted'
$ae_loc.administrator_check // etc.
// (rarely needed — prefer the _access flags)
// Behavior flags
$ae_loc.edit_mode // boolean — user preference, see below
$ae_loc.adv_mode // boolean — advanced mode toggle
```
### Additional intermediate levels (in permission checks, not in hierarchy order)
`support`, `assistant`, `verified`, `provisional` — appear in `_access` flags but are not part of the canonical `access_level_order`. Treat as internal/intermediate.
---
## Edit Mode — Critical Rules
`$ae_loc.edit_mode` is a **user preference**, not a permission level.
**Rules that must never be broken:**
1. **Components must never write to `$ae_loc.edit_mode`** — only the system menu toggle and sign-out/permission-drop handlers may change it.
2. Edit mode is only available to `trusted` and above in 95% of modules (the toggle is hidden from lower-access users).
3. Edit mode persists across navigation — it is NOT reset by page loads or component mounts.
4. Sign-out and permission drops to below `authenticated` should reset `edit_mode` to `false`.
> **Background:** A bug was fixed (2026-02-27) where `ae_comp__badge_obj_view.svelte` was writing `$ae_loc.edit_mode = false` in a data-loading `$effect`, silently overriding the user's preference on every navigation to the badge print page.
---
## Authentication Methods
| Method | Grants | Used For |
| --- | --- | --- |
| Site passcode (`site_access_code_kv`) | `trusted`, `public`, or `authenticated` | Onsite staff and event attendees |
| AE Username + Password | `trusted` and above | Staff with AE accounts |
| Novi UUID | `authenticated` | IDAA members (Novi membership system) |
Passcodes are stored per-level in `$ae_loc.site_access_code_kv`:
```typescript
site_access_code_kv: {
administrator: null, // highest passcode tier
trusted: null, // onsite staff passcode
public: 'public1980', // example
authenticated: 'auth1980'
}
```
---
## Utility Functions
### `process_permission_checks(access_type: string)`
Returns a full permission object (`_check` and `_access` flags) for a given access type string. Used when access type changes to update `$ae_loc`.
```typescript
import { process_permission_checks } from '$lib/ae_utils/ae_utils__perm_checks';
const checks = process_permission_checks('trusted');
// checks.trusted_access === true
// checks.administrator_access === false
```
### `compare_access_levels(level_a, level_b)`
Returns `1` if `level_a` is higher, `-1` if lower, `0` if equal. Useful for threshold comparisons.
---
## Privacy and Security Rules
### IDAA — International Doctors in Alcoholics Anonymous
- **ALL IDAA content is private. Always. No exceptions.**
- BB (Bulletin Board / Posts), Archives, Recovery Meetings — all require authentication.
- IDAA users authenticate via Novi UUID at `authenticated` level or higher.
- A prior agent accidentally exposed IDAA BB data publicly — treat any IDAA exposure as Sev-1.
#### IDAA IndexedDB (IDB) Caching — Auth-Before-Cache Rule
**Root cause discovered 2026-04:** SvelteKit `+page.ts`/`+layout.ts` load functions run *before* layout `$effect` hooks and fire during link prefetch (hover). `if (browser)` guards do NOT prevent this — they only prevent SSR. This means API calls inside these files execute before Novi auth completes, writing private IDAA data to the user's IndexedDB even for unauthenticated sessions.
**The fix — established pattern for all IDAA routes:**
1. **Load/layout `.ts` files = thin shells.** Pass URL params only. No API calls. No `if (browser)` data fetching.
2. **Data loading = `$effect` in `.svelte` files**, gated on:
```svelte
if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return;
```
3. **Three IDB purge paths** in `(idaa)/+layout.svelte` (auth failure, anonymous no-UUID, Reset & Retry button) clear `db_posts`, `db_archives`, and `db_events` tables.
**Auth path matrix:**
| User type | `novi_verified` | `trusted_access` | Can load data? | Purge fires? |
| --- | --- | --- | --- | --- |
| Anonymous / unauthenticated | false | false | No | Yes (Case 1) |
| Novi-verified IDAA member | true | false | Yes | No |
| Manager / trusted access | false | true | Yes | No (Case 3 exemption) |
**Applied to routes (as of 2026-04-19):**
- `idaa/bb/+page.svelte` — `$effect` gate added; `bb/+page.ts` stripped
- `idaa/bb/[post_id]/+page.ts` — stripped; loading handled by trigger in `bb/+layout.svelte`
- `idaa/archives/+page.svelte` — `$effect` gate added; `archives/+layout.ts` stripped
- `idaa/archives/[archive_id]/+page.svelte` — `$effect` gate added; `[archive_id]/+page.ts` stripped
- `idaa/recovery_meetings/+page.svelte` — `$effect` gate already present; `+layout.ts` stripped
- `idaa/recovery_meetings/[event_id]/+page.svelte` — `$effect` gate added; `+page.ts` stripped
**When adding a new IDAA route:** never put API calls in `+page.ts`/`+layout.ts`. Always gate data fetching with the `$effect` pattern above.
### Journals
- Private personal data. Always authenticated. Passcode/encryption features exist.
- Never expose journal content publicly.
### `PUBLIC_AE_API_SECRET_KEY`
- Audit closed 2026-03-11. `PUBLIC_*` prefix is by design — key is always in the client bundle.
- Anonymous site-domain lookup uses the limited-permission `PUBLIC_AE_BOOTSTRAP_KEY` instead.
- Security model: API key is one layer; JWT + `x-account-id` scoping provides the primary auth.
- Do not introduce new usages. Prefer `PUBLIC_AE_BOOTSTRAP_KEY` for unauthenticated lookups.
### Email Display
Non-trusted users must never see a full email address. Obscure using:
```typescript
// joh***@example.com
function obscure_email(email: string): string {
const at = email.indexOf('@');
if (at < 0) return email;
return `${email.slice(0, Math.min(3, at))}***${email.slice(at)}`;
}
```
This pattern lives in `ae_comp__badge_obj_li.svelte` — move to `ae_utils` if needed elsewhere.
---
## Module-Specific Permission Patterns
### Events — Badges
| Scenario | Visibility | Print Action | Review Actions |
| --- | --- | --- | --- |
| Anonymous / below trusted | Unprinted only | None (name display only) | Email Review Link button (→ email API) |
| Trusted, not Edit Mode | Unprinted only | Clickable (first print) | Email Review Link button |
| Trusted, Edit Mode | All non-hidden | Clickable incl. reprint; shows `Nx` count | Email Review Link + direct Review Link (clipboard) |
- Print count badge: shown as `Nx` (e.g. `2×`) next to the printer icon when `print_count >= 1`
- Edit mode for badges: limited to `trusted_access` users (toggle hidden from lower levels)
- `person_passcode` field (for attendee-gated review URL): **not yet in DB** as of 2026-02-27
### IDAA
- Auth gate test must be the **first test** in any test file — privacy enforcement is a hard requirement.
- Default required permission: `trusted_access` or higher for module access.
---
## Common Template Patterns
```svelte
<!-- Gate on trusted access -->
{#if $ae_loc.trusted_access}
<!-- Gate on edit mode (always check trusted too — edit mode alone is insufficient) -->
{#if $ae_loc.trusted_access && $ae_loc.edit_mode}
<!-- Gate on administrator -->
{#if $ae_loc.administrator_access}
<!-- Show full vs obscured email -->
{$ae_loc.trusted_access ? email : obscure_email(email)}
```
> Never gate purely on `$ae_loc.edit_mode` without also checking a permission level. Edit mode is a UI preference, not a permission grant.