Skip to content

feat(auth): add managed API key support#190

Merged
SantiagoDePolonia merged 3 commits intomainfrom
feature/multikey
Mar 31, 2026
Merged

feat(auth): add managed API key support#190
SantiagoDePolonia merged 3 commits intomainfrom
feature/multikey

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented Mar 30, 2026

Summary

  • add managed auth key storage/services with in-memory auth snapshots across sqlite, postgres, and mongo
  • wire managed keys into server auth, admin APIs, and startup/shutdown flow while preserving dashboard/admin recovery when no master key is configured
  • link authenticated key ids into audit logs and expose auth key context in the admin dashboard

Testing

  • go test ./...

Summary by CodeRabbit

  • New Features
    • Managed API key support: create, list, deactivate keys via admin API and dashboard; middleware honors managed keys alongside master key.
    • Audit logs capture and expose auth-key IDs (stored, indexed, searchable) and dashboard shows an auth-key-id badge on entries.
  • Tests
    • Added coverage for auth-key service, admin endpoints, audit middleware, storage readers, and auth middleware behaviors.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

Adds a managed auth-keys subsystem (types, stores, service, factory), admin APIs to create/list/deactivate keys, bearer-token authenticator middleware integrated with master key handling, audit-log propagation/storage of auth key IDs across readers/stores, and tests for service, admin, auth middleware, and audit logging.

Changes

Cohort / File(s) Summary
Auth Keys Core
internal/authkeys/types.go, internal/authkeys/store.go, internal/authkeys/service.go, internal/authkeys/factory.go
New auth-key data types, errors, Store interface, in-memory Service with issuance/authentication/refresh/background refresh, and factory Result for initialization and cleanup.
Auth Keys Stores
internal/authkeys/store_sqlite.go, internal/authkeys/store_postgresql.go, internal/authkeys/store_mongodb.go
SQLite/Postgres/MongoDB-backed Store implementations: schema/table creation, indexes, List/Create/Deactivate/Close methods, and row scanning/mapping.
Admin Handler & UI
internal/admin/handler.go, internal/admin/dashboard/templates/index.html
Handler gains authKeys dependency and Option; new admin endpoints for listing, creating, and deactivating auth keys; deactivation helper factored out; dashboard template conditionally renders auth_key_id badge.
Auth Integration (server)
internal/server/auth.go, internal/server/http.go
Introduced BearerTokenAuthenticator interface and AuthMiddlewareWithAuthenticator; middleware now supports master key + managed-key auth, sets auth key ID in context and enriches audit; HTTP config accepts an Authenticator and admin auth-key routes registered.
Application wiring
internal/app/app.go, internal/core/context.go
App initializes/wires authkeys Result into server/admin, startup/shutdown ordering updated, and context helpers WithAuthKeyID/GetAuthKeyID added.
Audit Log Surface
internal/auditlog/auditlog.go, internal/auditlog/middleware.go, internal/auditlog/stream_wrapper.go
Added AuthKeyID to LogEntry; middleware applies authentication enrichment from context and exposes EnrichEntryWithAuthKeyID; stream entry copies AuthKeyID.
Audit Log Storage & Readers
internal/auditlog/store_sqlite.go, internal/auditlog/store_postgresql.go, internal/auditlog/store_mongodb.go, internal/auditlog/reader_sqlite.go, internal/auditlog/reader_postgresql.go, internal/auditlog/reader_mongodb.go
Schema migrations and ALTER TABLEs to add auth_key_id, indexes for it, adjusted INSERT/batch placeholders and readers/scan/query filters to include auth_key_id across backends.
Admin & Auth Tests
internal/authkeys/service_test.go, internal/admin/handler_authkeys_test.go, internal/server/auth_test.go, internal/server/http_test.go
Unit tests for authkeys Service behavior (create/authenticate/deactivate/expiry), admin endpoints, middleware auth enrichment/failure cases, and HTTP route registration/auth-skip behavior.
Audit Log Tests & Integration
internal/auditlog/auditlog_test.go, internal/auditlog/store_postgresql_test.go, tests/integration/dbassert/auditlog.go
Added middleware test for storing auth key ID, updated SQL test expectations for extra bind/column, and expanded integration DB assertion structs/queries to include AuthKeyID.
Small UI change
internal/admin/dashboard/templates/index.html
Adds conditional monochrome auth_key_id: <value> badge for audit-log entries in the dashboard.

Sequence Diagram

