Skip to content

Implement CSPP+ "Plus" coauth REST endpoints (RefreshCoauthLock, GetCoauthTable) #454

Description

@petrsvihlik

Background

The current PR #438 ships a one-line honesty fix for the core-WOPI SupportsCoauth boolean in 6a33dea: when ICobaltProcessor is registered, the host advertises SupportsCoauth = true because the MS-FSSHTTP / Cobalt protocol genuinely delivers multi-user editing. That covers the core CSPP CheckFileInfo property (docs) — informational only, no specific REST endpoint required.

This issue tracks the CSPP+ "Plus" tier coauth REST extension, which is a separate Microsoft-specific feature living at /microsoft-365/cloud-storage-partner-program/plus/coauthoring/*. The first endpoint is RefreshCoauthLock, which together with GetCoauthTable and an explicit coauth-lock creation operation forms the full Plus surface.

Scope

New endpoints (X-WOPI-Override dispatched on POST /wopi/files/{id}):

  • REFRESH_COAUTH_LOCK — refresh existing coauth lock by (file_id, CoauthLockId). Headers: X-WOPI-CoauthLockId (1024 ASCII chars), X-WOPI-CoauthLockExpirationTimeout (1-3600s), X-WOPI-CoauthLockMetadata (optional, 4KB). Response: X-WOPI-CoauthTableVersion header + JSON CoauthTable.
  • GET_COAUTH_TABLE — return current CoauthTable for the file.
  • A "join/add coauth lock" operation — the spec says `RefreshCoauthLock` only works if an unexpired lock already exists, so there must be a separate primary call to create one.

Status codes: 200 (success), 400 (bad headers), 401 (invalid token), 404 (file not found or no edit perm), 409 (lock doesn't exist), 500, 501.

Why the existing infrastructure isn't a drop-in fit

Requirement IWopiLockProvider CoauthoringSessionTracker
Many locks per file ❌ single-lock-per-file ✅ keyed by userId
1024-char lock id (per client, not per user) ✅ string id ❌ keys by userId
Variable expiry 1–60 min (client-specified) ❌ fixed 30 min ❌ fixed 30 min
4KB metadata blob per lock ❌ no field ❌ no field
Lock type (`Coauth` vs `CoauthExclusive`)
Versioned `CoauthTable`
Survives multi-instance deployment ✅ via Azure/Redis providers ❌ static state, single-process

Specifically:

  • `IWopiLockProvider` is one-lock-per-file by contract. Repurposing it for multi-entry-per-file would either change the storage key shape (`fileId+coauthLockId`) — which breaks the existing Memory/Azure/Redis providers — or conflate coauth locks with regular WOPI locks, a correctness hazard.
  • `CoauthoringSessionTracker` keys by `userId`, not by client. The CSPP+ spec wants one lock per client (one user with two tabs = two coauth locks). And it's single-process today because Cobalt sessions are sticky to a WopiHost instance; CSPP+ REST is host-wide and needs distributed storage.

Proposed approach

Mirror the existing `IWopiLockProvider` package structure — don't reuse the types themselves:

```
WopiHost.Abstractions/
IWopiCoauthLockProvider ← new interface
CoauthLockInfo(LockId, UserId, Expires, ← new record
Metadata?, LockType)

WopiHost.MemoryCoauthLockProvider/ ← new (~120 LoC, parallels MemoryLockProvider)
WopiHost.AzureCoauthLockProvider/ ← new (~250 LoC, parallels AzureLockProvider)
WopiHost.RedisCoauthLockProvider/ ← new (~200 LoC, parallels RedisLockProvider;
Lua script for atomic table-version bump)

WopiHost.Abstractions.Testing/
CoauthLockProviderConformanceTests ← new conformance suite,
parallel to LockProviderConformanceTests
```

Then wire `CoauthoringSessionTracker` to read from the same store so Cobalt-side coauth status (`IsAlone`, `GetActiveEditorCount`, `GetEditorsTable`) reflects CSPP+ REST locks too. One logical "who's editing this file right now" state, two surface APIs (Cobalt protocol + CSPP+ REST).

Estimated scope

Day Work
1 `IWopiCoauthLockProvider` abstraction + `MemoryCoauthLockProvider` + conformance tests; `RefreshCoauthLock` endpoint with full state-machine tests
2 `GetCoauthTable` endpoint + initial coauth-lock-add operation; `CoauthTable` versioning state machine; wire `CoauthoringSessionTracker` to read the new store
3 `AzureCoauthLockProvider` + `RedisCoauthLockProvider` + their conformance subclasses + integration tests

After landing, `SupportsCoauth = cobaltProcessor is not null && coauthLockProvider is not null` would be honest in both the legacy ("we support multi-user via Cobalt") and CSPP+ ("we implement the REST coauth surface") senses.

Acceptance

  • `IWopiCoauthLockProvider` + `CoauthLockInfo` in Abstractions
  • `MemoryCoauthLockProvider` + `AzureCoauthLockProvider` + `RedisCoauthLockProvider` packages
  • `CoauthLockProviderConformanceTests` in Abstractions.Testing; each provider has a sealed conformance subclass
  • `REFRESH_COAUTH_LOCK` endpoint with state-machine tests (200 happy path, 400 bad-headers paths, 409 missing-lock, 501 fallback)
  • `GET_COAUTH_TABLE` endpoint
  • Initial coauth-lock-add operation (whatever name the spec assigns)
  • `CoauthoringSessionTracker` reads from `IWopiCoauthLockProvider` so Cobalt + REST share state
  • `SupportsCoauth` derivation updated to require both Cobalt processor and coauth lock provider
  • Microsoft WOPI validator passes the CSPP+ coauth conformance suite (if available)

References

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions