Skip to content

feat: add client (CLI/IDE) restrictions for user management#341

Merged
ding113 merged 4 commits intoding113:devfrom
miraserver:feat/allowed-clients
Dec 14, 2025
Merged

feat: add client (CLI/IDE) restrictions for user management#341
ding113 merged 4 commits intoding113:devfrom
miraserver:feat/allowed-clients

Conversation

@miraserver
Copy link
Contributor

@miraserver miraserver commented Dec 13, 2025

Summary

Add functionality to restrict which API clients (CLI tools/IDEs) can use a user's API keys based on User-Agent header matching.

Related PRs:

Features

  • New allowedClients field on users table (JSONB array)
  • ProxyClientGuard validates User-Agent in proxy pipeline (runs immediately after auth)
  • Admin UI with preset client checkboxes + custom pattern input
  • Display allowed clients info visible to both admin and user

Preset Client Patterns

Pattern Client
claude-cli Claude Code CLI
gemini-cli Gemini CLI
factory-cli Droid CLI (Factory AI)
codex-cli Codex CLI

Behavior

  • Empty array = no restrictions (all clients allowed)
  • Non-empty array = only listed patterns allowed (case-insensitive substring match)
  • Missing/empty User-Agent with restrictions configured → 400 error
  • Non-matching User-Agent with restrictions configured → 400 error

Files Changed (18 files)

  • Database: schema.ts + migration 0035_add_allowed_clients.sql
  • Types: user.ts (User, CreateUserData, UpdateUserData, UserDisplay)
  • Repository: user.ts, key.ts (validateApiKeyAndGetUser fix)
  • Validation: schemas.ts (Zod schema: max 50 patterns, 64 chars each)
  • Permissions: user-field-permissions.ts (admin-only)
  • Proxy: new client-guard.ts, guard-pipeline.ts integration
  • UI: user-form.tsx, key-list-header.tsx
  • i18n: all 5 locales (en, zh-CN, zh-TW, ja, ru)

Test Plan

  • Admin can add preset clients via checkboxes
  • Admin can add custom client patterns
  • API rejects requests with wrong User-Agent when restrictions exist (400)
  • API allows requests when no restrictions configured
  • Both admin and user see restrictions on dashboard

🤖 Generated with Claude Code

Description enhanced by Claude AI

github-actions bot and others added 2 commits December 13, 2025 16:42
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add functionality to restrict which API clients (CLI tools/IDEs) can use
a user's API keys based on User-Agent header matching.

Features:
- New `allowedClients` field on users table (JSONB array)
- ProxyClientGuard validates User-Agent in proxy pipeline (after auth)
- Admin UI with preset client checkboxes + custom pattern input
- Display allowed clients info visible to both admin and user

Preset client patterns:
- claude-cli: Claude Code CLI
- gemini-cli: Gemini CLI
- factory-cli: Droid CLI (Factory AI)
- codex-cli: Codex CLI

Behavior:
- Empty array = no restrictions (all clients allowed)
- Non-empty array = only listed patterns allowed (case-insensitive)
- Missing/empty User-Agent with restrictions → 400 error
- Non-matching User-Agent with restrictions → 400 error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @miraserver, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances security by introducing a client restriction mechanism for user API keys. It allows administrators to specify which CLI tools or IDEs are permitted to use a user's API keys by validating the User-Agent header of incoming requests. This feature provides granular control over API access, preventing unauthorized usage from unapproved client applications.

Highlights

  • Client Restriction Feature: Introduced the ability to restrict API key usage to specific CLI/IDE clients based on User-Agent headers.
  • Database & API Changes: Added a new 'allowedClients' JSONB array field to the 'users' table and updated user management APIs to support this new field.
  • Proxy Pipeline Integration: Implemented a 'ProxyClientGuard' that validates the User-Agent header in the proxy pipeline, immediately after authentication, to enforce client restrictions.
  • Admin & User UI: Provided an administrative interface for configuring allowed clients with preset options and custom patterns, and made this information visible to both administrators and users on the dashboard.
  • Restriction Logic: Defined clear behavior for restrictions: an empty list means no restrictions, a non-empty list requires a matching User-Agent (case-insensitive substring match), and missing or non-matching User-Agents result in a 400 error.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added enhancement New feature or request size/S Small PR (< 200 lines) labels Dec 13, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a valuable feature for restricting API client access based on the User-Agent. The implementation is comprehensive, covering the database schema, backend logic, and a well-designed UI for configuration. The code is generally well-structured and follows good practices. I've included a few specific comments with suggestions to enhance maintainability, readability, and performance in the new components. Overall, this is a solid and well-executed feature addition.

Comment on lines +345 to +411
{/* Allowed Clients (CLI/IDE restrictions) */}
<div className="space-y-3">
<div className="space-y-0.5">
<Label className="text-sm font-medium">{tForm("allowedClients.label")}</Label>
<p className="text-xs text-muted-foreground">{tForm("allowedClients.description")}</p>
</div>

{/* Preset client checkboxes */}
<div className="grid grid-cols-2 gap-2">
{PRESET_CLIENTS.map((client) => {
const isChecked = (form.values.allowedClients || []).includes(client.value);
return (
<div key={client.value} className="flex items-center space-x-2">
<Checkbox
id={`client-${client.value}`}
checked={isChecked}
onCheckedChange={(checked) => {
const currentClients = form.values.allowedClients || [];
if (checked) {
form.setValue("allowedClients", [...currentClients, client.value]);
} else {
form.setValue(
"allowedClients",
currentClients.filter((c: string) => c !== client.value)
);
}
}}
/>
<Label
htmlFor={`client-${client.value}`}
className="text-sm font-normal cursor-pointer"
>
{client.label}
</Label>
</div>
);
})}
</div>

{/* Custom client patterns */}
<ArrayTagInputField
label={tForm("allowedClients.customLabel")}
maxTagLength={64}
maxTags={50}
placeholder={tForm("allowedClients.customPlaceholder")}
onInvalidTag={(_tag, reason) => {
const messages: Record<string, string> = {
empty: tUI("emptyTag"),
duplicate: tUI("duplicateTag"),
too_long: tUI("tooLong", { max: 64 }),
invalid_format: tUI("invalidFormat"),
max_tags: tUI("maxTags"),
};
toast.error(messages[reason] || reason);
}}
value={(form.values.allowedClients || []).filter(
(c: string) => !PRESET_CLIENTS.some((p) => p.value === c)
)}
onChange={(customClients: string[]) => {
// Merge preset clients with custom clients
const presetClients = (form.values.allowedClients || []).filter((c: string) =>
PRESET_CLIENTS.some((p) => p.value === c)
);
form.setValue("allowedClients", [...presetClients, ...customClients]);
}}
/>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The current implementation for managing preset and custom clients is functional but a bit complex, with filtering logic repeated in multiple places. This can be simplified for better readability and maintainability.

I suggest two improvements:

  1. Use useMemo to derive presetClients and customClients from form.values.allowedClients once per render. This avoids repeated calculations in your JSX. You can add this hook before the component's return statement:
    const { presetClients, customClients } = useMemo(() => {
      const allClients = form.values.allowedClients || [];
      const presetClientValues = new Set(PRESET_CLIENTS.map((p) => p.value));
      
      const presetClients = allClients.filter(client => presetClientValues.has(client));
      const customClients = allClients.filter(client => !presetClientValues.has(client));
    
      return { presetClients, customClients };
    }, [form.values.allowedClients]);
  2. Use a Set in the checkbox onCheckedChange handler to make adding and removing clients more efficient and the code more declarative.

With these changes, the ArrayTagInputField's value would become customClients and its onChange would be (newCustomClients) => form.setValue("allowedClients", [...presetClients, ...newCustomClients]). The checkbox handler would also be simplified.

