Skip to content

feat(authkeys): labels on managed API keys applied to every request#467

Merged
SantiagoDePolonia merged 4 commits into
mainfrom
feature/apikeys-labelling
Jul 4, 2026
Merged

feat(authkeys): labels on managed API keys applied to every request#467
SantiagoDePolonia merged 4 commits into
mainfrom
feature/apikeys-labelling

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Managed API keys can now carry labels. Every request authenticated with a labelled key gets those labels attached automatically — merged and de-duplicated with any labels the header-based tagging rules already extracted — so per-key traffic shows up in the usage by-label breakdown, the request log label filter, and audit log entries (data.labels) with zero client changes. Issue each team/service its own labelled key and attribution is done.

User-visible changes

Admin API

  • POST /admin/auth-keys accepts "labels": ["team-a", "batch"] (normalized: trimmed, de-duplicated); list/create responses include them.
  • PUT /admin/auth-keys/:id/labels replaces a key's labels without rotating the key; [] clears them; 404 on unknown id; returns the updated key view.

Dashboard

  • Create API Key modal gains a comma-separated Labels field with inline help.
  • API Keys table shows a Labels column using the same color-hashed chips as the usage page.
  • New Edit Labels action per active key opens a small modal (empty input removes all labels).

Docs

  • New page docs/features/labelling.mdx covering both header-based tagging and API key labels (config.yaml, env vars, do_not_pass, update semantics, where labels surface); registered in docs.json. CLAUDE.md tagging reference updated.

Implementation notes

  • labels JSON/JSONB column on auth_keys across SQLite/PostgreSQL/MongoDB with idempotent migrations (same pattern as user_path).
  • Key labels merge into the request-label context in the auth middleware via a new core.MergeLabels helper; all existing usage/audit recorders pick them up unchanged. The audit middleware re-reads labels post-auth (applyAuthentication) because the entry snapshots them before authentication runs.
  • Label updates patch the service's in-memory snapshot immediately, so the instance serving the update applies new labels to the very next request; other replicas converge on the ~1-minute background refresh (documented).
  • Drive-by refactor: the JSON string-array column encode/decode duplicated across the usage stores/readers and the new authkeys code is consolidated into sqlutil.NullableJSONStrings/StringsFromJSON (goccy-backed).
  • Drive-by fix: the sidebar mobile theme toggle stacked one orphaned icon per click — lucide's createIcons() replaces <i data-lucide> nodes, breaking Alpine's x-if cleanup when the placeholder is the template root. The toggle now uses inline SVGs.

Test plan

  • Unit: label normalization/merge, service create/update/authenticate (including immediate snapshot effect and clearing), SQLite store round-trip + migration idempotency, admin handler create/update/404/503, auth middleware merge with and without header labels, audit label patching, sqlutil helpers, dashboard JS label parsing and the Edit Labels editor (all 400 dashboard JS tests pass).
  • Full go test ./... green; pre-commit race tests, lint, hot-path perf guard, and mint docs validation pass.
  • Live smoke: created a key with labels + a TAGGING_HEADER_1 rule, sent a chat request with an extra header label → audit entry recorded ["from-header","team-a","batch"] (cross-source dedup); PUT .../labels then an immediate request carried the new labels; clear → NULL column; unknown id → 404.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added labels support for API keys, including optional comma-separated labels during creation and an editor to update them later.
  • Documentation
    • Added a new documentation page covering labeling sources, merging/deduplication rules, and where labels appear.
  • Bug Fixes
    • Labels are now consistently reflected across usage views and audit logs, including cases where labels are enriched during authentication.
  • Style
    • Improved dashboard visuals for label chips and refined the mobile theme toggle icons.

SantiagoDePolonia and others added 3 commits July 4, 2026 13:44
Managed API keys can now carry labels. Every request authenticated with
a labelled key gets those labels merged (deduplicated) with any labels
the header-based tagging rules already extracted, so they flow into
usage analytics, the request log label filter, and audit log entries.

- AuthKey/CreateInput gain a labels field; labels JSON/JSONB column on
  all three storage backends with idempotent migrations
- auth middleware merges key labels into the request-label context; the
  audit middleware re-reads labels post-auth since the entry snapshots
  them before authentication runs
- labels are editable without rotating the key via
  PUT /admin/auth-keys/:id/labels (empty list clears); the service
  patches its in-memory snapshot immediately so the serving instance
  applies changes to the next request
- dashboard: labels input on Create API Key, label chips column, and an
  Edit Labels modal on the API Keys page
- new docs page docs/features/labelling.mdx covering header tagging and
  key labels; registered in docs.json

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The label column encode/decode that usage duplicated per backend now
lives in sqlutil (goccy-backed, added for auth key labels), replacing
marshalLabels and the inline reader unmarshal blocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… per click

lucide.createIcons() replaces <i data-lucide> placeholders with new svg
nodes, but Alpine's x-if cleanup only removes nodes it inserted itself,
so each theme toggle orphaned the previous icon and added another. The
toggle now uses inline SVGs (the copy-button pattern), which Alpine can
insert and remove cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mintlify

mintlify Bot commented Jul 4, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
gomodel 🟢 Ready View Preview Jul 4, 2026, 12:21 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 05814365-5f3f-4393-a130-95d84094c9cf

📥 Commits

Reviewing files that changed from the base of the PR and between d528d50 and 2197f45.

📒 Files selected for processing (1)
  • internal/authkeys/store_mongodb.go

📝 Walkthrough

Walkthrough

Adds label support for auth keys end to end: shared merge and JSON helpers, storage and admin API updates, dashboard editing, authentication/audit propagation, usage log serialization changes, and new documentation.

Changes

Auth key labels end-to-end

Layer / File(s) Summary
Label merge helper and types
internal/core/labels.go, internal/core/labels_test.go, internal/authkeys/types.go
Adds MergeLabels and Labels fields on AuthKey, CreateInput, and AuthenticationResult.
Auth key storage persistence and UpdateLabels
internal/authkeys/store*.go, internal/authkeys/store_sqlite_test.go
Extends auth-key storage to persist, read, and update labels across MongoDB, PostgreSQL, and SQLite, including schema changes and round-trip tests.
Auth key service create, update, authenticate
internal/authkeys/service.go, internal/authkeys/service_test.go
Creates auth keys with labels, returns labels from authentication, and adds UpdateLabels with in-memory snapshot updates and tests.
Admin API create and label update
internal/admin/handler_authkeys*.go, internal/admin/routes*.go
Adds labels to auth-key creation requests, introduces UpdateAuthKeyLabels, and registers the new PUT route with tests.
Dashboard label editor and display
internal/admin/dashboard/static/js/modules/auth-keys*.js/.cjs, internal/admin/dashboard/templates/page-auth-keys.html, internal/admin/dashboard/static/css/dashboard.css
Adds label input, labels column, edit-labels modal, parsing and submission flow, static chip styling, and dashboard tests.
Authentication and audit label propagation
internal/server/auth*.go, internal/auditlog/middleware*.go
Refactors managed-auth enrichment into applyAuthKeyResult and refreshes audit entry labels from request context, with supporting tests.
Shared JSON helpers for usage labels
internal/storage/sqlutil/*, internal/usage/*
Adds nullable JSON string helpers and wires usage readers and writers to use them instead of local marshal and unmarshal logic.
Docs and sidebar updates
docs/features/labelling.mdx, docs/docs.json, CLAUDE.md, internal/admin/dashboard/templates/sidebar.html
Adds the labelling docs page and navigation entry, updates the Tagging config bullet, and replaces mobile theme toggle placeholders with inline SVG.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant AuthMiddleware
    participant AuthKeysService
    participant AuditMiddleware
    participant RequestContext

    Client->>AuthMiddleware: authenticated request
    AuthMiddleware->>AuthKeysService: Authenticate(token)
    AuthKeysService-->>AuthMiddleware: AuthenticationResult(Labels, UserPath)
    AuthMiddleware->>RequestContext: applyAuthKeyResult()
    AuthMiddleware->>RequestContext: merge request labels and key labels
    AuthMiddleware->>AuditMiddleware: continue request
    AuditMiddleware->>RequestContext: RequestLabelsFromContext()
    AuditMiddleware->>AuditMiddleware: update entry.Data.Labels
Loading
sequenceDiagram
    participant AdminUI
    participant AdminHandler
    participant AuthKeysService
    participant Store

    AdminUI->>AdminHandler: PUT /admin/auth-keys/:id/labels
    AdminHandler->>AuthKeysService: UpdateLabels(id, labels)
    AuthKeysService->>AuthKeysService: MergeLabels(labels)
    AuthKeysService->>Store: UpdateLabels(id, labels, now)
    Store-->>AuthKeysService: ok or ErrNotFound
    AuthKeysService->>AuthKeysService: applyLabelsUpdate()
    AuthKeysService-->>AdminHandler: updated View
    AdminHandler-->>AdminUI: 200 JSON or 404
Loading

Possibly related PRs

  • ENTERPILOT/GoModel#190: Both PRs modify the managed-auth enrichment path in internal/server/auth.go and internal/auditlog/middleware.go.
  • ENTERPILOT/GoModel#197: Both PRs extend the auth-key authentication result and managed-auth enrichment flow in internal/authkeys/service.go and internal/server/auth.go.
  • ENTERPILOT/GoModel#454: Both PRs route labels through request context into audit and usage flows.

Poem

I’m a rabbit, soft and spry,
With labels hopping by and by 🐇
Merged and stored, then set in flight,
In logs and keys they shine so bright.
A little hop, a tidy cheer —
New tags are everywhere this year!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: managed API key labels now propagate onto requests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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/apikeys-labelling

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.

@codecov-commenter

codecov-commenter commented Jul 4, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 64.40678% with 63 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/authkeys/store_postgresql.go 0.00% 22 Missing ⚠️
internal/authkeys/store_mongodb.go 0.00% 15 Missing ⚠️
internal/authkeys/service.go 71.42% 7 Missing and 5 partials ⚠️
internal/admin/handler_authkeys.go 68.75% 3 Missing and 2 partials ⚠️
internal/authkeys/store_sqlite.go 83.33% 2 Missing and 2 partials ⚠️
internal/storage/sqlutil/sqlutil.go 83.33% 2 Missing and 1 partial ⚠️
internal/usage/reader_postgresql.go 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@SantiagoDePolonia SantiagoDePolonia self-assigned this Jul 4, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/admin/dashboard/static/js/modules/auth-keys.js`:
- Around line 281-370: In submitAuthKeyLabelsEditor, the current ordering leaves
a redundant editor.submitting reset after closeAuthKeyLabelsEditor replaces
authKeyLabelsEditor with a new object. Simplify the submit/close flow by
clearing submitting in the active editor state only once, then closing the
editor, and avoid mutating the stale local editor reference in the finally
block. Keep the existing behavior in openAuthKeyLabelsEditor,
closeAuthKeyLabelsEditor, and submitAuthKeyLabelsEditor unchanged aside from
this cleanup.

In `@internal/authkeys/store_mongodb.go`:
- Around line 99-112: Update MongoDBStore.UpdateLabels so clearing labels
removes the field instead of storing a null value: when labels is empty or nil,
use an $unset update for labels alongside the updated_at $set, and keep the
existing UpdateOne/ErrNotFound handling. Otherwise, continue setting labels
normally, using the UpdateLabels method and mongoAuthKeyIDFilter to locate the
document.

In `@internal/core/labels.go`:
- Around line 8-30: `MergeLabels` currently only trims and deduplicates, but it
should also enforce safe bounds before labels reach audit logs and usage
queries. Update the normalization chokepoint in `internal/core/labels.go` so
`MergeLabels` rejects or truncates overly long labels, caps the number of labels
per key, and optionally constrains allowed characters; keep the existing
trim/dedupe behavior and ensure both `Create` and `UpdateLabels` inherit the new
limits through this function.
🪄 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: 2e0b9f26-3e9a-429e-8643-bd203608d3e3

📥 Commits

Reviewing files that changed from the base of the PR and between 1d7cfe5 and d528d50.

📒 Files selected for processing (32)
  • CLAUDE.md
  • docs/docs.json
  • docs/features/labelling.mdx
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/modules/auth-keys.js
  • internal/admin/dashboard/static/js/modules/auth-keys.test.cjs
  • internal/admin/dashboard/templates/page-auth-keys.html
  • internal/admin/dashboard/templates/sidebar.html
  • internal/admin/handler_authkeys.go
  • internal/admin/handler_authkeys_test.go
  • internal/admin/routes.go
  • internal/admin/routes_test.go
  • internal/auditlog/middleware.go
  • internal/auditlog/middleware_test.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/store_sqlite_test.go
  • internal/authkeys/types.go
  • internal/core/labels.go
  • internal/core/labels_test.go
  • internal/server/auth.go
  • internal/server/auth_test.go
  • internal/storage/sqlutil/sqlutil.go
  • internal/storage/sqlutil/sqlutil_test.go
  • internal/usage/reader_postgresql.go
  • internal/usage/reader_sqlite.go
  • internal/usage/store_postgresql.go
  • internal/usage/store_sqlite.go

Comment on lines +281 to +370
openAuthKeyLabelsEditor(key) {
if (!key || this.authKeyLabelsEditor.submitting) {
return;
}
this.authKeyLabelsEditor = {
open: true,
id: key.id,
name: key.name || '',
value: (key.labels || []).join(', '),
submitting: false,
error: ''
};
},

closeAuthKeyLabelsEditor() {
if (!this.authKeyLabelsEditor.open || this.authKeyLabelsEditor.submitting) {
return;
}
this.authKeyLabelsEditor = {
open: false,
id: '',
name: '',
value: '',
submitting: false,
error: ''
};
},

async submitAuthKeyLabelsEditor() {
const editor = this.authKeyLabelsEditor;
if (!editor.open || editor.submitting || !editor.id) {
return;
}
editor.submitting = true;
editor.error = '';
this.authKeyNotice = '';
const payload = { labels: this.parseAuthKeyLabels(editor.value) };

try {
const request = typeof this.requestOptions === 'function'
? this.requestOptions({
method: 'PUT',
body: JSON.stringify(payload)
})
: {
method: 'PUT',
headers: this.headers(),
body: JSON.stringify(payload)
};
const res = await fetch('/admin/auth-keys/' + encodeURIComponent(editor.id) + '/labels', request);
if (res.status === 503) {
this.authKeysAvailable = false;
editor.error = 'Auth keys feature is unavailable.';
return;
}
if (typeof this.handleFetchResponse === 'function') {
const handled = this.handleFetchResponse(res, 'update API key labels', request);
if (typeof this.isStaleAuthFetchResult === 'function' && this.isStaleAuthFetchResult(handled)) {
return;
}
if (!handled) {
if (res.status === 401) {
editor.error = 'Authentication required.';
return;
}
editor.error = await this._authKeyResponseMessage(res, 'Failed to update labels.');
console.error('Failed to update auth key labels:', res.status, res.statusText, editor.error);
return;
}
} else if (res.status === 401) {
this.authError = true;
this.needsAuth = true;
editor.error = 'Authentication required.';
return;
} else if (res.status !== 200) {
editor.error = await this._authKeyResponseMessage(res, 'Failed to update labels.');
console.error('Failed to update auth key labels:', res.status, res.statusText, editor.error);
return;
}
await this.fetchAuthKeys();
this.authKeyNotice = 'Labels updated for key "' + editor.name + '".';
editor.submitting = false;
this.closeAuthKeyLabelsEditor();
} catch (e) {
console.error('Failed to update auth key labels:', e);
editor.error = 'Failed to update labels.';
} finally {
editor.submitting = false;
}
},

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.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant submitting reset after editor is closed.

Line 362 sets editor.submitting = false before closeAuthKeyLabelsEditor() reassigns this.authKeyLabelsEditor to a fresh object; the finally block at Line 368 then mutates the now-orphaned old editor reference. Harmless today, but worth simplifying so the intent (why submitting must be cleared before close) is clearer to future readers.

♻️ Simplify submit/close ordering
-                    await this.fetchAuthKeys();
-                    this.authKeyNotice = 'Labels updated for key "' + editor.name + '".';
-                    editor.submitting = false;
-                    this.closeAuthKeyLabelsEditor();
+                    await this.fetchAuthKeys();
+                    const notice = 'Labels updated for key "' + editor.name + '".';
+                    editor.submitting = false;
+                    this.closeAuthKeyLabelsEditor();
+                    this.authKeyNotice = notice;
📝 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
openAuthKeyLabelsEditor(key) {
if (!key || this.authKeyLabelsEditor.submitting) {
return;
}
this.authKeyLabelsEditor = {
open: true,
id: key.id,
name: key.name || '',
value: (key.labels || []).join(', '),
submitting: false,
error: ''
};
},
closeAuthKeyLabelsEditor() {
if (!this.authKeyLabelsEditor.open || this.authKeyLabelsEditor.submitting) {
return;
}
this.authKeyLabelsEditor = {
open: false,
id: '',
name: '',
value: '',
submitting: false,
error: ''
};
},
async submitAuthKeyLabelsEditor() {
const editor = this.authKeyLabelsEditor;
if (!editor.open || editor.submitting || !editor.id) {
return;
}
editor.submitting = true;
editor.error = '';
this.authKeyNotice = '';
const payload = { labels: this.parseAuthKeyLabels(editor.value) };
try {
const request = typeof this.requestOptions === 'function'
? this.requestOptions({
method: 'PUT',
body: JSON.stringify(payload)
})
: {
method: 'PUT',
headers: this.headers(),
body: JSON.stringify(payload)
};
const res = await fetch('/admin/auth-keys/' + encodeURIComponent(editor.id) + '/labels', request);
if (res.status === 503) {
this.authKeysAvailable = false;
editor.error = 'Auth keys feature is unavailable.';
return;
}
if (typeof this.handleFetchResponse === 'function') {
const handled = this.handleFetchResponse(res, 'update API key labels', request);
if (typeof this.isStaleAuthFetchResult === 'function' && this.isStaleAuthFetchResult(handled)) {
return;
}
if (!handled) {
if (res.status === 401) {
editor.error = 'Authentication required.';
return;
}
editor.error = await this._authKeyResponseMessage(res, 'Failed to update labels.');
console.error('Failed to update auth key labels:', res.status, res.statusText, editor.error);
return;
}
} else if (res.status === 401) {
this.authError = true;
this.needsAuth = true;
editor.error = 'Authentication required.';
return;
} else if (res.status !== 200) {
editor.error = await this._authKeyResponseMessage(res, 'Failed to update labels.');
console.error('Failed to update auth key labels:', res.status, res.statusText, editor.error);
return;
}
await this.fetchAuthKeys();
this.authKeyNotice = 'Labels updated for key "' + editor.name + '".';
editor.submitting = false;
this.closeAuthKeyLabelsEditor();
} catch (e) {
console.error('Failed to update auth key labels:', e);
editor.error = 'Failed to update labels.';
} finally {
editor.submitting = false;
}
},
openAuthKeyLabelsEditor(key) {
if (!key || this.authKeyLabelsEditor.submitting) {
return;
}
this.authKeyLabelsEditor = {
open: true,
id: key.id,
name: key.name || '',
value: (key.labels || []).join(', '),
submitting: false,
error: ''
};
},
closeAuthKeyLabelsEditor() {
if (!this.authKeyLabelsEditor.open || this.authKeyLabelsEditor.submitting) {
return;
}
this.authKeyLabelsEditor = {
open: false,
id: '',
name: '',
value: '',
submitting: false,
error: ''
};
},
async submitAuthKeyLabelsEditor() {
const editor = this.authKeyLabelsEditor;
if (!editor.open || editor.submitting || !editor.id) {
return;
}
editor.submitting = true;
editor.error = '';
this.authKeyNotice = '';
const payload = { labels: this.parseAuthKeyLabels(editor.value) };
try {
const request = typeof this.requestOptions === 'function'
? this.requestOptions({
method: 'PUT',
body: JSON.stringify(payload)
})
: {
method: 'PUT',
headers: this.headers(),
body: JSON.stringify(payload)
};
const res = await fetch('/admin/auth-keys/' + encodeURIComponent(editor.id) + '/labels', request);
if (res.status === 503) {
this.authKeysAvailable = false;
editor.error = 'Auth keys feature is unavailable.';
return;
}
if (typeof this.handleFetchResponse === 'function') {
const handled = this.handleFetchResponse(res, 'update API key labels', request);
if (typeof this.isStaleAuthFetchResult === 'function' && this.isStaleAuthFetchResult(handled)) {
return;
}
if (!handled) {
if (res.status === 401) {
editor.error = 'Authentication required.';
return;
}
editor.error = await this._authKeyResponseMessage(res, 'Failed to update labels.');
console.error('Failed to update auth key labels:', res.status, res.statusText, editor.error);
return;
}
} else if (res.status === 401) {
this.authError = true;
this.needsAuth = true;
editor.error = 'Authentication required.';
return;
} else if (res.status !== 200) {
editor.error = await this._authKeyResponseMessage(res, 'Failed to update labels.');
console.error('Failed to update auth key labels:', res.status, res.statusText, editor.error);
return;
}
await this.fetchAuthKeys();
const notice = 'Labels updated for key "' + editor.name + '".';
editor.submitting = false;
this.closeAuthKeyLabelsEditor();
this.authKeyNotice = notice;
} catch (e) {
console.error('Failed to update auth key labels:', e);
editor.error = 'Failed to update labels.';
} finally {
editor.submitting = false;
}
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/admin/dashboard/static/js/modules/auth-keys.js` around lines 281 -
370, In submitAuthKeyLabelsEditor, the current ordering leaves a redundant
editor.submitting reset after closeAuthKeyLabelsEditor replaces
authKeyLabelsEditor with a new object. Simplify the submit/close flow by
clearing submitting in the active editor state only once, then closing the
editor, and avoid mutating the stale local editor reference in the finally
block. Keep the existing behavior in openAuthKeyLabelsEditor,
closeAuthKeyLabelsEditor, and submitAuthKeyLabelsEditor unchanged aside from
this cleanup.

Comment thread internal/authkeys/store_mongodb.go
Comment thread internal/core/labels.go
Comment on lines +8 to +30
// MergeLabels combines label sets in order into one list, trimming whitespace
// and dropping empty values and duplicates. Returns nil when nothing remains.
func MergeLabels(sets ...[]string) []string {
var merged []string
var seen map[string]struct{}
for _, set := range sets {
for _, label := range set {
label = strings.TrimSpace(label)
if label == "" {
continue
}
if seen == nil {
seen = make(map[string]struct{})
}
if _, dup := seen[label]; dup {
continue
}
seen[label] = struct{}{}
merged = append(merged, label)
}
}
return merged
}

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.

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider bounding label length/count.

MergeLabels trims and dedups but places no limit on label length, count, or character set. Since labels now flow into audit log entries, request-log filters, and usage-by-label breakdowns (per PR objective), unbounded free-text labels create risk of PII leakage into logs, log/query bloat, or abuse via very large label sets on a single key. Consider enforcing a reasonable max length per label and a max count per key here (the single normalization chokepoint used by both Create and UpdateLabels).

🛡️ Example bound
+const (
+	maxLabelLength = 64
+	maxLabelCount  = 20
+)
+
 func MergeLabels(sets ...[]string) []string {
 	var merged []string
 	var seen map[string]struct{}
 	for _, set := range sets {
 		for _, label := range set {
 			label = strings.TrimSpace(label)
 			if label == "" {
 				continue
 			}
+			if len(label) > maxLabelLength {
+				label = label[:maxLabelLength]
+			}
 			if seen == nil {
 				seen = make(map[string]struct{})
 			}
 			if _, dup := seen[label]; dup {
 				continue
 			}
 			seen[label] = struct{}{}
 			merged = append(merged, label)
+			if len(merged) >= maxLabelCount {
+				return merged
+			}
 		}
 	}
 	return merged
 }
📝 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
// MergeLabels combines label sets in order into one list, trimming whitespace
// and dropping empty values and duplicates. Returns nil when nothing remains.
func MergeLabels(sets ...[]string) []string {
var merged []string
var seen map[string]struct{}
for _, set := range sets {
for _, label := range set {
label = strings.TrimSpace(label)
if label == "" {
continue
}
if seen == nil {
seen = make(map[string]struct{})
}
if _, dup := seen[label]; dup {
continue
}
seen[label] = struct{}{}
merged = append(merged, label)
}
}
return merged
}
const (
maxLabelLength = 64
maxLabelCount = 20
)
// MergeLabels combines label sets in order into one list, trimming whitespace
// and dropping empty values and duplicates. Returns nil when nothing remains.
func MergeLabels(sets ...[]string) []string {
var merged []string
var seen map[string]struct{}
for _, set := range sets {
for _, label := range set {
label = strings.TrimSpace(label)
if label == "" {
continue
}
if len(label) > maxLabelLength {
label = label[:maxLabelLength]
}
if seen == nil {
seen = make(map[string]struct{})
}
if _, dup := seen[label]; dup {
continue
}
seen[label] = struct{}{}
merged = append(merged, label)
if len(merged) >= maxLabelCount {
return merged
}
}
}
return merged
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/labels.go` around lines 8 - 30, `MergeLabels` currently only
trims and deduplicates, but it should also enforce safe bounds before labels
reach audit logs and usage queries. Update the normalization chokepoint in
`internal/core/labels.go` so `MergeLabels` rejects or truncates overly long
labels, caps the number of labels per key, and optionally constrains allowed
characters; keep the existing trim/dedupe behavior and ensure both `Create` and
`UpdateLabels` inherit the new limits through this function.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

Safe to merge with low risk.

The label flow is normalized, persisted, merged into request context, and covered by focused tests across service, admin handler, middleware, storage, and dashboard code.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • Ran the JavaScript behavior proof and confirmed the dashboard auth-key labels test suite passed.
  • Verified the real UI by reviewing the before and after proofs, showing the API Keys page before and after adding labels controls.
  • Captured and saved server and Playwright execution logs for both UI runs to support traceability.
  • Reviewed the full artifact set (logs, videos, and images) to support the validation work and inspect the UI changes.

View all artifacts

T-Rex Ran code and verified through T-Rex

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
  participant Admin as Admin API/Dashboard
  participant Svc as Auth Key Service
  participant Store as Auth Key Store
  participant Auth as Auth Middleware
  participant Ctx as Request Context
  participant Usage as Usage/Audit Recorders

  Admin->>Svc: Create key or PUT /auth-keys/:id/labels
  Svc->>Svc: Normalize + de-duplicate labels
  Svc->>Store: Persist labels
  Svc->>Svc: Patch in-memory snapshot
  Auth->>Svc: Authenticate Bearer token
  Svc-->>Auth: Key ID, user_path, labels
  Auth->>Ctx: Merge key labels with header labels
  Ctx-->>Usage: Request labels
  Usage->>Usage: Store usage labels and audit data.labels
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
  participant Admin as Admin API/Dashboard
  participant Svc as Auth Key Service
  participant Store as Auth Key Store
  participant Auth as Auth Middleware
  participant Ctx as Request Context
  participant Usage as Usage/Audit Recorders

  Admin->>Svc: Create key or PUT /auth-keys/:id/labels
  Svc->>Svc: Normalize + de-duplicate labels
  Svc->>Store: Persist labels
  Svc->>Svc: Patch in-memory snapshot
  Auth->>Svc: Authenticate Bearer token
  Svc-->>Auth: Key ID, user_path, labels
  Auth->>Ctx: Merge key labels with header labels
  Ctx-->>Usage: Request labels
  Usage->>Usage: Store usage labels and audit data.labels
Loading

Reviews (1): Last reviewed commit: "fix(dashboard): swap mobile theme toggle..." | Re-trigger Greptile

Keeps cleared keys shaped like keys created without labels, whose
insert path omits the field via omitempty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@SantiagoDePolonia SantiagoDePolonia merged commit 3c817e8 into main Jul 4, 2026
20 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.

2 participants