OCM (Open Cloud Mesh) compatibility adapter — interop with Nextcloud/ownCloud #117
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/Cairn#117
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Decided 2026-08-05 (Q7 on #100): revisit and implement in v0.6, beside — not instead of — Cairn's own peering.
Why. Open Cloud Mesh is the incumbent federation protocol: in the field since ~2016, implemented by Nextcloud, ownCloud, OpenCloud, CERNBox and Seafile, and on the IETF standards track since March 2026 (
draft-ietf-ocm-open-cloud-mesh-04). Speaking it turns "migrate off Nextcloud" into "put Cairn next to your Nextcloud and share across" — the single strongest migration story available to us, and a procurement checkbox in exactly the federated-research and public-sector markets that already deployed OCM.What it is not. OCM does not replace our peering (
IMPLEMENTED-PEERING.md). They answer different questions:Our positioning is the narrow trust model; OCM is the interop bridge. Both can exist — nothing in the peering design forecloses it, because OCM would sit beside the transfer plane rather than inside it.
Scope to settle at implementation time:
user@server). We have deliberately avoided identity federation. Does an OCM share map onto a local user, an Inbox drop, or a distinct "external shares" surface?Prerequisite: v0.5 peering shipped, so this is a bridge from something rather than the first federation implementation.
Triage: tandem (large; needs a real Nextcloud/ownCloud). OCM (Open Cloud Mesh) adapter for cross-product share/interop. Big surface; design + scope first, and interop testing needs a live Nextcloud/ownCloud peer. Post-launch tier — park until the closed beta settles.
Design approved 2026-09-07 and recorded in OCM.md (root, playing the role PEERING.md played for peering). Mockup rev 2 approved (
ocm-federation-mockup.html). Summary of the settled scope questions from this issue:draft-ietf-ocm-open-cloud-mesh-04, with deliberate dialect compat for what's deployed: legacy/ocm-provider+1.0-proposal1+publicKeyfor vanilla Nextcloud/ownCloud 10,/.well-known/ocm1.1 for OpenCloud/CERNBox. No/status.phpshim — that out-of-spec probe only matters for NC "trusted servers" directory sync, which is not our model.internal/ocm(Reva rejected as dependency; kept as dialect documentation), cs3org OCM Test Suite pattern as CI interop harness + live tests against the lab Nextcloud.ocm(registry + scanner per #433).Interop acceptance matrix and build phasing are in OCM.md §6/§10. Next: implementation plan, then the phased TDD build; ARCHITECTURE/handbook/wiki flowchart land with the ship per §10.7.
Shipped in v0.6.139 (PR #448), live on both dogfoods (
cairn_build_infoverified).Decision record (full design: OCM.md; guide: docs/handbook/federation-ocm.md; flowchart: wiki OCM-Federation)
Every scope question from this issue, settled and built:
draft-ietf-ocm-open-cloud-mesh-04target with deliberate dialect compat for what's deployed: we serve both/.well-known/ocm(1.1) and legacy/ocm-provider(1.0-proposal1 withpublicKey), and we parse all three field dialects including ownCloud 10's shareTypes-as-resourceTypes quirk and trailing-slash discovery. Signatures both ways: RFC 9421 and draft-cavage. No/status.phpshim (NC "trusted servers" directory sync is not federation).internal/ocm, stdlib crypto only; Reva rejected as dependency, kept as dialect documentation. Ten audit verbs in groupocm(the #433 scanner caught this feature's own non-literal carrier during development).Verification: TDD with witnessed reds throughout;
ocm-interop.yml(kept) black-boxes the real binary — discovery both dialects, jwks, inbound share, refusal-names-the-server, bearer-jailed grant, disabled-is-silent — ALL GREEN on main before tagging.Open follow-ups (deliberate v1 scope lines, filed here for the beta):
/spaces/paths (v1 shares Personal only).ocm-live-nc.ymlis ready but needs operator setup — enable OCM on the dogfood, allownextcloud.c0rdyceps.ch(policy "any user"), add Forgejo secretsNC_URL/NC_USER/NC_APP_PW— then run it watching the Federated view.UserExiststightening + per-peer full-URI signing conventions, both riding the interop findings.Archive: the implementation plan this feature was built from (written 2026-09-07 against OCM.md + mockup rev 3, executed 2026-09-07/08; kept here so the plan, the spec and the decision record live in one place).
OCM Federation Implementation Plan (#117)
Goal: Bidirectional Open Cloud Mesh federation (Nextcloud/ownCloud/OpenCloud/Seafile/CERNBox interop) beside Cairn Peering, per OCM.md.
Architecture: New
internal/ocmpackage following theinternal/peeringregistry idiom (atomic JSON throughstatestore.Read/Write, mutex-guarded, fail-closed). Three signed inbound JSON endpoints + two discovery documents + a jwks endpoint; outbound = dialect-detecting discovery client + signed share POSTs; bytes move over WebDAV (inbound: Cairn as WebDAV client proxying to the user; outbound: a bearer-scoped/dav/ocm/<providerId>/grant surface). UI per approved mockups rev 3.Tech Stack: Go stdlib only (crypto/rsa, crypto/sha256, net/http); no new dependencies. Frontend: vanilla JS in
web/static/index.htmlhouse style. CI: Forgejo one-shot workflows on the runner (house TDD: witnessed red → splice → green).Global Constraints
settings.OCMEnabledfalse ⇒ discovery/jwks/ocm endpoints return 404 and no outbound calls occur.contacts(strict).ocm-117; every task = red test pushed via MCP (witnessed failing on runner) → one-shot splice workflow → green full suite.ocmin the same task that first emits them (scanner tripwire enforces this).Tasks (each: red test → witnessed red → implement → green full suite → commit)
internal/ocm/state.go:Server(allow-list entry: fqdn, addedBy/At,Policycontacts|any, relaxedBy/At stamp, cachedDisco), per-userContact+ single-use 7-dayInvite,ShareIn/ShareOutledgers;Open(4 paths),Allow/Get/List/SetPolicy/Remove(Remove ends every share both ways and returns them), contacts CRUD +HasContact(user, addr),MintInvite/TakeInvite(prune on take), share status transitions + secret/providerId lookups. Persisted viastatestore.Read/Write, peering idiom.EnsureKey(stable RSA-2048, persisted),JWKS(),PublicPEM();ParseDiscoverynormalising IETF 1.1 / NC 1.0-proposal1 (legacy publicKey captured) / oC10 shareTypes-as-resourceTypes quirk;OurDiscovery(1.1) +OurLegacyDiscovery(1.0 shape), round-tripped through our own parser in tests. Fixtures verbatim from field research.Sign(RFC 9421 over @method/@target-uri/content-digest/date, rsa-v1_5-sha256, fqdn#key1),SignLegacy(draft-cavage: (request-target) content-length date digest),Verifyauto-detecting with errors naming exactly what failed;KeyFromJWKS/KeyFromPEM. (Execution finding: sign the request-target, not the absolute URL — the base must survive the client→server hop.)ocm.Servicewith injected closures (Enabled,UserExists,PeerKey): both discovery routes (+ oC10 trailing slash), jwks,POST /ocm/sharespipeline (allow-list → signature, unsigned tolerated only for keyless peers → per-server policy → known recipient → ledger, refusals naming the reason),POST /ocm/notifications(ACCEPTED/DECLINED/UNSHARED).settings.OCMEnabled; main wires the four state files into the statestore binds; every route 404s while disabled.Client.Discover(well-known → /ocm-provider → trailing slash),SendSharedialect-adaptive (legacy options vs protocol.webdav),SendNotification,FetchRemote(+ RFC 6749 code-flow client for must-exchange-token),ListRemote(namespace-agnostic multistatus, proven against our own grant);GrantHandlerfor/dav/ocm/<pid>/— constant-time bearer, subtree jail, GET/HEAD/PROPFIND always, PUT only on write shares, dead when the share ends; mounted over the unscoped driver (audit + #19 decryption apply).EncodeInvite/DecodeInvite(canonical base64 token@provider, last-@ split), inviter-side/ocm/invite-accepted(allow-list before token burn),Client.AcceptInvite; user API (shares list/send/accept/decline/end both directions, browse/fetch proxy, invites, contacts) + admin API (status, servers list/check/allow/policy/remove); ten audit verbs groupocm.ocm-interop.yml(kept) builds the real binary, seeds state, drives the wire with curl (discovery both dialects, jwks, inbound share, named refusal, grant jail, disabled-is-silent);ocm-live-nc.yml(kept, secrets-gated) drives the lab Nextcloud via OCS.federation-ocm.md, README bullet, ARCHITECTURE entry, CHANGELOG; wiki page with the end-to-end Mermaid flowchart; PR → CI → merge → interop green on main → tag → bump dogfoods → verify → close with decision record.Self-review notes
Store,Discovery,ShareIn/Out,Policyused identically across tasks;Verifyreturns(string, error)everywhere.(The full step-level plan with verbatim test code lives in the session file
2026-09-07-ocm-federation-plan.md; deltas between plan and execution are recorded in the closing decision-record comment above.)Dogfood finding 4 → fixed in v0.6.142 (PR #453, live on both dogfoods).
Finding: the Servers-tab policy chip toggled on a single click — accident-prone and not identifiable as a setting — and the mockup's Status column was absent (reachability had been deferred to the metrics pass, follow-up 4, without saying so in the UI).
Design (approved mockup
ocm-servers-policy-mockup.html, rev 1):POST /api/v1/admin/ocm/servers/check→ reachable/unreachable + "checked just now". Retention: nothing is stored and nothing is audited — the result lives only in the response/page memory, so no logs accumulate. The continuous reachability gauge + Prometheus alert remain deferred to the metrics pass (follow-up 4 stands for those). If "unreachable since N days" is ever wanted, that is one fixed-size timestamp per server in the servers doc, not a log.Cleanup:
.ocmchip/.ocmchip.strictand the click-to-toggle handler removed entirely. 12 new i18n keys ×4.