-
Notifications
You must be signed in to change notification settings - Fork 345
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
External Authentication Providers Support for Cody #6526
Changes from all commits
7b6f712
51ce8fa
43b5c21
87d2228
a0ad157
1c4e54a
90991a9
29f412c
70abf16
92c47ec
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 |
---|---|---|
@@ -0,0 +1,80 @@ | ||
#!/usr/bin/env python3 | ||
|
||
from aiohttp import web, ClientSession | ||
from urllib.parse import urlparse | ||
import argparse | ||
import asyncio | ||
import re | ||
|
||
async def proxy_handler(request): | ||
async with ClientSession(auto_decompress=False) as session: | ||
print(f'Request to: {request.url}') | ||
|
||
# Modify headers here | ||
headers = dict(request.headers) | ||
|
||
# Reset the Host header to use target server host instead of the proxy host | ||
if 'Host' in headers: | ||
headers['Host'] = urlparse(target_url).netloc.split(':')[0] | ||
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. This is ancient URL archaeology but with credentials the netloc is 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. Yea, since this is solely for testing purposes I do not think we care too much |
||
|
||
# 'chunked' encoding results in error 400 from Cloudflare, removing it still keeps response chunked anyway | ||
if 'Transfer-Encoding' in headers: | ||
del headers['Transfer-Encoding'] | ||
|
||
# Use value of 'Authorization: Bearer' to fill 'X-Forwarded-User' and remove 'Authorization' header | ||
if 'Authorization' in headers: | ||
match = re.match('Bearer (.*)', headers['Authorization']) | ||
if match: | ||
headers['X-Forwarded-User'] = match.group(1) | ||
del headers['Authorization'] | ||
|
||
# Forward the request to target | ||
async with session.request( | ||
method=request.method, | ||
url=f'{target_url}{request.path_qs}', | ||
headers=headers, | ||
data=await request.read() | ||
) as response: | ||
proxy_response = web.StreamResponse( | ||
status=response.status, | ||
headers=response.headers | ||
) | ||
|
||
await proxy_response.prepare(request) | ||
|
||
# Stream the response back | ||
async for chunk in response.content.iter_chunks(): | ||
await proxy_response.write(chunk[0]) | ||
|
||
await proxy_response.write_eof() | ||
return proxy_response | ||
|
||
app = web.Application() | ||
app.router.add_route('*', '/{path_info:.*}', proxy_handler) | ||
|
||
""" | ||
Reverse Proxy Server for testing External Auth Providers in Cody | ||
|
||
This script implements a simple reverse proxy server to facilitate testing of external authentication providers | ||
with Cody. It's role is to simulate simulate HTTP authentication proxy setups. It handles incoming requests by: | ||
- Forwarding them to a target Sourcegraph instance | ||
- Converting Bearer tokens from Authorization headers into X-Forwarded-User headers | ||
- Managing request/response streaming | ||
- Handling header modifications required for Cloudflare compatibility | ||
|
||
Target Sourcegraph instance needs to be configured to use HTTP authentication proxies | ||
as described in https://sourcegraph.com/docs/admin/auth#http-authentication-proxies | ||
""" | ||
if __name__ == '__main__': | ||
parser = argparse.ArgumentParser(description='External auth provider test proxy server') | ||
parser.add_argument('target_url', help='Target Sourcegraph instance URL to proxy to') | ||
parser.add_argument('proxy_port', type=int, nargs='?', default=5555, | ||
help='Port for the proxy server (default: %(default)s)') | ||
|
||
args = parser.parse_args() | ||
|
||
target_url = args.target_url.rstrip('/') | ||
port = args.proxy_port | ||
|
||
print(f'Starting proxy server on port {port} targeting {target_url}...') | ||
web.run_app(app, port=port) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { describe, expect, test } from 'vitest' | ||
import { type HeaderCredential, type TokenSource, isWindows } from '..' | ||
import { resolveAuth } from './auth-resolver' | ||
import type { ClientSecrets } from './resolver' | ||
|
||
class TempClientSecrets implements ClientSecrets { | ||
constructor(readonly store: Map<string, [string, TokenSource]>) {} | ||
|
||
getToken(endpoint: string): Promise<string | undefined> { | ||
return Promise.resolve(this.store.get(endpoint)?.[0]) | ||
} | ||
getTokenSource(endpoint: string): Promise<TokenSource | undefined> { | ||
return Promise.resolve(this.store.get(endpoint)?.[1]) | ||
} | ||
} | ||
|
||
describe('auth-resolver', () => { | ||
test('resolve with serverEndpoint and credentials overrides', async () => { | ||
const auth = await resolveAuth( | ||
'sourcegraph.com', | ||
{ | ||
authExternalProviders: [], | ||
overrideServerEndpoint: 'my-endpoint.com', | ||
overrideAuthToken: 'my-token', | ||
}, | ||
new TempClientSecrets(new Map([['sourcegraph.com/', ['sgp_212323123', 'paste']]])) | ||
) | ||
|
||
expect(auth.serverEndpoint).toBe('my-endpoint.com/') | ||
expect(auth.credentials).toEqual({ token: 'my-token' }) | ||
}) | ||
|
||
test('resolve with serverEndpoint override', async () => { | ||
const auth = await resolveAuth( | ||
'sourcegraph.com', | ||
{ | ||
authExternalProviders: [], | ||
overrideServerEndpoint: 'my-endpoint.com', | ||
overrideAuthToken: undefined, | ||
}, | ||
new TempClientSecrets(new Map([['my-endpoint.com/', ['sgp_212323123', 'paste']]])) | ||
) | ||
|
||
expect(auth.serverEndpoint).toBe('my-endpoint.com/') | ||
expect(auth.credentials).toEqual({ token: 'sgp_212323123', source: 'paste' }) | ||
}) | ||
|
||
test('resolve with token override', async () => { | ||
const auth = await resolveAuth( | ||
'sourcegraph.com', | ||
{ | ||
authExternalProviders: [], | ||
overrideServerEndpoint: undefined, | ||
overrideAuthToken: 'my-token', | ||
}, | ||
new TempClientSecrets(new Map([['sourcegraph.com/', ['sgp_777777777', 'paste']]])) | ||
) | ||
|
||
expect(auth.serverEndpoint).toBe('sourcegraph.com/') | ||
expect(auth.credentials).toEqual({ token: 'my-token' }) | ||
}) | ||
|
||
test('resolve custom auth provider', async () => { | ||
const credentialsJson = JSON.stringify({ | ||
headers: { Authorization: 'token X' }, | ||
expiration: 1337, | ||
}) | ||
|
||
const auth = await resolveAuth( | ||
'sourcegraph.com', | ||
{ | ||
authExternalProviders: [ | ||
{ | ||
endpoint: 'https://my-server.com', | ||
executable: { | ||
commandLine: [ | ||
isWindows() ? `echo ${credentialsJson}` : `echo '${credentialsJson}'`, | ||
], | ||
shell: isWindows() ? process.env.ComSpec : '/bin/bash', | ||
timeout: 5000, | ||
windowsHide: true, | ||
}, | ||
}, | ||
], | ||
overrideServerEndpoint: 'https://my-server.com', | ||
overrideAuthToken: undefined, | ||
}, | ||
new TempClientSecrets(new Map()) | ||
) | ||
|
||
expect(auth.serverEndpoint).toBe('https://my-server.com/') | ||
|
||
const headerCredential = auth.credentials as HeaderCredential | ||
expect(headerCredential.expiration).toBe(1337) | ||
expect(headerCredential.getHeaders()).toStrictEqual({ | ||
Authorization: 'token X', | ||
}) | ||
|
||
expect(JSON.stringify(headerCredential)).not.toContain('token X') | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a shebang and make it executable.
Add a brief comment or module doc comment explaining that this demonstrates using external authentication providers. Link to the docs for the X-Forwarded-User configuration would be great. And gentle disclaimer that it is for testing or demoing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done :)