sequenceDiagram
    participant Client
    participant AuthMiddleware as Auth Middleware
    participant AuthService as Auth Service
    participant Store as Auth Store
    participant Handler
    participant AuditLog
    Client->>AuthMiddleware: HTTP request + Authorization header
    AuthMiddleware->>AuthMiddleware: Check master key
    alt Master key valid
        AuthMiddleware->>Handler: proceed
    else Attempt managed-key auth
        AuthMiddleware->>AuthService: Authenticate(token)
        AuthService->>Store: Read cached keys / refresh as needed
        AuthService->>AuthService: parse token, hash secret, lookup active key
        alt Valid active key
            AuthService-->>AuthMiddleware: authKeyID
            AuthMiddleware->>AuditLog: EnrichEntryWithAuthKeyID(authKeyID)
            AuthMiddleware->>Handler: proceed (context contains authKeyID)
        else Invalid / inactive / expired
            AuthService-->>AuthMiddleware: error
            AuthMiddleware->>AuditLog: Enrich auth failure
            AuthMiddleware-->>Client: 401 Authentication Error
        end
    end
    Handler->>AuditLog: Write log entry (includes AuthKeyID if set)
    AuditLog->>AuditLog: Persist to DB (includes auth_key_id column)
    AuditLog-->>Handler: ack
    Handler-->>Client: Response
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I found a key beneath the log,

