-
Notifications
You must be signed in to change notification settings - Fork 2
feat(connections): validate API keys before storing #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
a28b365
24e6c84
705abeb
7bdf6ab
f7a5130
97c8d52
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ export type ConnectionDeps = { | |
| states: StateStore | ||
| grants: GrantStore | ||
| enablement: EnablementStore | ||
| fetch?: typeof fetch | ||
| exchange?: typeof exchangeCode | ||
| } | ||
|
|
||
|
|
@@ -136,6 +137,15 @@ export async function setApiKey( | |
| const key = input.key.trim() | ||
| if (!key) throw new GatewayError('invalid_arguments', 'api_key must not be empty') | ||
|
|
||
| if (adapter.validateKey) { | ||
| await adapter.validateKey({ | ||
| workspaceId: input.workspaceId, | ||
| requestId: `key-validation-${input.prefix}`, | ||
| accessToken: key, | ||
| fetch: deps.fetch ?? fetch, | ||
| }) | ||
| } | ||
|
Comment on lines
+140
to
+147
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Separately, this call has no timeout budget. Every tool call goes through |
||
|
|
||
| await deps.grants.save(input.workspaceId, adapter.grantId, { | ||
| accessToken: key, | ||
| refreshToken: null, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -64,6 +64,55 @@ function withTools(tools: ProviderManifest['tools'], over: Partial<ProviderManif | |||||
| const demo = manifestProvider(base) | ||||||
|
|
||||||
| describe('manifest executor: requests', () => { | ||||||
| it('validates an api key without storing or exposing it', async () => { | ||||||
| const validator = withTools([], { validate: { request: 'GET /users/@me' }, auth: { type: 'api_key', in: 'header', name: 'authorization', prefix: 'Bot ' } }) | ||||||
| const upstream = fakeUpstream([{ match: /users\/@me/, body: { id: 'bot-1' } }]) | ||||||
|
|
||||||
| await validator.validateKey?.({ ...ctx(upstream), accessToken: 'secret-token' }) | ||||||
|
|
||||||
| expect(upstream.calls[0]?.url).toBe('https://api.demo.test/users/@me') | ||||||
| expect((upstream.calls[0]?.init?.headers as Record<string, string>).authorization).toBe('Bot secret-token') | ||||||
| }) | ||||||
|
|
||||||
| it('rejects an api key when its validation request fails', async () => { | ||||||
| const validator = withTools([], { validate: { request: 'GET /users/@me' } }) | ||||||
| const upstream = fakeUpstream([{ match: /users\/@me/, status: 401, body: { message: 'bad token' } }]) | ||||||
|
|
||||||
| await expect(validator.validateKey?.({ ...ctx(upstream), accessToken: 'wrong' })).rejects.toMatchObject({ | ||||||
| code: 'invalid_credential', | ||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Follows from the error code change you took above.
Suggested change
|
||||||
| }) | ||||||
| }) | ||||||
|
|
||||||
| it('puts query API keys on the validation request', async () => { | ||||||
| const validator = withTools([], { | ||||||
| validate: { request: 'GET /users/@me' }, | ||||||
| auth: { type: 'api_key', in: 'query', name: 'api_key' }, | ||||||
| }) | ||||||
| const upstream = fakeUpstream([{ match: /users\/@me\?api_key=secret-token/, body: { id: 'bot-1' } }]) | ||||||
|
|
||||||
| await validator.validateKey?.({ ...ctx(upstream), accessToken: 'secret-token' }) | ||||||
|
|
||||||
| expect(upstream.calls[0]?.url).toContain('api_key=secret-token') | ||||||
| }) | ||||||
|
|
||||||
| it('reports validation service failures as upstream errors', async () => { | ||||||
| const validator = withTools([], { validate: { request: 'GET /users/@me' } }) | ||||||
| const upstream = fakeUpstream([{ match: /users\/@me/, status: 503, body: { message: 'busy' } }]) | ||||||
|
|
||||||
| await expect(validator.validateKey?.({ ...ctx(upstream), accessToken: 'secret-token' })).rejects.toMatchObject({ | ||||||
| code: 'upstream_error', | ||||||
| }) | ||||||
| }) | ||||||
|
|
||||||
| it('reports validation network failures as upstream errors', async () => { | ||||||
| const validator = withTools([], { validate: { request: 'GET /users/@me' } }) | ||||||
| const unreachable = { ...ctx(fakeUpstream([])), fetch: (async () => { throw new Error('offline') }) as typeof fetch } | ||||||
|
|
||||||
| await expect(validator.validateKey?.({ ...unreachable, accessToken: 'secret-token' })).rejects.toMatchObject({ | ||||||
| code: 'upstream_error', | ||||||
| }) | ||||||
| }) | ||||||
|
|
||||||
| it('fills path placeholders and escapes every segment', async () => { | ||||||
| const upstream = fakeUpstream([{ match: /boxes/, body: itemsPage(1) }]) | ||||||
| await demo.callTool(ctx(upstream), 'list_things', { box: '../../admin' }) | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.