mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 18:13:26 +00:00
2945ee27dd
* feat(server): add WebSocket handler at /api/v1/collab/{itemID} (TASK-1254)
WebSocket entry point for Yjs-based collaborative editing on a
single item under PLAN-1248. Bare-bones in this PR by design:
upgrade + log connect/disconnect + drain reads. Protocol logic
(forwarding to OpBus, persisting to op-log, awareness fan-out)
arrives in TASK-1255 (room manager).
Authorisation mirrors RequireWorkspaceAccess but keyed on the
item's workspace ID rather than a {slug} URL param — the WS URL
only carries itemID. Implementation re-uses the same access
ladder:
fresh-install escape hatch (no users)
→ grant
legacy workspace-scoped API token, no user
→ grant if token's workspace matches the item's workspace
OAuth token allow-list (TASK-953)
→ reject when workspace not on consented list
authenticated user
→ admin OR member OR has guest grants
User is re-fetched from the store on each upgrade (not trusted
from session-context cache) so a mid-session admin demotion or
member removal closes the upgrade path immediately. Mirrors
sseSubscriberStillHasAccess. Periodic per-connection
revalidation lives in TASK-1256.
Route registered alongside SSE (outside the jsonContentType
middleware group, but inside the auth middleware chain). Promotes
github.com/gorilla/websocket from indirect to direct dep and
bumps to v1.5.3 (latest stable; v1.5.0 was already in
go.mod transitively via another package).
Tests cover:
- fresh-install escape hatch grants the upgrade
- bootstrapped server rejects unauthenticated upgrade with 401
- non-member with valid session is rejected with 403
(NOT 401 — confirms the access path runs after auth, not before)
- unknown item surfaces as 404 (not 401/403 leak)
- empty itemID segment doesn't match the route
Test infrastructure note: dialCollab takes an explicit User-Agent
because pad's session-binding middleware hashes the UA at
CreateSession time and re-checks on every request — the dialer
must match what was stored, otherwise the cookie is rejected
before the workspace check fires (and we'd see a misleading 401
where 403 was expected).
Parent: PLAN-1248. Phase 1 — Backend foundation.
* style: gofmt handlers_collab_test.go per Codex review (round 1)
* fix(server): SetReadLimit + nginx upgrade headers for collab WS per Codex review (round 2)
P-MEDIUM #1: handleCollab.ReadMessage had no per-message size cap, so an
authenticated client could send an arbitrarily large frame and force
unbounded server-side buffering — the HTTP body limit applied by the
auth chain doesn't apply once the connection is upgraded. Set
SetReadLimit(1 MiB), generous for keystroke-rate Yjs ops and large
enough for a typical initial-sync state. ReadMessage returns an error
when exceeded, which the existing read loop handles as a normal close.
P-MEDIUM #2: deploy/nginx.conf routed /api/v1/collab/ through the
default `location /` block, which sets `Connection ""` (cleared so HTTP
keepalive works) — that strips the Upgrade header, so WebSocket
upgrades silently fail behind the documented nginx deployment. Add a
dedicated location block with proxy_set_header Upgrade $http_upgrade /
Connection "upgrade", same 24h read/send timeouts as SSE so an idle
editor tab does not get cut off mid-session.
* fix(server): enforce per-item visibility in collab WS upgrade per Codex review (round 3)
P2: authorizeCollabAccess granted upgrade to any workspace member or
guest-with-grants without checking whether THIS specific item was
visible to that user. A restricted member (collection_access=specific)
or a guest with grants on item A could upgrade /api/v1/collab/{itemID}
for an item B in a different collection — they'd see live edits to a
document they have no right to read.
Restructure the access ladder:
1. Workspace-level gate stays as-is: "any access at all?" If no
membership AND no grants → 403 (unchanged).
2. Item-level visibility check added on top, mirroring requireItemVisible
without depending on middleware-set request context (the WS path
doesn't go through RequireWorkspaceAccess):
- VisibleCollectionIDs nil → "all" access → grant.
- Item's collection in the visible set → grant.
- Item-level grant on this exact item → grant (covers guests
given access to a single item rather than a whole collection).
- Else → 404, mirroring requireItemVisible's "don't leak
existence" pattern.
Admin path returns nil before this check, so no change there.
Legacy workspace-scoped API tokens grant editor-equivalent access
on workspace match (predates the grants design); that branch is
untouched since legacy tokens don't have a user identity to scope
per-item grants against.
Test added: TestCollabUpgradeRejectsRestrictedMemberForeignCollection
— member with specific access to collA tries to upgrade for an item
in collB → 404. Existing 5 tests still pass.
* fix(server): strict per-item visibility check + sibling-grant test per Codex review (round 4)
P1 (round 4): VisibleCollectionIDs is broader than full-collection
access — it includes collections "anchored" by an item-level grant
(so the nav can still surface the parent collection of a granted
item). Round 3's check treated every visible collection as full
access; a guest with grant `item:A` could upgrade
/api/v1/collab/{B} for a sibling B in the same collection.
Tighten by mirroring guestResourceFilter / requireItemVisible:
1. Coarse stage stays — collection must be in the visible set.
2. NEW strict stage when the user has item-level grants:
(a) full collection grant on this collection → grant
(b) member's "specific" access list including this collection
→ grant
(c) item grant on THIS exact item → grant
Else → 404 (the visible-set hit was anchored by a sibling's
grant, not by full collection access).
When the user has NO item grants, the coarse-only check is
sufficient — visibility came from full collection access (member's
"specific" list, full collection grant, or "all" access).
Test added: TestCollabUpgradeRejectsGuestWithSiblingItemGrantOnly
— guest with item:A grant tries to upgrade for sibling B in the
same collection → 404 (the bug being regression-tested) AND verifies
the granted item A still upgrades cleanly to 101 Switching Protocols.
124 lines
5.6 KiB
Modula-2
124 lines
5.6 KiB
Modula-2
module github.com/PerpetualSoftware/pad
|
|
|
|
go 1.26.3
|
|
|
|
require (
|
|
github.com/BurntSushi/toml v1.6.0
|
|
github.com/disintegration/imaging v1.6.2
|
|
github.com/fatih/color v1.19.0
|
|
github.com/go-chi/chi/v5 v5.2.5
|
|
github.com/go-chi/cors v1.2.2
|
|
github.com/google/uuid v1.6.0
|
|
github.com/jackc/pgx/v5 v5.9.2
|
|
github.com/mark3labs/mcp-go v0.52.0
|
|
github.com/ory/fosite v0.49.0
|
|
github.com/pquerna/otp v1.5.0
|
|
github.com/prometheus/client_golang v1.23.2
|
|
github.com/prometheus/client_model v0.6.2
|
|
github.com/redis/go-redis/v9 v9.19.0
|
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
|
|
github.com/sergi/go-diff v1.4.0
|
|
github.com/spf13/cobra v1.10.2
|
|
github.com/spf13/pflag v1.0.10
|
|
github.com/trustelem/zxcvbn v1.0.1
|
|
golang.org/x/crypto v0.50.0
|
|
golang.org/x/image v0.39.0
|
|
golang.org/x/term v0.42.0
|
|
golang.org/x/text v0.36.0
|
|
golang.org/x/time v0.15.0
|
|
modernc.org/sqlite v1.50.0
|
|
)
|
|
|
|
require (
|
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
|
github.com/beorn7/perks v1.0.1 // indirect
|
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
github.com/cristalhq/jwt/v4 v4.0.2 // indirect
|
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
github.com/dgraph-io/ristretto v1.0.0 // indirect
|
|
github.com/dlclark/regexp2 v1.12.0 // indirect
|
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
|
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
|
github.com/go-logr/logr v1.4.3 // indirect
|
|
github.com/go-logr/stdr v1.2.2 // indirect
|
|
github.com/gobuffalo/pop/v6 v6.1.1 // indirect
|
|
github.com/gogo/protobuf v1.3.2 // indirect
|
|
github.com/golang/mock v1.6.0 // indirect
|
|
github.com/golang/protobuf v1.5.3 // indirect
|
|
github.com/google/jsonschema-go v0.4.2 // indirect
|
|
github.com/gorilla/websocket v1.5.3 // indirect
|
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect
|
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
|
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
|
github.com/magiconair/properties v1.8.7 // indirect
|
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
github.com/mattn/goveralls v0.0.12 // indirect
|
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
|
github.com/openzipkin/zipkin-go v0.4.2 // indirect
|
|
github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe // indirect
|
|
github.com/ory/go-convenience v0.1.0 // indirect
|
|
github.com/ory/x v0.0.665 // indirect
|
|
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
|
|
github.com/pkg/errors v0.9.1 // indirect
|
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
github.com/prometheus/common v0.66.1 // indirect
|
|
github.com/prometheus/procfs v0.16.1 // indirect
|
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
|
github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect
|
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
|
github.com/spf13/afero v1.9.5 // indirect
|
|
github.com/spf13/cast v1.7.1 // indirect
|
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
|
github.com/spf13/viper v1.16.0 // indirect
|
|
github.com/stretchr/testify v1.11.1 // indirect
|
|
github.com/subosito/gotenv v1.4.2 // indirect
|
|
github.com/test-go/testify v1.1.4 // indirect
|
|
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
|
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect
|
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
|
|
go.opentelemetry.io/contrib/propagators/b3 v1.21.0 // indirect
|
|
go.opentelemetry.io/contrib/propagators/jaeger v1.21.1 // indirect
|
|
go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1 // indirect
|
|
go.opentelemetry.io/otel v1.40.0 // indirect
|
|
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
|
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
|
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect
|
|
go.opentelemetry.io/otel/exporters/zipkin v1.21.0 // indirect
|
|
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
|
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
|
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
|
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
|
go.uber.org/atomic v1.11.0 // indirect
|
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
|
golang.org/x/mod v0.34.0 // indirect
|
|
golang.org/x/net v0.53.0 // indirect
|
|
golang.org/x/oauth2 v0.30.0 // indirect
|
|
golang.org/x/sync v0.20.0 // indirect
|
|
golang.org/x/sys v0.43.0 // indirect
|
|
golang.org/x/tools v0.43.0 // indirect
|
|
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
|
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
|
google.golang.org/grpc v1.59.0 // indirect
|
|
google.golang.org/protobuf v1.36.8 // indirect
|
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
modernc.org/libc v1.72.0 // indirect
|
|
modernc.org/mathutil v1.7.1 // indirect
|
|
modernc.org/memory v1.11.0 // indirect
|
|
)
|