Skip to content

feat(parity): wire API sessions into access control, fix authz level bug#279

Merged
SamTV12345 merged 1 commit into
mainfrom
feat/session-access
Jun 11, 2026
Merged

feat(parity): wire API sessions into access control, fix authz level bug#279
SamTV12345 merged 1 commit into
mainfrom
feat/session-access

Conversation

@SamTV12345

Copy link
Copy Markdown
Member

Summary

Follow-up to #276 — completes the API-session feature end-to-end and fixes two more porting bugs found along the way:

Session access control (the missing half of the session API)

  • SessionManager.findAuthorID / doesSessionExist were stubs (always nil/false), so SecurityManager.CheckAccess never honoured sessionID cookies. The API-session storage now lives in pad.SessionManager (the lib/api/session endpoints delegate to it, same storage keys), and CheckAccess grants access to private group pads for valid, unexpired sessions of the pad's group. Quoted and comma-separated cookie values are handled like the original (upstream #3819).

Porting bugs fixed

  • NormalizeAuthzLevel denied readOnly and modify: the JS switch fallthrough was ported as empty Go cases, so only create passed — users with readOnly/modify pad authorizations were rejected. true now also normalizes to "create" like the original.
  • UserCanModify panicked on a nil PadAuthorizations map (panic("This should not happen")) — now denies instead of crashing the request.

New endpoints (original API parity)

  • GET /admin/api/stats{totalPads, totalSessions, totalActivePads} (original getStats)
  • POST /admin/api/pads/{padId}/sendClientsMessage → broadcasts a custom COLLABROOM message type to all pad clients (original sendClientsMessage / handleCustomMessage)

Also replaces the misleading //FIXME this needs to be set in webaccess.go with documentation of the not-yet-ported preAuthorize plugin hook.

Test plan

  • go test -p 1 ./... — all 27 packages green (Memory/SQLite/Postgres/MySQL), go vet ./... clean
  • New tests: FindAuthorID (valid/quoted multi-cookie/expired/wrong group), CheckAccess e2e on a private group pad with and without session, NormalizeAuthzLevel regression, UserCanModify nil-map regression, getStats endpoint, sendClientsMessage (WS broadcast + API validation)

🤖 Generated with Claude Code

Completes the session feature end-to-end and closes more gaps vs the
original etherpad:

- SessionManager: implement the findAuthorID/doesSessionExist stubs.
  API-session storage (create/get/delete/list) now lives here; the
  lib/api/session endpoints delegate to it. SecurityManager.CheckAccess
  therefore honours sessionID cookies: private group pads are reachable
  with a valid, unexpired session of the pad's group (quoted and
  comma-separated cookies handled like upstream #3819).
- webaccess: NormalizeAuthzLevel dropped "readOnly" and "modify" through
  the switch (missing fallthrough in the port) and denied them; `true`
  now normalizes to "create" like the original. UserCanModify no longer
  panics on a nil PadAuthorizations map - it denies.
- new GET /admin/api/stats endpoint (totalPads, totalSessions,
  totalActivePads) mirroring the original getStats API.
- new POST /admin/api/pads/{padId}/sendClientsMessage endpoint backed by
  PadMessageHandler.SendCustomMessageToPad, mirroring the original
  handleCustomMessage broadcast.
- document the not-yet-ported preAuthorize plugin hook in webaccess
  instead of a misleading FIXME.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 11, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0)

Grey Divider


Action required

1. Session bypasses token validation 🐞 Bug ⛨ Security
Description
Once SessionManager.FindAuthorID returns a non-nil author for a valid session cookie,
SecurityManager.CheckAccess skips utils.IsValidAuthorToken() and still calls
AuthorManager.GetAuthorId() with an unvalidated token. Because GetAuthorId() auto-creates an author
for unknown tokens, a valid session can be combined with arbitrary token strings (including empty /
non-"t." tokens) to mint authors and bypass token-format enforcement.
Code

lib/pad/SessionManager.go[R149-159]

+	sessionIDs := strings.Split(cookieQuoteTrimmer.ReplaceAllString(*sessionCookie, ""), ",")
+	now := time.Now().Unix()
+	for _, sessionID := range sessionIDs {
+		info, err := sm.GetSessionInfo(strings.TrimSpace(sessionID))
+		if err != nil || info == nil {
+			continue
+		}
+		if info.GroupID == groupID && now < info.ValidUntil {
+			return &info.AuthorID
+		}
+	}
Evidence
The new FindAuthorID returns a session-bound author ID, which makes CheckAccess skip token-format
validation; then GetAuthorId() can create a new author mapping for any token string that does not
already exist.

