Merge pull request #814 from Guccbai/feature/multi-select-permissions [skip ci]

feat(permissions): 服务权限从单选改为多选
This commit is contained in:
Wesley Liddick
2025-12-26 00:52:42 -05:00
committed by GitHub
10 changed files with 171 additions and 113 deletions

View File

@@ -8,6 +8,43 @@ const config = require('../../../config/config')
const router = express.Router()
// 有效的权限值列表
const VALID_PERMISSIONS = ['claude', 'gemini', 'openai', 'droid']
/**
* 验证权限数组格式
* @param {any} permissions - 权限值(可以是数组或其他)
* @returns {string|null} - 返回错误消息null 表示验证通过
*/
function validatePermissions(permissions) {
// 空值或未定义表示全部服务
if (permissions === undefined || permissions === null || permissions === '') {
return null
}
// 兼容旧格式字符串
if (typeof permissions === 'string') {
if (permissions === 'all' || VALID_PERMISSIONS.includes(permissions)) {
return null
}
return `Invalid permissions value. Must be an array of: ${VALID_PERMISSIONS.join(', ')}`
}
// 新格式数组
if (Array.isArray(permissions)) {
// 空数组表示全部服务
if (permissions.length === 0) {
return null
}
// 验证数组中的每个值
for (const perm of permissions) {
if (!VALID_PERMISSIONS.includes(perm)) {
return `Invalid permission value "${perm}". Valid values are: ${VALID_PERMISSIONS.join(', ')}`
}
}
return null
}
return `Permissions must be an array. Valid values are: ${VALID_PERMISSIONS.join(', ')}`
}
// 👥 用户管理 (用于API Key分配)
// 获取所有用户列表用于API Key分配
@@ -1382,16 +1419,10 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
}
}
// 验证服务权限字段
if (
permissions !== undefined &&
permissions !== null &&
permissions !== '' &&
!['claude', 'gemini', 'openai', 'droid', 'all'].includes(permissions)
) {
return res.status(400).json({
error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all'
})
// 验证服务权限字段(支持数组格式)
const permissionsError = validatePermissions(permissions)
if (permissionsError) {
return res.status(400).json({ error: permissionsError })
}
const newKey = await apiKeyService.generateApiKey({
@@ -1481,15 +1512,10 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
.json({ error: 'Base name must be less than 90 characters to allow for numbering' })
}
if (
permissions !== undefined &&
permissions !== null &&
permissions !== '' &&
!['claude', 'gemini', 'openai', 'droid', 'all'].includes(permissions)
) {
return res.status(400).json({
error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all'
})
// 验证服务权限字段(支持数组格式)
const batchPermissionsError = validatePermissions(permissions)
if (batchPermissionsError) {
return res.status(400).json({ error: batchPermissionsError })
}
// 生成批量API Keys
@@ -1592,13 +1618,12 @@ router.put('/api-keys/batch', authenticateAdmin, async (req, res) => {
})
}
if (
updates.permissions !== undefined &&
!['claude', 'gemini', 'openai', 'droid', 'all'].includes(updates.permissions)
) {
return res.status(400).json({
error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all'
})
// 验证服务权限字段(支持数组格式)
if (updates.permissions !== undefined) {
const updatePermissionsError = validatePermissions(updates.permissions)
if (updatePermissionsError) {
return res.status(400).json({ error: updatePermissionsError })
}
}
logger.info(
@@ -1873,11 +1898,10 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
}
if (permissions !== undefined) {
// 验证权限值
if (!['claude', 'gemini', 'openai', 'droid', 'all'].includes(permissions)) {
return res.status(400).json({
error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all'
})
// 验证服务权限字段(支持数组格式)
const singlePermissionsError = validatePermissions(permissions)
if (singlePermissionsError) {
return res.status(400).json({ error: singlePermissionsError })
}
updates.permissions = permissions
}

View File

@@ -118,11 +118,7 @@ async function handleMessagesRequest(req, res) {
const startTime = Date.now()
// Claude 服务权限校验,阻止未授权的 Key
if (
req.apiKey.permissions &&
req.apiKey.permissions !== 'all' &&
req.apiKey.permissions !== 'claude'
) {
if (!apiKeyService.hasPermission(req.apiKey.permissions, 'claude')) {
return res.status(403).json({
error: {
type: 'permission_error',

View File

@@ -4,12 +4,12 @@ const { authenticateApiKey } = require('../middleware/auth')
const droidRelayService = require('../services/droidRelayService')
const sessionHelper = require('../utils/sessionHelper')
const logger = require('../utils/logger')
const apiKeyService = require('../services/apiKeyService')
const router = express.Router()
function hasDroidPermission(apiKeyData) {
const permissions = apiKeyData?.permissions || 'all'
return permissions === 'all' || permissions === 'droid'
return apiKeyService.hasPermission(apiKeyData?.permissions, 'droid')
}
/**

View File

@@ -6,6 +6,7 @@ const geminiAccountService = require('../services/geminiAccountService')
const unifiedGeminiScheduler = require('../services/unifiedGeminiScheduler')
const { getAvailableModels } = require('../services/geminiRelayService')
const crypto = require('crypto')
const apiKeyService = require('../services/apiKeyService')
// 生成会话哈希
function generateSessionHash(req) {
@@ -21,8 +22,7 @@ function generateSessionHash(req) {
// 检查 API Key 权限
function checkPermissions(apiKeyData, requiredPermission = 'gemini') {
const permissions = apiKeyData.permissions || 'all'
return permissions === 'all' || permissions === requiredPermission
return apiKeyService.hasPermission(apiKeyData?.permissions, requiredPermission)
}
// 转换 OpenAI 消息格式到 Gemini 格式
@@ -499,7 +499,6 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
// 记录使用统计
if (!usageReported && totalUsage.totalTokenCount > 0) {
try {
const apiKeyService = require('../services/apiKeyService')
await apiKeyService.recordUsage(
apiKeyData.id,
totalUsage.promptTokenCount || 0,
@@ -580,7 +579,6 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
// 记录使用统计
if (openaiResponse.usage) {
try {
const apiKeyService = require('../services/apiKeyService')
await apiKeyService.recordUsage(
apiKeyData.id,
openaiResponse.usage.prompt_tokens || 0,

View File

@@ -20,8 +20,7 @@ function createProxyAgent(proxy) {
// 检查 API Key 是否具备 OpenAI 权限
function checkOpenAIPermissions(apiKeyData) {
const permissions = apiKeyData?.permissions || 'all'
return permissions === 'all' || permissions === 'openai'
return apiKeyService.hasPermission(apiKeyData?.permissions, 'openai')
}
function normalizeHeaders(headers = {}) {

View File

@@ -8,6 +8,7 @@ const {
handleStreamGenerateContent: geminiHandleStreamGenerateContent
} = require('../handlers/geminiHandlers')
const openaiRoutes = require('./openaiRoutes')
const apiKeyService = require('../services/apiKeyService')
const router = express.Router()
@@ -73,7 +74,7 @@ async function routeToBackend(req, res, requestedModel) {
return await openaiRoutes.handleResponses(req, res)
} else if (backend === 'gemini') {
// Gemini 后端
if (permissions !== 'all' && permissions !== 'gemini') {
if (!apiKeyService.hasPermission(permissions, 'gemini')) {
return res.status(403).json({
error: {
message: 'This API key does not have permission to access Gemini',