Comment on lines +272 to +286
{/* Allowed Clients Display - on separate line, visible to both admin and user */}
{activeUser && (
<div className="mt-2 px-2 py-1 text-xs text-muted-foreground border border-muted-foreground/30 rounded-md w-fit">
<span>
{activeUser.allowedClients?.length
? `${t("allowedClients.label")} [${activeUser.allowedClients.length}]:`
: t("allowedClients.noRestrictions")}
</span>
{activeUser.allowedClients && activeUser.allowedClients.length > 0 && (
<span className="text-foreground ml-1">
{activeUser.allowedClients.join(", ")}
</span>
)}
</div>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The JSX for displaying the allowed clients can be made more concise and readable. The current implementation has a slightly redundant check and separates the label from the list. Refactoring this into a single conditional block improves clarity.

Suggested change
{/* Allowed Clients Display - on separate line, visible to both admin and user */}
{activeUser && (
<div className="mt-2 px-2 py-1 text-xs text-muted-foreground border border-muted-foreground/30 rounded-md w-fit">
<span>
{activeUser.allowedClients?.length
? `${t("allowedClients.label")} [${activeUser.allowedClients.length}]:`
: t("allowedClients.noRestrictions")}
</span>
{activeUser.allowedClients && activeUser.allowedClients.length > 0 && (
<span className="text-foreground ml-1">
{activeUser.allowedClients.join(", ")}
</span>
)}
</div>
)}
{/* Allowed Clients Display - on separate line, visible to both admin and user */}
{activeUser && (
<div className="mt-2 px-2 py-1 text-xs text-muted-foreground border border-muted-foreground/30 rounded-md w-fit">
{activeUser.allowedClients?.length > 0 ? (
<>
{`${t("allowedClients.label")} [${activeUser.allowedClients.length}]: `}
<span className="text-foreground">
{activeUser.allowedClients.join(", ")}
</span>
</>
) : (
t("allowedClients.noRestrictions")
)}
</div>
)}

Comment on lines +47 to +50
const userAgentLower = userAgent.toLowerCase();
const isAllowed = allowedClients.some((pattern) =>
userAgentLower.includes(pattern.toLowerCase())
);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To improve performance slightly, you can avoid calling pattern.toLowerCase() inside the some loop. Since the allowedClients array can have up to 50 items, it's better to map them to lowercase once before the loop.

Suggested change
const userAgentLower = userAgent.toLowerCase();
const isAllowed = allowedClients.some((pattern) =>
userAgentLower.includes(pattern.toLowerCase())
);
const userAgentLower = userAgent.toLowerCase();
const allowedClientsLower = allowedClients.map((p) => p.toLowerCase());
const isAllowed = allowedClientsLower.some((pattern) =>
userAgentLower.includes(pattern)
);

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

No significant issues identified in this PR. The implementation adds a well-designed client restriction feature that follows existing patterns in the codebase.

PR Size: S

  • Lines changed: 294 (293 additions, 1 deletion)
  • Files changed: 18 (primarily i18n translations + core implementation)

Implementation Highlights

  • ProxyClientGuard: Clean implementation with proper early-exit when no restrictions configured
  • Guard Pipeline Integration: Correctly placed after auth, ensuring user context is available
  • Type Safety: Proper TypeScript types added across User, CreateUserData, UpdateUserData, and UserDisplay interfaces
  • Validation: Zod schema properly limits patterns (max 50 patterns, 64 chars each)
  • Database: Simple migration adding JSONB column with sensible default

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean (case-insensitive substring matching is intentional and documented)
  • Error handling - Clean (proper 400 responses with descriptive messages)
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - No new tests added, but feature is low-risk with simple logic
  • Code clarity - Good

Notes

  • The substring matching approach (e.g., claude-cli matches claude-cli/2.0.50) is appropriate for User-Agent validation
  • Empty allowedClients array correctly bypasses all checks (fail-open for unconfigured users)
  • The guard runs immediately after auth, which is the correct position in the pipeline

Automated review by Claude AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

No significant issues identified in this PR. The implementation adds a well-designed client restriction feature that follows existing patterns in the codebase.

PR Size: S

  • Lines changed: 294 (293 additions, 1 deletion)
  • Files changed: 18 (primarily i18n translations + core implementation)

Implementation Highlights

  • ProxyClientGuard: Clean implementation with proper early-exit when no restrictions configured
  • Guard Pipeline Integration: Correctly placed after auth, ensuring user context is available
  • Type Safety: Proper TypeScript types added across User, CreateUserData, UpdateUserData, and UserDisplay interfaces
  • Validation: Zod schema properly limits patterns (max 50 patterns, 64 chars each)
  • Database: Simple migration adding JSONB column with sensible default

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean (case-insensitive substring matching is intentional and documented)
  • Error handling - Clean (proper 400 responses with descriptive messages)
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - No new tests added, but feature is low-risk with simple logic
  • Code clarity - Good

Notes

  • The substring matching approach (e.g., `claude-cli` matches `claude-cli/2.0.50`) is appropriate for User-Agent validation
  • Empty `allowedClients` array correctly bypasses all checks (fail-open for unconfigured users)
  • The guard runs immediately after auth, which is the correct position in the pipeline

Automated review by Claude AI

@ding113 ding113 changed the base branch from main to dev December 14, 2025 13:07
@ding113
Copy link
Owner

ding113 commented Dec 14, 2025

It looks like this PR did not use the bun run db:generate command correctly to generate the database migration file, which may cause conflicts in future development.

Please resolve this issue before merging.

@miraserver
Copy link
Contributor Author

It looks like this PR did not use the bun run db:generate command correctly to generate the database migration file, which may cause conflicts in future development.

Please resolve this issue before merging.

not sure what i understand correctly, here is drizzle/0035_add_allowed_clients.sql migration file. Need some other?

@ding113
Copy link
Owner

ding113 commented Dec 14, 2025

It looks like this PR did not use the bun run db:generate command correctly to generate the database migration file, which may cause conflicts in future development.
Please resolve this issue before merging.

not sure what i understand correctly, here is drizzle/0035_add_allowed_clients.sql migration file. Need some other?

After correctly running the command, a snapshot file and a journal file will be automatically generated. This is necessary for subsequent development.

@miraserver
Copy link
Contributor Author

Migration Fix

Fixed the database migration files by using proper Drizzle Kit workflow.

What was done:

  1. Removed manually created SQL file: drizzle/0035_add_allowed_clients.sql
  2. Regenerated migration using bun run db:generate
  3. Created all required files:
    • ✅ SQL migration: drizzle/0035_blushing_fabian_cortez.sql
    • ✅ Snapshot JSON: drizzle/meta/0035_snapshot.json
    • ✅ Updated journal: drizzle/meta/_journal.json (added entry idx=35)

Validation:

  • ✅ All 36 migration files passed idempotency check
  • ✅ Proper Drizzle Kit auto-generation workflow used
  • ✅ Ready for merge into dev

Commit: fix: regenerate allowed_clients migration with proper Drizzle Kit workflow

@ding113 The migration structure is now correct as requested. All three file types (SQL, snapshot, journal) are properly generated.

@ding113 ding113 merged commit 8d046cf into ding113:dev Dec 14, 2025
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Dec 14, 2025
@miraserver miraserver deleted the feat/allowed-clients branch December 20, 2025 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size/S Small PR (< 200 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants