Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ _Avoid_: vault_name, vault_handle

**VaultHandle**: Human-readable name for a Vault (e.g., `default`). Used in UIs and CLI; the VaultId is authoritative in storage.

**IdentityClaimRecord**: Binding from an Identity (Handle) to a Principal (PrincipalId) with a `ClaimStatus`. Created during `authsome init --email`. Vault access is gated until the claim is accepted.
**IdentityClaimRecord**: Binding from an Identity (Handle) to a Principal (PrincipalId) with a `ClaimStatus`. Created when an authenticated Principal confirms the browser claim that `authsome init` initiates. Vault access is gated until the claim is accepted.
_Avoid_: Claim, IdentityRegistration (as claim), join request

**ClaimStatus**: Lifecycle state: `pending` → `accepted` | `rejected`.
Expand All @@ -209,9 +209,7 @@ _Avoid_: Claim, IdentityRegistration (as claim), join request

## Initialization & Claim Flow

**Local mode**: `authsome init` creates an Identity, auto-accepts its claim under the implicit local Principal, and creates the default Vault. No email required.

**Hosted mode**: `authsome init --email manoj@example.com` creates an Identity, creates or finds the Principal by email, and registers an `IdentityClaimRecord` with `claim_status = pending`. A human reviews the claim in the UI and accepts or rejects it. All vault operations return `403` until the claim is accepted.
There is a single flow for every deployment — no deployment mode (see ADR 0007). `authsome init` creates an Identity and registers it; the daemon returns `registration_status = "claim_required"` with a browser **claim URL**. The user opens the URL and registers (or logs in) with **email + password**: the first Principal created on a server becomes `admin`, all subsequent Principals are `user`. The authenticated Principal then confirms the claim, which binds the Identity to the Principal and creates the Principal's default Vault. Until the claim is `accepted`, all vault operations return `403`. The CLI opens the claim URL automatically and polls for completion (and prints the URL to stderr for headless use).

---

Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0006-principal-roles-admin-user.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Principals need an authorization tier to gate deployment-level operations (audit

**Separate `principal_roles` table** — considered for future extensibility (multiple roles per principal). Rejected as premature: the role model is binary and a join adds complexity without benefit today.

**Default admin account created at server init** — would require deciding on credentials before any real user exists. Rejected in favour of first-user-becomes-admin, which is zero-config and correct for both local and hosted deployments.
**Default admin account created at server init** — would require deciding on credentials before any real user exists. Rejected in favour of first-user-becomes-admin: the first principal to register is admin, all subsequent principals are users. (Originally justified as "zero-config" for an implicit local principal; that implicit-principal path was later removed when local and hosted were unified into a single registration + claim flow — see ADR 0007. First-principal-is-admin remains the rule, now reached the same way in every deployment.)

## Consequences

Expand Down
36 changes: 36 additions & 0 deletions docs/adr/0007-unified-deployment-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Unified deployment flow: one registration + claim path for every deployment

The daemon previously branched on a deployment mode (`AUTHSOME_DEPLOYMENT_MODE`, defaulting to `local`) selected at startup, and ran two parallel implementations of nearly every ownership concept:

- **Local**: every Identity collapsed onto one synthetic Principal (`local@authsome.internal`); no claim was required; the Ed25519 PoP key was the only credential. Implemented by `LocalOwnershipResolver` and `LocalIdentityBootstrapService`.
- **Hosted**: each account was its own Principal authenticated by email+password in the browser; an Identity had to be explicitly claimed and accepted. Implemented by `HostedOwnershipResolver` and `HostedIdentityBootstrapService`.

Two code paths doubled the test surface, scattered `if get_deployment_mode() == "hosted"` branches across `dependencies.py`, `routes/_deps.py`, `routes/ui.py`, `routes/health.py`, and `credential_service.py`, and — most importantly — meant "local and hosted behave the same" was asserted in prose but never enforced in code, so the two paths were free to drift. The synthetic local Principal also had a latent wart: a second local Identity silently inherited the *admin* Principal and its Vault, which the hosted model would never allow.

## Decision

Collapse to a single flow, identical for every deployment. There is no deployment mode.

1. `authsome init` generates the Ed25519 Identity and registers it; the daemon returns `registration_status = "claim_required"` with a browser **claim URL**.
2. The user opens the claim URL and **registers (or logs in) with email + password**. The first Principal created on a server becomes **admin** (ADR 0006); all subsequent Principals are users.
3. The authenticated Principal **confirms the claim**, binding the Identity to the Principal and creating the Principal's default Vault.
4. PoP-authenticated calls are then authorized; `OwnershipResolver.resolve` requires an *accepted* claim.

The one irreducible difference between a single-machine and a networked deployment — *who authenticates the claiming Principal* — is resolved by always requiring the email+password browser step. Local is no longer special-cased; the local user is simply the first to sign up and therefore the admin.

The CLI (`AuthsomeApiClient.ensure_identity_ready`) was already mode-agnostic: it reacts to `claim_required` by opening the browser and polling, so it required no behavioural change. It now prints the claim URL to stderr so headless users can complete the step manually.

This supersedes the local/hosted split described in ADR 0003 (§ "local mode … hosted mode") and the "zero-config local" rationale in ADR 0006.

## Considered alternatives

**Single resolver with an injected claim-acceptance policy** (local auto-accepts, hosted requires a human). Keeps one code path while preserving local's zero-config experience. Rejected by explicit product decision: we want local and hosted to be byte-for-byte identical, with no second path or policy seam to maintain.

**Keep two resolvers, only harden with tests + docs.** Smallest change, but leaves the drift risk and duplicate test surface in place. Rejected.

## Consequences

- `AUTHSOME_DEPLOYMENT_MODE`, `get_deployment_mode()`, `LOCAL_PRINCIPAL_EMAIL`, the `Local*`/`Hosted*` resolver and bootstrap classes, and the `AuthService(deployment_mode=...)` parameter are all removed. `OwnershipResolver` and `IdentityBootstrapService` are single concrete classes.
- Admin enforcement is purely role-based: `_ensure_admin_operation_allowed` / `_ensure_provider_client_mutation_allowed` raise for any non-admin Principal, in every deployment (previously non-admins were implicitly allowed in local mode because the sole local Principal was always admin).
- The server-rendered UI always requires a hosted browser session; the local filesystem-identity UI path is gone. `HealthResponse.mode` is removed.
- **Breaking change.** Existing local installs have an Identity registered with no accepted claim and data under the `local@authsome.internal` Principal/Vault. After upgrading, those Identities are rejected until the user registers a Principal (email+password) and claims the Identity. A migration that rebinds the existing local Vault to a freshly registered Principal is possible but is intentionally out of scope here.
Loading
Loading