I tucked it in my pocket like a cog,
Services hum and tokens gleam,
Audits blink in midnight beam,
Hopping bytes make systems jog.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(auth): add managed API key support' clearly and concisely summarizes the primary change—introducing managed API key functionality to the authentication system.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/multikey

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/admin/handler_authkeys_test.go`:
- Around line 71-93: The test
TestAuthKeyEndpointsReturn503WhenServiceUnavailable misses asserting
DeactivateAuthKey returns 503 when h.authKeys is nil and also declares an unused
variable e; update the test to remove the unused echo variable (e) and add a
DeactivateAuthKey case that constructs an HTTP request (e.g., DELETE
/admin/api/v1/auth-keys/:id) and context, calls
h.DeactivateAuthKey(deactivateCtx), then assert deactivateRec.Code ==
http.StatusServiceUnavailable, keeping the existing pattern used for
ListAuthKeys and CreateAuthKey and referencing NewHandler and DeactivateAuthKey
to locate the code to change.

In `@internal/admin/handler.go`:
- Around line 736-738: The handler currently treats a nil IssuedKey as a valid
204 by returning when issued == nil; instead treat this as an unexpected service
bug: after calling h.authKeys.Create, if issued == nil and err == nil, log or
create a descriptive error and return an internal server error (e.g., HTTP 500)
rather than No Content. Update the code around the issued check in the handler
that calls h.authKeys.Create to surface the failure (include context like
request identifiers) and return an error response so the unexpected nil is
visible to callers/operators.

In `@internal/auditlog/store_postgresql_test.go`:
- Around line 67-68: The test's fatal error message references the wrong index:
when asserting the value of args[18] (in the test that checks "log-2") update
the t.Fatalf call inside the block that reads `if got := args[18]; got !=
"log-2" { ... }` to report `args[18]` (e.g., `t.Fatalf("args[18] = %v, want
log-2", got)`) so the message matches the checked index and value.

In `@internal/authkeys/service.go`:
- Around line 97-105: The Enabled() method currently returns true if any rows
exist in s.snapshot.byID, which can be true for only expired or deactivated
keys; change Enabled() (in type Service) to instead iterate the snapshot
(s.snapshot.byID) under RLock and return true only if at least one key is
currently usable by evaluating its fields against time.Now() (e.g., not
deactivated and not expired based on the key's expiry/active fields). Ensure you
use time.Now() for the check so AuthMiddlewareWithAuthenticator will not treat
snapshots with only unusable keys as enabling auth.
- Around line 177-182: When persisting changes (e.g., in s.store.Create and
s.store.Deactivate) don't make the API return an error if the subsequent
s.Refresh() reconciliation fails; instead apply the corresponding mutation to
the in-memory snapshot first (update the in-memory keys/snapshot state inside
the service), then call s.Refresh() as a best-effort reconciliation and
log/ignore refresh errors while returning success for the write. Update the
create path (where s.store.Create is called) and the deactivate path (where
s.store.Deactivate is called, also referenced around the other block at lines
~203-209) to mutate the in-memory snapshot before invoking s.Refresh(), and only
surface errors from the persistent store operations while treating s.Refresh()
failures as non-fatal reconciliation issues.
- Around line 215-243: The Authenticate method returns early when a secret is
found in snapshot.activeByHash, which bypasses per-request expiry checks and
allows expired keys to authenticate until the next Refresh; update Authenticate
(and/or snapshot population logic) so that finding an entry in activeByHash does
not skip time-sensitive checks: after locating the active entry in
snapshot.activeByHash within Authenticate, re-check the corresponding key's
ExpiresAt (and Enabled/DeactivatedAt) against time.Now().UTC() before returning,
or remove time-dependent "active" caching and instead always consult
snapshot.bySecretHash (or include expiration metadata in the active entry) so
expiry is validated on every request.

In `@internal/authkeys/store_mongodb.go`:
- Around line 95-105: The Deactivate method currently uses "$max" which can
overwrite a previously set deactivated_at; change the update to use an
aggregation update pipeline so deactivated_at is assigned only when missing
(preserving the original timestamp). In the MongoDBStore.Deactivate call
(s.collection.UpdateOne with mongoAuthKeyIDFilter), replace the document-style
update that contains "$max" with an aggregation pipeline update that sets
"enabled" to false and "updated_at" to now.UTC() and sets "deactivated_at" using
an expression like $ifNull/$cond to keep the existing value if present (i.e.,
deactivated_at = ifNull("$deactivated_at", now.UTC())). Ensure the UpdateOne
call still targets the same filter and behavior but no longer mutates an
existing deactivated_at.

In `@internal/authkeys/store_postgresql.go`:
- Around line 43-45: Remove the redundant unique index creation for secret_hash:
delete the entry that creates `idx_auth_keys_secret_hash` from the index list so
you only keep `CREATE INDEX IF NOT EXISTS idx_auth_keys_enabled ON
auth_keys(enabled)`; this avoids duplicating the `UNIQUE` constraint already
declared on `auth_keys(secret_hash)`.

In `@internal/authkeys/store_sqlite.go`:
- Around line 22-48: The auth_keys table schema already declares secret_hash
TEXT NOT NULL UNIQUE, so remove the redundant explicit unique index creation
`CREATE UNIQUE INDEX IF NOT EXISTS idx_auth_keys_secret_hash ON
auth_keys(secret_hash)` from the index loop; keep the UNIQUE constraint in the
table definition and leave the other indexes (`idx_auth_keys_enabled`,
`idx_auth_keys_created_at`) intact to avoid duplicate indexes on the secret_hash
column.

In `@internal/authkeys/types.go`:
- Around line 38-42: The CreateInput struct fields (Name, Description,
ExpiresAt) lack JSON tags so snake_case requests like "expires_at" won't bind to
ExpiresAt; update the CreateInput type to add JSON tags mapping "name",
"description", and "expires_at" to the respective fields (e.g., ExpiresAt ->
"expires_at") so standard JSON decoding correctly populates the struct during
admin key creation.

In `@internal/server/auth.go`:
- Around line 81-82: The audit currently uses authFailureMessage(err) and that
same string is returned to clients; change the flow so the detailed reason
(authFailureMessage(err)) is only used to enrich the audit/logging context,
while the JSON response uses a normalized message (e.g., "authentication
failed"). Concretely, update the code paths calling authenticationError(c,
authFailureMessage(err)) (and similar calls in the 91-107 block) to: 1) call
your audit/enrichment with authFailureMessage(err), and 2) call
authenticationError(c, "authentication failed") to construct the authErr
returned via authErr.ToJSON(), leaving the rich reason out of the response body
but preserved in logs.

In `@internal/server/http.go`:
- Around line 135-139: The current condition that appends "/admin/api/v1/*" to
authSkipPaths allows unauthenticated admin access when cfg.MasterKey == "" even
if no authenticator exists; change the guard so the skip is only applied when an
authentication mechanism is present (e.g., require cfg.Authenticator != nil or a
helper like cfg.HasAuthenticator() in the if condition alongside
cfg.AdminEndpointsEnabled and cfg.AdminHandler) and update the comment to note
this is only for bootstrapping; ensure you reference authSkipPaths,
cfg.MasterKey, cfg.Authenticator, cfg.AdminEndpointsEnabled and cfg.AdminHandler
when making the change.

