chore(api): remove unused admin/readonly API key roles (#16)
This commit is contained in:
parent
50fef48ebe
commit
21e159ffd5
@ -1359,13 +1359,14 @@ function generateApiKey(): string {
|
|||||||
return 'ogk_' + crypto.randomUUID().replace(/-/g, '')
|
return 'ogk_' + crypto.randomUUID().replace(/-/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only 'ingest' role is currently implemented. May expand in the future.
|
||||||
export async function createApiKey(
|
export async function createApiKey(
|
||||||
db: D1Database,
|
db: D1Database,
|
||||||
name: string,
|
name: string,
|
||||||
role: 'admin' | 'ingest' | 'readonly' = 'ingest',
|
|
||||||
allowedEvents: string[] = [],
|
allowedEvents: string[] = [],
|
||||||
rateLimit: number = 100,
|
rateLimit: number = 100,
|
||||||
): Promise<CreateApiKeyResponse> {
|
): Promise<CreateApiKeyResponse> {
|
||||||
|
const role = 'ingest'
|
||||||
const key = generateApiKey()
|
const key = generateApiKey()
|
||||||
const keyHash = await sha256(key)
|
const keyHash = await sha256(key)
|
||||||
const createdAt = Date.now()
|
const createdAt = Date.now()
|
||||||
@ -1416,7 +1417,7 @@ export async function listApiKeys(
|
|||||||
const apiKeys: ApiKey[] = (rows.results || []).map((row) => ({
|
const apiKeys: ApiKey[] = (rows.results || []).map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
role: row.role as 'admin' | 'ingest' | 'readonly',
|
role: 'ingest' as const,
|
||||||
allowed_events: JSON.parse(row.allowed_events),
|
allowed_events: JSON.parse(row.allowed_events),
|
||||||
rate_limit: row.rate_limit,
|
rate_limit: row.rate_limit,
|
||||||
last_used_at: row.last_used_at || undefined,
|
last_used_at: row.last_used_at || undefined,
|
||||||
@ -1457,7 +1458,7 @@ export async function validateApiKey(
|
|||||||
const apiKey: ApiKey = {
|
const apiKey: ApiKey = {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
role: row.role as 'admin' | 'ingest' | 'readonly',
|
role: 'ingest' as const,
|
||||||
allowed_events: JSON.parse(row.allowed_events),
|
allowed_events: JSON.parse(row.allowed_events),
|
||||||
rate_limit: row.rate_limit,
|
rate_limit: row.rate_limit,
|
||||||
last_used_at: row.last_used_at || undefined,
|
last_used_at: row.last_used_at || undefined,
|
||||||
|
|||||||
@ -2071,7 +2071,7 @@ describe('API Key Management', () => {
|
|||||||
|
|
||||||
it('POST /events with valid API key and correct event type → 201', async () => {
|
it('POST /events with valid API key and correct event type → 201', async () => {
|
||||||
const keyRes = await app.fetch(
|
const keyRes = await app.fetch(
|
||||||
req('POST', '/api-keys', { name: 'ingest-key', role: 'ingest', allowed_events: ['task_assigned'] }),
|
req('POST', '/api-keys', { name: 'ingest-key', allowed_events: ['task_assigned'] }),
|
||||||
{ DB: db, API_TOKEN },
|
{ DB: db, API_TOKEN },
|
||||||
)
|
)
|
||||||
const { key } = await keyRes.json()
|
const { key } = await keyRes.json()
|
||||||
@ -2108,7 +2108,7 @@ describe('API Key Management', () => {
|
|||||||
|
|
||||||
it('POST /events with deleted key → 401', async () => {
|
it('POST /events with deleted key → 401', async () => {
|
||||||
const keyRes = await app.fetch(
|
const keyRes = await app.fetch(
|
||||||
req('POST', '/api-keys', { name: 'temp-key', role: 'ingest', allowed_events: ['task_assigned'] }),
|
req('POST', '/api-keys', { name: 'temp-key', allowed_events: ['task_assigned'] }),
|
||||||
{ DB: db, API_TOKEN },
|
{ DB: db, API_TOKEN },
|
||||||
)
|
)
|
||||||
const { id, key } = await keyRes.json()
|
const { id, key } = await keyRes.json()
|
||||||
@ -2124,7 +2124,7 @@ describe('API Key Management', () => {
|
|||||||
|
|
||||||
it('POST /events with valid key but wrong event type → 403', async () => {
|
it('POST /events with valid key but wrong event type → 403', async () => {
|
||||||
const keyRes = await app.fetch(
|
const keyRes = await app.fetch(
|
||||||
req('POST', '/api-keys', { name: 'limited-key', role: 'ingest', allowed_events: ['other_event'] }),
|
req('POST', '/api-keys', { name: 'limited-key', allowed_events: ['other_event'] }),
|
||||||
{ DB: db, API_TOKEN },
|
{ DB: db, API_TOKEN },
|
||||||
)
|
)
|
||||||
const { key } = await keyRes.json()
|
const { key } = await keyRes.json()
|
||||||
@ -2138,7 +2138,7 @@ describe('API Key Management', () => {
|
|||||||
|
|
||||||
it('empty allowed_events means no events allowed for ingest role', async () => {
|
it('empty allowed_events means no events allowed for ingest role', async () => {
|
||||||
const keyRes = await app.fetch(
|
const keyRes = await app.fetch(
|
||||||
req('POST', '/api-keys', { name: 'empty-key', role: 'ingest', allowed_events: [] }),
|
req('POST', '/api-keys', { name: 'empty-key', allowed_events: [] }),
|
||||||
{ DB: db, API_TOKEN },
|
{ DB: db, API_TOKEN },
|
||||||
)
|
)
|
||||||
const { key } = await keyRes.json()
|
const { key } = await keyRes.json()
|
||||||
@ -2158,8 +2158,9 @@ describe('API Key Management', () => {
|
|||||||
expect(res.status).toBe(201)
|
expect(res.status).toBe(201)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('admin role API key bypasses event type check', async () => {
|
it('admin role API key is treated as ingest (admin role not implemented)', async () => {
|
||||||
const keyRes = await app.fetch(req('POST', '/api-keys', { name: 'admin-key', role: 'admin' }), {
|
// Even if someone passes role=admin, it's ignored — always ingest
|
||||||
|
const keyRes = await app.fetch(req('POST', '/api-keys', { name: 'admin-key', allowed_events: ['task_assigned'] }), {
|
||||||
DB: db,
|
DB: db,
|
||||||
API_TOKEN,
|
API_TOKEN,
|
||||||
})
|
})
|
||||||
@ -2171,6 +2172,23 @@ describe('API Key Management', () => {
|
|||||||
)
|
)
|
||||||
expect(res.status).toBe(201)
|
expect(res.status).toBe(201)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('creating API key without role defaults to ingest', async () => {
|
||||||
|
const res = await app.fetch(req('POST', '/api-keys', { name: 'no-role-key' }), { DB: db, API_TOKEN })
|
||||||
|
expect(res.status).toBe(201)
|
||||||
|
const json = await res.json()
|
||||||
|
expect(json.role).toBe('ingest')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('any explicitly passed role value is ignored, always ingest', async () => {
|
||||||
|
const res = await app.fetch(
|
||||||
|
req('POST', '/api-keys', { name: 'readonly-attempt', role: 'readonly' as any }),
|
||||||
|
{ DB: db, API_TOKEN },
|
||||||
|
)
|
||||||
|
expect(res.status).toBe(201)
|
||||||
|
const json = await res.json()
|
||||||
|
expect(json.role).toBe('ingest')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -2231,7 +2249,7 @@ describe('Request Logs', () => {
|
|||||||
|
|
||||||
it('request log includes api_key_name for API key auth', async () => {
|
it('request log includes api_key_name for API key auth', async () => {
|
||||||
const keyRes = await app.fetch(
|
const keyRes = await app.fetch(
|
||||||
req('POST', '/api-keys', { name: 'log-test-key', role: 'ingest', allowed_events: ['task_assigned'] }),
|
req('POST', '/api-keys', { name: 'log-test-key', allowed_events: ['task_assigned'] }),
|
||||||
{ DB: db, API_TOKEN },
|
{ DB: db, API_TOKEN },
|
||||||
)
|
)
|
||||||
const { key } = await keyRes.json()
|
const { key } = await keyRes.json()
|
||||||
@ -2255,7 +2273,7 @@ describe('Request Logs', () => {
|
|||||||
|
|
||||||
it('GET /request-logs supports api_key_id filter', async () => {
|
it('GET /request-logs supports api_key_id filter', async () => {
|
||||||
const keyRes = await app.fetch(
|
const keyRes = await app.fetch(
|
||||||
req('POST', '/api-keys', { name: 'filter-key', role: 'ingest', allowed_events: ['task_assigned'] }),
|
req('POST', '/api-keys', { name: 'filter-key', allowed_events: ['task_assigned'] }),
|
||||||
{ DB: db, API_TOKEN },
|
{ DB: db, API_TOKEN },
|
||||||
)
|
)
|
||||||
const { key, id: keyId } = await keyRes.json()
|
const { key, id: keyId } = await keyRes.json()
|
||||||
|
|||||||
@ -481,7 +481,8 @@ app.post('/api-keys', async (c) => {
|
|||||||
try {
|
try {
|
||||||
const body = await c.req.json<CreateApiKeyRequest>()
|
const body = await c.req.json<CreateApiKeyRequest>()
|
||||||
if (!body.name) return apiError(c, 400, ErrorCode.MISSING_FIELD, 'Missing name')
|
if (!body.name) return apiError(c, 400, ErrorCode.MISSING_FIELD, 'Missing name')
|
||||||
const result = await createApiKey(c.env.DB, body.name, body.role, body.allowed_events, body.rate_limit)
|
// Role is always 'ingest' — admin/readonly not implemented
|
||||||
|
const result = await createApiKey(c.env.DB, body.name, body.allowed_events, body.rate_limit)
|
||||||
return c.json(result, 201)
|
return c.json(result, 201)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return apiError(c, 500, ErrorCode.INTERNAL_ERROR, err.message || 'Internal error')
|
return apiError(c, 500, ErrorCode.INTERNAL_ERROR, err.message || 'Internal error')
|
||||||
|
|||||||
@ -181,10 +181,11 @@ export interface ReactionLog {
|
|||||||
// API Key Types
|
// API Key Types
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
|
// Only 'ingest' role is currently implemented. May expand in the future.
|
||||||
export interface ApiKey {
|
export interface ApiKey {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
role: 'admin' | 'ingest' | 'readonly'
|
role: 'ingest'
|
||||||
allowed_events: string[]
|
allowed_events: string[]
|
||||||
rate_limit: number
|
rate_limit: number
|
||||||
last_used_at?: number
|
last_used_at?: number
|
||||||
@ -193,7 +194,6 @@ export interface ApiKey {
|
|||||||
|
|
||||||
export interface CreateApiKeyRequest {
|
export interface CreateApiKeyRequest {
|
||||||
name: string
|
name: string
|
||||||
role?: 'admin' | 'ingest' | 'readonly'
|
|
||||||
allowed_events?: string[]
|
allowed_events?: string[]
|
||||||
rate_limit?: number
|
rate_limit?: number
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -164,9 +164,6 @@ export default function ApiKeys() {
|
|||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
||||||
Name
|
Name
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
|
||||||
Role
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
||||||
Allowed Events
|
Allowed Events
|
||||||
</th>
|
</th>
|
||||||
@ -192,11 +189,6 @@ export default function ApiKeys() {
|
|||||||
>
|
>
|
||||||
<td className="px-4 py-3 font-mono text-sm text-gray-300">{key.id}</td>
|
<td className="px-4 py-3 font-mono text-sm text-gray-300">{key.id}</td>
|
||||||
<td className="px-4 py-3 text-sm text-gray-200 font-medium">{key.name}</td>
|
<td className="px-4 py-3 text-sm text-gray-200 font-medium">{key.name}</td>
|
||||||
<td className="px-4 py-3">
|
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-500/20 text-purple-300 border border-purple-500/30">
|
|
||||||
{key.role || 'default'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{key.allowed_events && key.allowed_events.length > 0 ? (
|
{key.allowed_events && key.allowed_events.length > 0 ? (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user