Files
OSIT-AE-App-Svelte/documentation/AE__Permissions_and_Security.md
Scott Idem 54707a00e3 Refine journal entry config
Polish the Journal Entry Config modal to match the desired section outline, hide alert messaging unless enabled, update the shared draft typing for entry flows, and replace deprecated privacy icons.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-05 17:14:20 -04:00

11 KiB
Raw Blame History

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:

$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:

site_access_code_kv: {
    administrator: null,  // highest passcode tier
    trusted: null,        // onsite staff passcode
    public: 'public1980', // example
    authenticated: 'auth1980'
}

x-no-account-id — Narrow Transport Exception

x-no-account-id is a transport-level escape hatch that strips account context before the request leaves the frontend. It is not a permission grant and it is not a replacement for JWT or x-account-id.

Use it only when the request truly cannot be made account-scoped. Current legitimate cases should stay narrow:

  1. Bootstrap / site-domain discovery before the account is known.
  2. Explicit public or guest endpoints that do not have an account context.
  3. Helper paths that intentionally need a global-default fallback.

If a request already has a valid account context, prefer x-account-id and let the JWT carry session identity. Treat any new x-no-account-id use as temporary until it is reviewed and either replaced or justified.


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.

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:
    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.

JWT usage guidance

  • JWTs are the preferred proof of an established session. Keep them attached to authenticated flows instead of leaning on transport-level bypasses.
  • If a route or helper can work with a JWT and an account ID, it should not need x-no-account-id.
  • If a helper still needs the bypass today, document the reason and add a removal target.

Email Display

Non-trusted users must never see a full email address. Obscure using:

// 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

Journals — Entry Config Admin Actions

  • Entry configuration admin controls are gated to trusted_access and above.
  • manager_access and administrator_access see the Delete action, which performs a hard delete.
  • trusted_access users see Remove instead, which follows disable semantics rather than a hard delete.
  • The Admin section is the place for staff notes, enabled/default access state, and destructive entry actions; the template toggle belongs in Metadata, while visibility/audience flags remain separate.

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

<!-- 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.