In `@tests/integration/dbassert/auditlog.go`:
- Around line 62-65: The DB scan is currently scanning auth_key_id into a plain
string which will error on NULL; change the scan to use sql.NullString (or a
local *string/nullable carrier) when reading rows in the audit log query loop,
then set entry.AuthKeyID = authKeyID.String if authKeyID.Valid (or nil/empty
otherwise). Update the scan call that currently contains &entry.AuthKeyID among
the targets and the subsequent assignment to normalize NULL -> "" (or keep nil)
so NULL auth_key_id values are handled safely.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a13ad73e-a404-44bd-8a3b-546918fffd12

📥 Commits

Reviewing files that changed from the base of the PR and between 64320e7 and 92efa6c.

📒 Files selected for processing (29)
  • internal/admin/dashboard/templates/index.html
  • internal/admin/handler.go
  • internal/admin/handler_authkeys_test.go
  • internal/app/app.go
  • internal/auditlog/auditlog.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/middleware.go
  • internal/auditlog/reader_mongodb.go
  • internal/auditlog/reader_postgresql.go
  • internal/auditlog/reader_sqlite.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/store_postgresql.go
  • internal/auditlog/store_postgresql_test.go
  • internal/auditlog/store_sqlite.go
  • internal/auditlog/stream_wrapper.go
  • internal/authkeys/factory.go
  • internal/authkeys/service.go
  • internal/authkeys/service_test.go
  • internal/authkeys/store.go
  • internal/authkeys/store_mongodb.go
  • internal/authkeys/store_postgresql.go
  • internal/authkeys/store_sqlite.go
  • internal/authkeys/types.go
  • internal/core/context.go
  • internal/server/auth.go
  • internal/server/auth_test.go
  • internal/server/http.go
  • internal/server/http_test.go
  • tests/integration/dbassert/auditlog.go

Comment on lines +97 to +105
// Enabled reports whether managed auth keys should be enforced.
func (s *Service) Enabled() bool {
if s == nil {
return false
}
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.snapshot.byID) > 0
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Enabled() should reflect usable keys, not stored rows.

AuthMiddlewareWithAuthenticator uses this to decide whether auth can be bypassed. Returning true for a snapshot that only contains expired or deactivated keys makes a deployment with no master key permanently unreachable, which breaks the recovery path this PR is trying to preserve. Evaluate the current snapshot against time.Now() instead of len(s.snapshot.byID).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/authkeys/service.go` around lines 97 - 105, The Enabled() method
currently returns true if any rows exist in s.snapshot.byID, which can be true
for only expired or deactivated keys; change Enabled() (in type Service) to
instead iterate the snapshot (s.snapshot.byID) under RLock and return true only
if at least one key is currently usable by evaluating its fields against
time.Now() (e.g., not deactivated and not expired based on the key's
expiry/active fields). Ensure you use time.Now() for the check so
AuthMiddlewareWithAuthenticator will not treat snapshots with only unusable keys
as enabling auth.

Comment on lines +38 to +42
type CreateInput struct {
Name string
Description string
ExpiresAt *time.Time
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add JSON tags to CreateInput to preserve snake_case request compatibility.

Without tags, expires_at won’t bind to ExpiresAt in standard JSON decoding. This can silently drop expiry during admin key creation (Line 41).

🔧 Proposed fix
 type CreateInput struct {
-	Name        string
-	Description string
-	ExpiresAt   *time.Time
+	Name        string     `json:"name"`
+	Description string     `json:"description"`
+	ExpiresAt   *time.Time `json:"expires_at"`
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type CreateInput struct {
Name string
Description string
ExpiresAt *time.Time
}
type CreateInput struct {
Name string `json:"name"`
Description string `json:"description"`
ExpiresAt *time.Time `json:"expires_at"`
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/authkeys/types.go` around lines 38 - 42, The CreateInput struct
fields (Name, Description, ExpiresAt) lack JSON tags so snake_case requests like
"expires_at" won't bind to ExpiresAt; update the CreateInput type to add JSON
tags mapping "name", "description", and "expires_at" to the respective fields
(e.g., ExpiresAt -> "expires_at") so standard JSON decoding correctly populates
the struct during admin key creation.