/lib/pad/SessionManager.go[140-160]
/lib/pad/SecurityManager.go[108-129]
/lib/author/authorManager.go[36-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
With real session lookups enabled, `SecurityManager.CheckAccess` now conditionally skips `utils.IsValidAuthorToken()` when `sessionAuthorID != nil`. This allows unvalidated token strings to reach `AuthorManager.GetAuthorId()`, which auto-creates authors for unknown tokens.

## Issue Context
This becomes reachable only after this PR because `SessionManager.FindAuthorID()` now returns non-nil values for valid sessions.

## Fix Focus Areas
- lib/pad/SecurityManager.go[108-130]
- lib/author/authorManager.go[36-56]
- lib/pad/SessionManager.go[140-160]

## Suggested fix
1. In `SecurityManager.CheckAccess`, validate `token` format regardless of `sessionAuthorID` (and guard against `token == nil` before dereferencing).
2. If the intent is to allow session-only auth, avoid calling `AuthorManager.GetAuthorId()` with arbitrary tokens in the session-authenticated path; otherwise, keep token validation mandatory even when a session is present.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Session author not enforced 🐞 Bug ≡ Correctness
Description
SecurityManager.CheckAccess uses the session cookie only as a gate (for private group pad access)
but never enforces that the token-derived author matches the session’s stored AuthorID. This breaks
the author-binding semantics of API sessions and allows a session cookie for group G to be combined
with any other valid author token to access G’s private pads as that other author.
Code

lib/pad/SessionManager.go[R156-158]

+		if info.GroupID == groupID && now < info.ValidUntil {
+			return &info.AuthorID
+		}
Evidence
Sessions persist an AuthorID and FindAuthorID returns it, but CheckAccess ignores it and always uses
the token-derived author as the granted identity, without any equality check.

/lib/pad/SessionManager.go[26-31]
/lib/pad/SessionManager.go[140-160]
/lib/pad/SecurityManager.go[108-149]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
API sessions are stored with an `AuthorID`, but `SecurityManager.CheckAccess` does not use `sessionAuthorID` to determine the granted author identity, nor does it verify the token-derived author matches the session author.

## Issue Context
`SessionManager.FindAuthorID` returns the author of the first valid, unexpired session for the pad's group. `CheckAccess` computes this but always sets `GrantedAccess.AuthorId` from `AuthorManager.GetAuthorId(*token)`.

## Fix Focus Areas
- lib/pad/SecurityManager.go[108-149]
- lib/pad/SessionManager.go[26-31]
- lib/pad/SessionManager.go[140-160]

## Suggested fix
Choose one consistent model:
- **Strict binding (recommended for API sessions):** if `sessionAuthorID != nil`, require `retrievedAuthorFromToken.Id == *sessionAuthorID`, otherwise deny.
- **Session-driven identity:** if `sessionAuthorID != nil`, set `GrantedAccess.AuthorId = *sessionAuthorID` (and avoid creating/mapping authors via arbitrary tokens in this path).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Session errors silently denied 🐞 Bug ☼ Reliability
Description
SessionManager.FindAuthorID ignores datastore errors when loading session records and continues as
if the session does not exist. This can turn backend storage outages into misleading 403/denied
behavior, making incidents harder to debug and causing false denials for valid sessions.
Code

lib/pad/SessionManager.go[R151-155]

+	for _, sessionID := range sessionIDs {
+		info, err := sm.GetSessionInfo(strings.TrimSpace(sessionID))
+		if err != nil || info == nil {
+			continue
+		}
Evidence
FindAuthorID explicitly continues on errors, while the underlying datastore getters can return
errors on query failures; this makes the system behave as if sessions are missing rather than
failing fast or surfacing the error.

/lib/pad/SessionManager.go[149-155]
/lib/db/MySQLDB.go[1060-1069]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SessionManager.FindAuthorID` currently drops `GetSessionInfo` errors (`continue` on `err != nil`). Storage failures therefore present as "no valid session" instead of an internal error or at least a log/metric.

## Issue Context
`GetSessionInfo` depends on `DataStore.GetOIDCStorageValue`, which can return real DB errors; these are currently swallowed in the access-control path.

## Fix Focus Areas
- lib/pad/SessionManager.go[140-160]
- lib/pad/SecurityManager.go[108-114]
- lib/db/MySQLDB.go[1060-1069]

## Suggested fix
- Option A (best): change `FindAuthorID` to return `(*string, error)` and have `SecurityManager.CheckAccess` treat errors as internal (500) rather than access denied.
- Option B: if changing signatures is too invasive, at least add structured logging when `GetSessionInfo` returns an error so production issues are diagnosable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Wire API sessions into access control, fix authz level normalization, add stats/message APIs
✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Implement API-session storage/lookup in SessionManager and enforce it in access control.
• Fix authz normalization and nil-map handling to prevent incorrect denies and panics.
• Add /admin/api/stats and sendClientsMessage endpoints with regression and broadcast tests.
Diagram
graph TD
  A["Admin HTTP API"] --> B["Session endpoints"] --> C["pad.SessionManager"] --> D[("DataStore KV")]
  E["SecurityManager.CheckAccess"] --> C
  F["Stats endpoint"] --> G["WS SessionStore"]
  H["sendClientsMessage"] --> I["PadMessageHandler"] --> J{{"Pad clients"}}

  subgraph Legend
    direction LR
    _api["API/Service"] ~~~ _db[("Database/KV")]] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep API-session storage in lib/api/session
  • ➕ Avoids expanding pad.SessionManager responsibilities
  • ➕ Keeps HTTP API and persistence co-located
  • ➖ CheckAccess still needs a separate lookup path to honor cookies
  • ➖ Duplicates storage/lookup logic across packages, increasing drift risk
2. Use a dedicated DB table for sessions
  • ➕ More queryable and enforceable constraints (indexes, TTL cleanup)
  • ➕ Easier reporting without scanning/maintaining JSON id lists
  • ➖ Requires schema/migrations across supported backends
  • ➖ Deviates from upstream Etherpad behavior and current KV patterns

Recommendation: The chosen approach (centralizing API-session persistence + lookup in pad.SessionManager and having both HTTP endpoints and CheckAccess delegate to it) is the best fit for parity and correctness: it eliminates duplicated logic and ensures sessionID cookies actually affect authorization. A dedicated table could be revisited later if operational needs (cleanup/reporting) outgrow the KV-backed model.

Grey Divider

File Changes

Enhancement (6)
init.go Register sendClientsMessage admin route for pads +1/-0

Register sendClientsMessage admin route for pads

• Adds a new POST route under /admin/api/pads/:padId/sendClientsMessage. This wires the new handler into the existing pad admin API router.

lib/api/pad/init.go


messaging.go Add sendClientsMessage endpoint to broadcast custom room messages +46/-0

Add sendClientsMessage endpoint to broadcast custom room messages

• Introduces a new handler that validates a JSON body containing msg and checks pad existence. On success, it delegates to PadMessageHandler.SendCustomMessageToPad to broadcast a custom COLLABROOM message type.

lib/api/pad/messaging.go


init.go Expose GET /admin/api/stats route +2/-0

Expose GET /admin/api/stats route

• Registers a new stats endpoint on the private admin API router. Keeps existing health/metrics wiring intact while adding stats alongside them.

lib/api/stats/init.go


stats.go Implement getStats-style admin endpoint payload +45/-0

Implement getStats-style admin endpoint payload

• Adds GET handler returning total pads from the datastore and active session/pad counts from the websocket SessionStore. Returns a structured JSON payload mirroring upstream getStats.

lib/api/stats/stats.go


SessionManager.go Implement API-session CRUD, listing indexes, and cookie-based author lookup +176/-10

Implement API-session CRUD, listing indexes, and cookie-based author lookup

• Replaces stubbed session methods with full KV-backed storage using session:* plus group/author index lists. Adds FindAuthorID that supports quoted and comma-separated cookie values and enforces group match + expiry before granting an author.

lib/pad/SessionManager.go


PadMessageHandler.go Add SendCustomMessageToPad broadcast helper +21/-0

Add SendCustomMessageToPad broadcast helper

• Implements a server-side broadcast that emits a COLLABROOM message with type and timestamp to every socket in the pad room. This method backs the new sendClientsMessage HTTP API.

lib/ws/PadMessageHandler.go


Bug fix (1)
webaccess.go Fix authz normalization and harden modify checks against nil maps +16/-19

Fix authz normalization and harden modify checks against nil maps

• Changes UserCanModify to deny instead of panic when PadAuthorizations is nil. Fixes NormalizeAuthzLevel to allow readOnly/modify/create and to normalize true to create, matching upstream behavior; also replaces a misleading FIXME with accurate preAuthorize-hook documentation.

lib/pad/webaccess.go


Refactor (1)
init.go Delegate session API persistence to pad.SessionManager +36/-135

Delegate session API persistence to pad.SessionManager

• Removes in-handler KV persistence/indexing and replaces it with calls into pad.SessionManager (create/get/delete/list). Adds response conversion helpers and ensures list responses are deterministic via sorting.

lib/api/session/init.go


Tests (4)
pad_api_test.go Add API tests for sendClientsMessage validation and routing +31/-0

Add API tests for sendClientsMessage validation and routing

• Adds coverage for successful sendClientsMessage calls and verifies 400 on missing msg and 404 on unknown pads. Ensures the new pad admin endpoint behaves consistently.

lib/test/api/pad/pad_api_test.go


metrics_test.go Add test for GET /admin/api/stats response shape +27/-0

Add test for GET /admin/api/stats response shape

• Adds an end-to-end test that creates a pad, calls /admin/api/stats, unmarshals the response, and asserts non-negative counters with at least one pad present.

lib/test/api/stats/metrics_test.go


session_manager_test.go Add SessionManager, CheckAccess, authz normalization, and nil-map regressions +169/-0

Add SessionManager, CheckAccess, authz normalization, and nil-map regressions

• Adds unit/integration tests for FindAuthorID (including quoted multi-cookie behavior), session expiry/group mismatch handling, and CheckAccess gating for private group pads. Adds regressions for NormalizeAuthzLevel fallthrough and UserCanModify nil-map panic behavior.

lib/test/pad/session_manager_test.go


pad_message_handler_test.go Test custom message broadcast to all pad websocket clients +42/-0

Test custom message broadcast to all pad websocket clients

• Adds a websocket-layer test validating that SendCustomMessageToPad broadcasts a COLLABROOM message containing the custom type to all connected clients in the pad room.

lib/test/ws/pad_message_handler_test.go


Grey Divider

Qodo Logo

@SamTV12345 SamTV12345 enabled auto-merge (squash) June 11, 2026 20:29
Comment thread lib/pad/SessionManager.go
Comment on lines +149 to +159
sessionIDs := strings.Split(cookieQuoteTrimmer.ReplaceAllString(*sessionCookie, ""), ",")
now := time.Now().Unix()
for _, sessionID := range sessionIDs {
info, err := sm.GetSessionInfo(strings.TrimSpace(sessionID))
if err != nil || info == nil {
continue
}
if info.GroupID == groupID && now < info.ValidUntil {
return &info.AuthorID
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Session bypasses token validation 🐞 Bug ⛨ Security

Once SessionManager.FindAuthorID returns a non-nil author for a valid session cookie,
SecurityManager.CheckAccess skips utils.IsValidAuthorToken() and still calls
AuthorManager.GetAuthorId() with an unvalidated token. Because GetAuthorId() auto-creates an author
for unknown tokens, a valid session can be combined with arbitrary token strings (including empty /
non-"t." tokens) to mint authors and bypass token-format enforcement.
Agent Prompt
## Issue description
With real session lookups enabled, `SecurityManager.CheckAccess` now conditionally skips `utils.IsValidAuthorToken()` when `sessionAuthorID != nil`. This allows unvalidated token strings to reach `AuthorManager.GetAuthorId()`, which auto-creates authors for unknown tokens.

## Issue Context
This becomes reachable only after this PR because `SessionManager.FindAuthorID()` now returns non-nil values for valid sessions.

## Fix Focus Areas
- lib/pad/SecurityManager.go[108-130]
- lib/author/authorManager.go[36-56]
- lib/pad/SessionManager.go[140-160]

## Suggested fix
1. In `SecurityManager.CheckAccess`, validate `token` format regardless of `sessionAuthorID` (and guard against `token == nil` before dereferencing).
2. If the intent is to allow session-only auth, avoid calling `AuthorManager.GetAuthorId()` with arbitrary tokens in the session-authenticated path; otherwise, keep token validation mandatory even when a session is present.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread lib/pad/SessionManager.go
Comment on lines +156 to +158
if info.GroupID == groupID && now < info.ValidUntil {
return &info.AuthorID
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Session author not enforced 🐞 Bug ≡ Correctness

SecurityManager.CheckAccess uses the session cookie only as a gate (for private group pad access)
but never enforces that the token-derived author matches the session’s stored AuthorID. This breaks
the author-binding semantics of API sessions and allows a session cookie for group G to be combined
with any other valid author token to access G’s private pads as that other author.
Agent Prompt
## Issue description
API sessions are stored with an `AuthorID`, but `SecurityManager.CheckAccess` does not use `sessionAuthorID` to determine the granted author identity, nor does it verify the token-derived author matches the session author.

## Issue Context
`SessionManager.FindAuthorID` returns the author of the first valid, unexpired session for the pad's group. `CheckAccess` computes this but always sets `GrantedAccess.AuthorId` from `AuthorManager.GetAuthorId(*token)`.

## Fix Focus Areas
- lib/pad/SecurityManager.go[108-149]
- lib/pad/SessionManager.go[26-31]
- lib/pad/SessionManager.go[140-160]

## Suggested fix
Choose one consistent model:
- **Strict binding (recommended for API sessions):** if `sessionAuthorID != nil`, require `retrievedAuthorFromToken.Id == *sessionAuthorID`, otherwise deny.
- **Session-driven identity:** if `sessionAuthorID != nil`, set `GrantedAccess.AuthorId = *sessionAuthorID` (and avoid creating/mapping authors via arbitrary tokens in this path).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@SamTV12345 SamTV12345 merged commit a689f64 into main Jun 11, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant