#117: Open Cloud Mesh federation — bidirectional, allow-list gated, beside Cairn Peering #448

Merged
Cordy merged 38 commits from ocm-117 into main 2026-09-07 23:20:14 +00:00
Owner

Implements #117 per OCM.md (approved design + mockup rev 3), TDD throughout with witnessed reds on the runner.

Backend (internal/ocm, stdlib crypto only): four state registries (allow-list with strict-default per-server policy + stamped relaxation, user-owned contacts + single-use 7-day invites, share ledgers both directions; Remove ends every share both ways), stable RSA identity published as jwks + legacy PEM, dual-dialect discovery served and parsed (IETF 1.1, NC 1.0-proposal1, oC10 shareTypes quirk + trailing slash), RFC 9421 and draft-cavage signatures (digest before signature, hop-crossing target canonicalisation caught by tests), the full inbound pipeline with refusals that name the reason, dialect-adaptive outbound send, RFC 6749 token-exchange client, invites both roles, namespace-agnostic ListRemote, and the bearer-jailed /dav/ocm/<providerId>/ grant surface (subtree jail, write only if granted, dies with the share) over the unscoped driver — audit + on-the-fly decryption apply as for share links.

API: session-gated user endpoints (shares list/send/accept/decline/end, live browse + fetch proxy, invites, contacts) and admin endpoints (status, servers check/allow/policy/remove with best-effort SHARE_UNSHARED); ten new audit verbs in group ocm — the #433 scanner caught this PR's own non-literal carrier during development.

Frontend (mockup rev 3): Federation rail section (Cairn Peering + Open Cloud Mesh), OCM admin topic (Settings with honest status card; Servers with check-before-allow preview + policy chips + consequence-naming remove), Federated user view (Shares = one folder per federation with live remote browsing and explicit Save-a-copy; Requests with badge/dot; user-owned Contacts), Share-dialog federated mode, boot feature probe, i18n ×4.

Docs & verification: docs/handbook/federation-ocm.md, README bullet, ARCHITECTURE entry, CHANGELOG v0.6.139; ocm-interop.yml (kept) boots the real binary and drives the wire black-box — to be run against main before tagging; ocm-live-nc.yml (kept, secrets-gated) for the lab-Nextcloud test.

Everything off by default: while ocmEnabled is false, no OCM endpoint answers at all.

Implements #117 per OCM.md (approved design + mockup rev 3), TDD throughout with witnessed reds on the runner. **Backend (`internal/ocm`, stdlib crypto only):** four state registries (allow-list with strict-default per-server policy + stamped relaxation, user-owned contacts + single-use 7-day invites, share ledgers both directions; `Remove` ends every share both ways), stable RSA identity published as jwks + legacy PEM, dual-dialect discovery served and parsed (IETF 1.1, NC `1.0-proposal1`, oC10 shareTypes quirk + trailing slash), RFC 9421 **and** draft-cavage signatures (digest before signature, hop-crossing target canonicalisation caught by tests), the full inbound pipeline with refusals that name the reason, dialect-adaptive outbound send, RFC 6749 token-exchange client, invites both roles, namespace-agnostic `ListRemote`, and the bearer-jailed `/dav/ocm/<providerId>/` grant surface (subtree jail, write only if granted, dies with the share) over the unscoped driver — audit + on-the-fly decryption apply as for share links. **API:** session-gated user endpoints (shares list/send/accept/decline/end, live browse + fetch proxy, invites, contacts) and admin endpoints (status, servers check/allow/policy/remove with best-effort SHARE_UNSHARED); ten new audit verbs in group `ocm` — the #433 scanner caught this PR's own non-literal carrier during development. **Frontend (mockup rev 3):** Federation rail section (Cairn Peering + Open Cloud Mesh), OCM admin topic (Settings with honest status card; Servers with check-before-allow preview + policy chips + consequence-naming remove), Federated user view (Shares = one folder per federation with live remote browsing and explicit Save-a-copy; Requests with badge/dot; user-owned Contacts), Share-dialog federated mode, boot feature probe, i18n ×4. **Docs & verification:** `docs/handbook/federation-ocm.md`, README bullet, ARCHITECTURE entry, CHANGELOG v0.6.139; `ocm-interop.yml` (kept) boots the real binary and drives the wire black-box — to be run against main before tagging; `ocm-live-nc.yml` (kept, secrets-gated) for the lab-Nextcloud test. Everything off by default: while `ocmEnabled` is false, no OCM endpoint answers at all.
Four registries behind one Store in the peering idiom (atomic JSON via statestore paths): servers (strict PolicyContacts default, relaxation stamped who/when, Remove ends every share both directions and reports them - never a no-op), user-owned contacts with wire-address HasContact, single-use 7-day invites with prune-on-take, and the in/out share ledgers with status transitions and secret/providerId lookups. Witnessed compile-red first; Discovery struct forward-declared with final fields for Task 2 to replace.
EnsureKey: stable persisted RSA-2048, published as RFC 7517 jwks and legacy PKIX PEM. ParseDiscovery normalises IETF 1.1, Nextcloud 1.0-proposal1 (legacy publicKey captured), and ownCloud 10's shareTypes-as-resourceTypes quirk into one Discovery, with errors that name what is wrong (they surface in the admin Check preview). OurDiscovery/OurLegacyDiscovery serve Cairn's own documents from one source and round-trip through our own parser (proven in tests); the invites capability appears only when enabled. Witnessed red first.
Sign/Verify on stdlib crypto only: 9421 signature base over @method/@target-uri/content-digest/date with rsa-v1_5-sha256 and fqdn#key1 keyids; SignLegacy produces the draft-cavage form ((request-target) content-length date digest) pre-9421 Nextcloud/ownCloud verify. Verify auto-detects the dialect, checks the digest before the signature, and returns errors that name exactly what failed - they become refusal bodies. KeyFromJWKS/KeyFromPEM load peer keys from either discovery style and are proven to agree on our own key. Witnessed red first.
The 9421 base built @target-uri from req.URL.String(): absolute when the client signs, path-only once the server re-parses - every signature died crossing a real HTTP hop (caught by the inbound endpoint tests, invisible to the same-object unit round-trip). Both sides now use req.URL.RequestURI(); per-peer full-URI conventions get pinned in the interop pass.
ocm.Service mounts the public endpoints (well-known + legacy discovery incl. the oC10 trailing-slash form, jwks, shares, notifications), every route 404ing while settings.ocmEnabled is off. The shares pipeline enforces allow-list -> signature (either dialect; unsigned tolerated only when the peer publishes no key) -> per-server policy -> known recipient, each refusal naming its reason in the body. Both protocol dialects normalise into the ShareIn ledger (legacy options form defaults to read-only). Notifications flip ledger states, identified by the origin the signature claims and then verifies. main wires the four ocm state files into the backend statestore binds. Witnessed red first.
Client.Discover walks well-known -> /ocm-provider -> trailing-slash (the ownCloud proxy quirk) and normalises whatever answers. SendShare emits the dialect the peer speaks: protocol.options for legacy 1.0 (signed draft-cavage), protocol.webdav with uri+secret+permissions for 1.1+ (signed RFC 9421). FetchRemote pulls remote bytes with the share secret, running the RFC 6749 code-flow exchange first when the share requires it. GrantHandler serves /dav/ocm/<providerId>/: constant-time bearer check, subtree jail, GET/HEAD/PROPFIND always, PUT only on write shares, 401 the moment the share ends - reads flow through the live driver stack so audit and on-the-fly decryption apply as for share links. Witnessed red first.
EncodeInvite/DecodeInvite implement the spec's canonical base64 token@provider format (split on the last @, tolerating @ inside tokens). The inviter-side /ocm/invite-accepted gates on the allow-list BEFORE consuming the token (a refusal must not burn it), verifies the signature, consumes the single-use invite, records the inviter-side contact and answers with the inviter's identity. Client.AcceptInvite is the receiver role: decode, allow-list refusal that names the missing server so the user knows what to ask their admin, discover, signed acceptance, contact stored from the response. Witnessed red first.
ListRemote drives PROPFIND with namespace-agnostic multistatus parsing, proven against our own grant surface (the Cairn-to-Cairn CI floor in miniature). internal/api/ocm.go: session-gated user endpoints (shares list/send/accept/decline/end both directions, live browse + fetch proxy, invites create/list/accept, contacts) and admin endpoints (status, servers list/check/allow/policy/remove with best-effort SHARE_UNSHARED on removal), every allow-list refusal naming the server and pointing at the admin; secrets never leave the API. Ten new audit verbs under the ocm group; the #433 scanner caught this commit's own non-literal carrier on the first run (auditVerb variable, now exempted at its literal source) - the tripwire pays for itself. main wires apiH.OCM and mounts the enabled-gated /dav/ocm/ grant over the unscoped driver. v1 scope line: OCM-sharing personal paths only; spaces ride a later milestone. Witnessed red first (ListRemote).
The admin rail splits per approved mockup rev 3: 'Access, sharing & federation' slims to 'Access & sharing' and a new Federation group holds Cairn Peering (relabelled) and Open Cloud Mesh. The OCM topic has Settings (enable toggle transplanted from the settings form via SET_SLOTS, with the no-open-federation amber note, plus the live endpoint/jwks/compat/address status card that honestly says 'off' while disabled) and Servers (Check-before-allow with a product/dialect/invites preview, the table with per-server policy chip cycling contacts<->any, and Remove with a consequence-naming confirm). i18n x4.
The Federated view lands per approved mockup rev 3: Shares holds only established relationships, one root folder per federation with counts, opening one shows Shared-with-you (live Browse through Cairn as WebDAV client, Download, End) and You-shared groups; Requests takes everything transactional (accept/decline cards, sent-awaiting with Withdraw, ended-by-owner notices) and feeds the sidebar dot + tab badge; Contacts is user-owned end to end (shown-once invite with execCommand copy for the insecure-origin dogfood, paste-accept whose errors name a non-allowed server, contact list). Remote browsing drills folders with breadcrumb-back, per-file Download and the explicit Save-a-copy-to-Personal (fetch through our proxy, then the normal upload path - the copy is encrypted at rest like any upload). The Share dialog gains the federated-address mode when the boot probe detects OCM. i18n x4.
#117: docs (handbook page + README + ARCHITECTURE + CHANGELOG v0.6.139) and interop workflows
Some checks failed
ocm-interop.yml / #117: docs (handbook page + README + ARCHITECTURE + CHANGELOG v0.6.139) and interop workflows (pull_request) Failing after 0s
ocm-live-nc.yml / #117: docs (handbook page + README + ARCHITECTURE + CHANGELOG v0.6.139) and interop workflows (pull_request) Failing after 0s
ci / test-and-build (pull_request) Successful in 48s
4fe877493b
federation-ocm.md carries the operator and user guide incl. the peer-product matrix and encryption boundary; README gains the OCM feature bullet; ARCHITECTURE records the design decisions in one bullet next to peering and marking. ocm-interop.yml is a KEPT black-box harness: builds the real binary, seeds state (allow-list, outbound grant), and drives the wire with curl - discovery in both dialects, jwks, an unsigned-tolerated inbound share from a keyless peer, a refusal that names the unlisted server, the bearer-jailed grant surface, and the everything-404s-while-disabled guarantee. ocm-live-nc.yml is the manual lab-Nextcloud test, gated on operator-provided secrets.
#117 fix: interop workflow written directly (heredoc emission expanded the job token and broke YAML)
Some checks failed
ocm-live-nc.yml / #117 fix: interop workflow written directly (heredoc emission expanded the job token and broke YAML) (push) Failing after 0s
ocm-live-nc.yml / #117 fix: interop workflow written directly (heredoc emission expanded the job token and broke YAML) (pull_request) Failing after 0s
ci / test-and-build (pull_request) Successful in 46s
489e61fe06
#117 fix: live-NC workflow written directly (same emission bug)
All checks were successful
ci / test-and-build (pull_request) Successful in 49s
25af7717a4
Cordy merged commit 7fa8db068a into main 2026-09-07 23:20:14 +00:00
Cordy deleted branch ocm-117 2026-09-07 23:20:17 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#448
No description provided.