Comment on lines +135 to +139
// When no bootstrap master key is configured, keep admin APIs reachable so
// the dashboard can recover managed-key access instead of locking itself out.
if cfg != nil && cfg.MasterKey == "" && cfg.AdminEndpointsEnabled && cfg.AdminHandler != nil {
authSkipPaths = append(authSkipPaths, "/admin/api/v1/*")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Security: Admin APIs exposed without authentication when no master key is configured.

When cfg.MasterKey == "" and cfg.AdminEndpointsEnabled is true, /admin/api/v1/* is added to authSkipPaths. However, if cfg.Authenticator is also nil (no managed keys configured), all admin API endpoints become completely unauthenticated—including sensitive operations like creating/deactivating auth keys.

Consider gating this skip-path logic on the presence of an authenticator, or adding a check that at least one auth mechanism exists:

-	if cfg != nil && cfg.MasterKey == "" && cfg.AdminEndpointsEnabled && cfg.AdminHandler != nil {
+	if cfg != nil && cfg.MasterKey == "" && cfg.Authenticator != nil && cfg.AdminEndpointsEnabled && cfg.AdminHandler != nil {
 		authSkipPaths = append(authSkipPaths, "/admin/api/v1/*")
 	}

Alternatively, if intentional for bootstrap scenarios, document the security implications clearly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// When no bootstrap master key is configured, keep admin APIs reachable so
// the dashboard can recover managed-key access instead of locking itself out.
if cfg != nil && cfg.MasterKey == "" && cfg.AdminEndpointsEnabled && cfg.AdminHandler != nil {
authSkipPaths = append(authSkipPaths, "/admin/api/v1/*")
}
// When no bootstrap master key is configured, keep admin APIs reachable so
// the dashboard can recover managed-key access instead of locking itself out.
if cfg != nil && cfg.MasterKey == "" && cfg.Authenticator != nil && cfg.AdminEndpointsEnabled && cfg.AdminHandler != nil {
authSkipPaths = append(authSkipPaths, "/admin/api/v1/*")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/server/http.go` around lines 135 - 139, The current condition that
appends "/admin/api/v1/*" to authSkipPaths allows unauthenticated admin access
when cfg.MasterKey == "" even if no authenticator exists; change the guard so
the skip is only applied when an authentication mechanism is present (e.g.,
require cfg.Authenticator != nil or a helper like cfg.HasAuthenticator() in the
if condition alongside cfg.AdminEndpointsEnabled and cfg.AdminHandler) and
update the comment to note this is only for bootstrapping; ensure you reference
authSkipPaths, cfg.MasterKey, cfg.Authenticator, cfg.AdminEndpointsEnabled and
cfg.AdminHandler when making the change.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/auditlog/store_postgresql_test.go (1)

58-81: 🧹 Nitpick | 🔵 Trivial

Add direct assertions for auth_key_id bind slots.

The test validates SQL text and shifted indices, but it still doesn’t assert the new auth_key_id argument positions. A bind-order swap with nearby fields (like client_ip) could pass unnoticed.

Proposed test hardening
 	if got := args[8]; got != CacheTypeExact {
 		t.Fatalf("args[8] = %v, want %q", got, CacheTypeExact)
 	}
+	if got := args[11]; got != "" {
+		t.Fatalf("args[11] = %v, want empty auth_key_id", got)
+	}
+	if got := args[12]; got != "127.0.0.1" {
+		t.Fatalf("args[12] = %v, want 127.0.0.1", got)
+	}
 	if got := args[18]; got != "log-2" {
 		t.Fatalf("args[18] = %v, want log-2", got)
 	}
+	if got := args[29]; got != "" {
+		t.Fatalf("args[29] = %v, want empty auth_key_id", got)
+	}
+	if got := args[30]; got != "10.0.0.1" {
+		t.Fatalf("args[30] = %v, want 10.0.0.1", got)
+	}
 	if got := string(args[17].([]byte)); got != `{"user_agent":"test-agent"}` {
 		t.Fatalf("args[17] = %q, want %q", got, `{"user_agent":"test-agent"}`)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auditlog/store_postgresql_test.go` around lines 58 - 81, Add
explicit assertions in the test (internal/auditlog/store_postgresql_test.go)
that verify the bind-slot values for the auth_key_id parameter so a bind-order
regression is caught; locate where args is being examined (the existing checks
around args[0], args[8], args[17], args[18], args[26], args[35]) and add checks
for the auth_key_id slots (the index/indices adjacent to client_ip and other
nearby fields) asserting the exact expected value or nil and the expected type
(e.g., string, []byte, or nil) to ensure auth_key_id's position and value are
validated.
♻️ Duplicate comments (1)
internal/authkeys/service.go (1)

98-106: ⚠️ Potential issue | 🟠 Major

Enabled() still returns true for snapshots with only expired/deactivated keys.

The method checks len(s.snapshot.byID) > 0 but doesn't verify that at least one key is currently usable. This could cause AuthMiddlewareWithAuthenticator to enforce auth when no valid keys exist, potentially making the deployment unreachable if all keys are expired/deactivated and no master key is configured.

Consider checking len(s.snapshot.activeByHash) > 0 or iterating to verify at least one key is Active(now).

🔧 Proposed fix using activeByHash
 func (s *Service) Enabled() bool {
 	if s == nil {
 		return false
 	}
 	s.mu.RLock()
 	defer s.mu.RUnlock()
-	return len(s.snapshot.byID) > 0
+	return len(s.snapshot.activeByHash) > 0
 }

Note: Using activeByHash is a fast approximation. If expiry can occur between refreshes, you may need to evaluate Active(time.Now().UTC()) on at least one key for precise behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/authkeys/service.go` around lines 98 - 106, Enabled() currently
returns true if any key exists in s.snapshot.byID which can be true even when
all keys are expired/deactivated; update Enabled() to check for usable keys
instead (either return len(s.snapshot.activeByHash) > 0 or iterate
s.snapshot.byID and call key.Active(time.Now().UTC()) and return true on the
first active key) so that AuthMiddlewareWithAuthenticator will only enforce auth
when at least one key is currently usable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/authkeys/store_postgresql.go`:
- Around line 120-125: Replace the direct equality check against pgx.ErrNoRows
with errors.Is to handle wrapped errors: in the error-handling block that
returns AuthKey{}, ErrNotFound (the code comparing err == pgx.ErrNoRows), change
it to use errors.Is(err, pgx.ErrNoRows) and ensure the "errors" package is
imported; keep the existing return semantics for ErrNotFound and other errors.

---

Outside diff comments:
In `@internal/auditlog/store_postgresql_test.go`:
- Around line 58-81: Add explicit assertions in the test
(internal/auditlog/store_postgresql_test.go) that verify the bind-slot values
for the auth_key_id parameter so a bind-order regression is caught; locate where
args is being examined (the existing checks around args[0], args[8], args[17],
args[18], args[26], args[35]) and add checks for the auth_key_id slots (the
index/indices adjacent to client_ip and other nearby fields) asserting the exact
expected value or nil and the expected type (e.g., string, []byte, or nil) to
ensure auth_key_id's position and value are validated.

---

Duplicate comments:
In `@internal/authkeys/service.go`:
- Around line 98-106: Enabled() currently returns true if any key exists in
s.snapshot.byID which can be true even when all keys are expired/deactivated;
update Enabled() to check for usable keys instead (either return
len(s.snapshot.activeByHash) > 0 or iterate s.snapshot.byID and call
key.Active(time.Now().UTC()) and return true on the first active key) so that
AuthMiddlewareWithAuthenticator will only enforce auth when at least one key is
currently usable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8a276386-d595-4344-9a42-bd00650e848b

📥 Commits

Reviewing files that changed from the base of the PR and between 92efa6c and b53fa1e.

📒 Files selected for processing (11)
  • internal/admin/handler.go
  • internal/admin/handler_authkeys_test.go
  • internal/auditlog/store_postgresql_test.go
  • internal/authkeys/service.go
  • internal/authkeys/service_test.go
  • internal/authkeys/store_mongodb.go
  • internal/authkeys/store_postgresql.go
  • internal/authkeys/store_sqlite.go
  • internal/server/auth.go
  • internal/server/auth_test.go
  • tests/integration/dbassert/auditlog.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/authkeys/store_postgresql.go`:
- Around line 48-50: The DDL error omits which index statement failed: when
calling pool.Exec(ctx, index) update the error wrap to include the index (or its
name) for context (e.g., change the fmt.Errorf call around pool.Exec to include
the index string or index name variable), so replace the current
fmt.Errorf("failed to create auth_keys index: %w", err) with one that embeds the
index/statement (for example "failed to create auth_keys index (%s): %w", index,
err) to make failures immediately diagnosable; locate the pool.Exec(ctx, index)
call and the surrounding error return in store_postgresql.go to apply this
change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 91f064af-5b0b-4e4e-b2c7-dad3b2b1df2c

📥 Commits

Reviewing files that changed from the base of the PR and between b53fa1e and ad5ae69.

📒 Files selected for processing (2)
  • internal/auditlog/store_postgresql_test.go
  • internal/authkeys/store_postgresql.go

@SantiagoDePolonia SantiagoDePolonia merged commit 255d775 into main Mar 31, 2026
16 checks passed
@SantiagoDePolonia SantiagoDePolonia deleted the feature/multikey branch April 1, 2026 21:29
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