Skip to content

feat(users): add allowed models restriction for users#347

Merged
ding113 merged 3 commits intoding113:devfrom
miraserver:feat/allowed-models
Dec 17, 2025
Merged

feat(users): add allowed models restriction for users#347
ding113 merged 3 commits intoding113:devfrom
miraserver:feat/allowed-models

Conversation

@miraserver
Copy link
Contributor

@miraserver miraserver commented Dec 15, 2025

Summary

Add functionality to restrict which AI models a user can access based on a per-user whitelist. This provides administrators with fine-grained control over model access at the user level.

Related PRs:

Problem

Administrators need to control which AI models specific users can access. For example:

  • Restricting high-cost models (like Opus) to senior developers
  • Limiting users to specific models for billing/compliance reasons
  • Preventing accidental use of experimental models

Solution

Implement allowedModels field on the users table with enforcement through a new ModelGuard in the proxy pipeline.

Features

  • New allowedModels field on users table (JSONB array)
  • ProxyModelGuard validates requested model in proxy pipeline (runs immediately after auth)
  • Admin UI with tag-style input for model names
  • Display allowed models info visible to both admin and user on dashboard
  • Field permission restricted to admin-only editing

Behavior

  • Empty array = no restrictions (all models allowed)
  • Non-empty array = only listed models allowed (case-insensitive exact match)
  • Missing or null model with restrictions configured → 400 error
  • Non-matching model with restrictions configured → 400 error

Changes

Core Changes

  • Database: src/drizzle/schema.ts + migration 0036_add_allowed_models.sql
  • Proxy Guard: src/app/v1/_lib/proxy/model-guard.ts (new)
  • Pipeline Integration: src/app/v1/_lib/proxy/guard-pipeline.ts

Supporting Changes

  • Types: src/types/user.ts (User, CreateUserData, UpdateUserData)
  • Repository: src/repository/user.ts, src/repository/key.ts
  • Validation: src/lib/validation/schemas.ts (Zod schema: max 50 models, 64 chars each)
  • Permissions: src/lib/permissions/user-field-permissions.ts (admin-only)
  • UI: src/app/[locale]/dashboard/_components/user/forms/user-form.tsx, key-list-header.tsx
  • i18n: all 5 locales (en, zh-CN, zh-TW, ja, ru)

Files Changed (21 files)

Category Files
Database drizzle/0036_add_allowed_models.sql, drizzle/meta/0036_snapshot.json, drizzle/meta/_journal.json
Types src/types/user.ts
Schema src/drizzle/schema.ts
Validation src/lib/validation/schemas.ts
Permissions src/lib/permissions/user-field-permissions.ts
Repository src/repository/user.ts, src/repository/key.ts, src/repository/_shared/transformers.ts
Proxy src/app/v1/_lib/proxy/model-guard.ts, src/app/v1/_lib/proxy/guard-pipeline.ts
Actions src/actions/users.ts
UI src/app/[locale]/dashboard/_components/user/forms/user-form.tsx, key-list-header.tsx
i18n messages/{en,zh-CN,zh-TW,ja,ru}/dashboard.json

Test Plan

  • Create a user with specific allowed models
  • Verify that the user can only access the specified models
  • Verify that requests for non-allowed models are rejected with 400 error
  • Verify that users with empty allowedModels (no restrictions) can access all models
  • Verify i18n strings display correctly in all languages
  • Verify that non-admin users cannot modify the allowedModels field

Checklist


Description enhanced by Claude AI

@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 introduces a significant feature that allows administrators to define specific AI model access restrictions for individual users. By integrating a new allowedModels field into the user profiles and implementing a ModelGuard in the request processing pipeline, the system can now precisely control which models each user is permitted to use. This enhancement improves security and resource management by preventing unauthorized model usage, while also providing a clear and localized interface for managing these restrictions.

Highlights

  • User Model Restrictions: Introduced a new allowedModels field to the users table, stored as a JSONB array, to restrict which AI models a user can access.
  • Proxy Pipeline Enforcement: Implemented a ModelGuard within the proxy pipeline to enforce these model restrictions, blocking requests for non-allowed models with a 400 error.
  • User Interface Updates: The user form now includes a tag-style UI for administrators to easily select and manage allowed models for each user. The key-list-header component also displays the allowed models or a 'No restrictions' message.
  • Internationalization Support: Added i18n translations for the new 'allowedModels' fields and related messages across all supported languages (English, Japanese, Russian, Simplified Chinese, Traditional Chinese).
  • Admin-Only Permissions: The allowedModels field is configured with admin-only editing permissions, ensuring that only authorized personnel can modify user model access.
  • Database Migration: A new database migration (0035_add_allowed_models.sql) was added to introduce the allowed_models column to the users table.
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.

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 new feature to restrict user access to specific AI models. The changes are comprehensive, spanning from a database migration to add the allowed_models field, to backend logic for enforcement via a new ModelGuard in the proxy pipeline, and frontend UI for configuration and display. The implementation is well-executed, including i18n translations and proper permission handling for the new field. My main feedback is to enforce a NOT NULL constraint on the new allowed_models database column to improve data integrity and consistency, which I've detailed in the specific comments.

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 ensure data integrity and prevent NULL values for the allowed_models column, it's recommended to add a NOT NULL constraint. This aligns with the intention of having a default empty array [] and simplifies handling in the application code, as you won't need to check for null values from the database.

ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "allowed_models" jsonb NOT NULL DEFAULT '[]'::jsonb;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This suggestion doesn't match the existing codebase patterns. The tags field in the same table uses the identical pattern without NOT NULL:

tags: jsonb('tags').$type<string[]>().default([]),

Consistency with existing code is preferred over this style change.

@@ -40,6 +40,10 @@ export const users = pgTable('users', {
isEnabled: boolean('is_enabled').notNull().default(true),
expiresAt: timestamp('expires_at', { withTimezone: true }),

// Allowed models (AI model restrictions)
// Empty array = no restrictions, non-empty = only listed models allowed
allowedModels: jsonb('allowed_models').$type<string[]>().default([]),
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 match the recommended NOT NULL constraint in the database migration, you should also update the Drizzle schema definition to include .notNull(). This ensures that the ORM is aware of the non-nullable constraint, providing better type safety and preventing accidental null insertions.

Suggested change
allowedModels: jsonb('allowed_models').$type<string[]>().default([]),
allowedModels: jsonb('allowed_models').$type<string[]>().notNull().default([]),

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same reasoning as above - consistency with existing codebase patterns. The tags field uses the same approach without .notNull().

@github-actions github-actions bot added size/M Medium PR (< 500 lines) size/L Large PR (< 1000 lines) and removed size/M Medium PR (< 500 lines) labels Dec 15, 2025
Copy link
Contributor

Choose a reason for hiding this comment

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

[Critical] [LOGIC-BUG] Model guard is never executed - feature is completely broken

Why this is a problem: The ProxyModelGuard class is imported into guard-pipeline.ts and a step definition is added to the Steps record, but the "model" step key is never added to either CHAT_PIPELINE or COUNT_TOKENS_PIPELINE configurations. This means the model restriction feature will never be enforced at runtime, even when users configure allowedModels.

Users who configure model restrictions will believe their API is protected, but requests with disallowed models will still be processed.

Suggested fix:

// In guard-pipeline.ts, add "model" to CHAT_PIPELINE after auth:
export const CHAT_PIPELINE: GuardConfig = {
  steps: [
    "auth",
    "model",  // <-- Add this line to enforce model restrictions
    "version",
    "probe",
    "session",
    "requestFilter",
    "sensitive",
    "rateLimit",
    "provider",
    "messageContext",
  ],
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a false positive. The "model" step IS added to both pipelines in this PR. Please check the actual diff:

  • CHAT_PIPELINE line 151: "model" added after "auth"
  • COUNT_TOKENS_PIPELINE line 165: "model" added after "auth"

The feature is fully functional.

Copy link
Contributor

Choose a reason for hiding this comment

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

[High] [TEST-MISSING-CRITICAL] No unit tests for ProxyModelGuard

Why this is a problem: The new ProxyModelGuard class has critical security logic for model restrictions. However, there are no tests to verify:

  • Empty/undefined allowedModels bypasses the check correctly
  • Non-empty allowedModels with matching model passes
  • Non-empty allowedModels with non-matching model is rejected
  • Case-insensitive matching works correctly
  • Empty/null model is rejected when restrictions exist

This is a security-sensitive feature that could silently fail without proper test coverage.

Suggested fix:

// src/app/v1/_lib/proxy/model-guard.test.ts
import { describe, it, expect, vi } from 'vitest';
import { ProxyModelGuard } from './model-guard';

describe('ProxyModelGuard', () => {
  it('should skip check when allowedModels is empty', async () => {
    const session = createMockSession({ allowedModels: [], model: 'any-model' });
    const result = await ProxyModelGuard.ensure(session);
    expect(result).toBeNull();
  });

  it('should reject model not in allowedModels', async () => {
    const session = createMockSession({ 
      allowedModels: ['claude-3-opus'], 
      model: 'gpt-4' 
    });
    const result = await ProxyModelGuard.ensure(session);
    expect(result?.status).toBe(400);
  });

  it('should allow case-insensitive matching', async () => {
    const session = createMockSession({ 
      allowedModels: ['Claude-3-Opus'], 
      model: 'claude-3-opus' 
    });
    const result = await ProxyModelGuard.ensure(session);
    expect(result).toBeNull();
  });
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged. Unit tests are desirable but the project doesn't have mandatory test coverage requirements for guard components. This can be addressed in a follow-up PR if the maintainers prefer.

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

This PR adds user-level model restrictions (allowedModels) feature, allowing admins to restrict which AI models a user can access. The implementation spans database schema, backend logic, UI forms, and localization. However, there is a critical bug that makes the entire feature non-functional.

PR Size: L (Large)

  • Lines changed: 2,153 (2,151 additions, 2 deletions)
  • Files changed: 20

Split suggestion: Consider separating the schema migration and i18n snapshot files from the core feature changes to reduce review burden.

Issues Found

Category Critical High Medium Low
Logic/Bugs 1 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 1 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

  1. [LOGIC-BUG] Model guard is never executed (src/app/v1/_lib/proxy/guard-pipeline.ts)

    The ProxyModelGuard class is created, imported, and registered in the Steps record, but the "model" step key is never added to the pipeline configurations (CHAT_PIPELINE or COUNT_TOKENS_PIPELINE). This means:

    • The model restriction feature will NEVER be enforced at runtime
    • Users who configure allowedModels will falsely believe they're protected
    • All models will be accepted regardless of configuration

    Fix: Add "model" step to the pipeline after "auth".

High Priority Issues (Should Fix)

  1. [TEST-MISSING-CRITICAL] No unit tests for ProxyModelGuard (src/app/v1/_lib/proxy/model-guard.ts)

    This security-sensitive feature has zero test coverage. The model guard logic includes case-insensitive matching and various edge cases that should be tested.

Review Coverage

  • Logic and correctness - Critical issue found
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Missing tests for core feature
  • Code clarity - Good

Automated review by Claude AI

@miraserver
Copy link
Contributor Author

Response to Code Review Comments

1. ✅ [CRITICAL BUG] Model guard is never executed - FALSE POSITIVE

The code review bot analyzed incorrectly. Looking at the actual PR diff:

// CHAT_PIPELINE - line 151
steps: [
  "auth",
+ "model",  // ✅ ADDED
  "version",
  ...
]

// COUNT_TOKENS_PIPELINE - line 165
- steps: ["auth", "version", "probe", "requestFilter", "provider"],
+ steps: ["auth", "model", "version", "probe", "requestFilter", "provider"],  // ✅ ADDED

The "model" step IS correctly added to both pipelines. The feature is fully functional.


2. ⏭️ [MEDIUM] NOT NULL constraint - Does not match codebase patterns

Checked existing code in src/drizzle/schema.ts:

// Existing pattern for JSONB arrays with defaults:
tags: jsonb('tags').$type<string[]>().default([]),  // no .notNull()

The codebase consistently uses JSONB arrays with default values without NOT NULL. This is a deliberate choice for backward compatibility. Adding NOT NULL would break consistency with existing patterns.


3. ⏭️ [HIGH] Unit tests - Nice-to-have, not blocking

Unit tests are desirable but the project doesn't require mandatory test coverage for guard components. Can be added in a follow-up PR if needed.


Summary

All critical issues raised are either false positives or style suggestions that don't match project conventions. The PR is ready for review and merge.


Response by PR author

- Add allowedModels field to users table (JSONB array)
- Add ModelGuard to proxy pipeline to enforce model restrictions
- Display allowed models in key-list-header component
- Add allowedModels input to user form with tag-style UI
- Add i18n translations for all supported languages (en, ja, ru, zh-CN, zh-TW)
- Add field permission for allowedModels (admin-only)
- Add database migration 0035_add_allowed_models.sql

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@ding113
Copy link
Owner

ding113 commented Dec 17, 2025

It looks like you modified the existing drizzle/meta/0035_snapshot.json file, which may cause merge conflicts for future development. Could you check whether this change is necessary?

- Restore original 0035_snapshot.json (allowed_clients)
- Regenerate 0036 migration using `bun run db:generate`
- Migration now properly generated, not manually edited

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@miraserver
Copy link
Contributor Author

Thanks for catching this! You're right - I had incorrectly modified the existing 0035_snapshot.json instead of generating a new migration properly.

Fixed in commit 8995def:

  • Restored original 0035_snapshot.json (for allowed_clients)
  • Deleted the manually-created 0036_add_allowed_models.sql
  • Regenerated migration using bun run db:generate
  • New migration: 0036_common_forge.sql with properly generated snapshot

The migration file and snapshot are now properly generated by Drizzle Kit, following the same workflow used for allowed_clients in #341.

@ding113 ding113 merged commit 340e2da into ding113:dev Dec 17, 2025
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Dec 17, 2025
@github-actions github-actions bot mentioned this pull request Dec 19, 2025
12 tasks
@miraserver miraserver deleted the feat/allowed-models 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

size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants

Comments