Skip to content

Commit d04dd80

Browse files
committed
add service token to projects requests
1 parent 4835396 commit d04dd80

13 files changed

Lines changed: 108 additions & 36 deletions

.dev.vars.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,8 @@ ALLOW_LOCAL_AUTH="true"
1919
# - VITE_CLIENT_ID: Your OAuth client ID
2020
#
2121
# The backend expects RS256 JWT tokens from the OIDC provider.
22+
23+
# Cloudflare Access headers for service-to-service authentication with Projects service
24+
# These are optional - only needed if you want to test Cloudflare Access authentication locally
25+
# CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID="your-client-id"
26+
# CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET="your-client-secret"

.github/workflows/.deploy-reusable.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ on:
2222
required: true
2323
CLOUDFLARE_ACCOUNT_ID:
2424
required: true
25+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID:
26+
required: false
27+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET:
28+
required: false
2529

2630
jobs:
2731
deploy:
@@ -100,6 +104,24 @@ jobs:
100104
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
101105
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
102106

107+
- name: Set Cloudflare service token secrets from GitHub
108+
# Set secrets in Cloudflare Workers from GitHub environment secrets
109+
# This approach is compatible with future Kubernetes deployment where
110+
# these would be set as environment variables in the pod/deployment
111+
run: |
112+
if [ -n "$CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID" ] && [ -n "$CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET" ]; then
113+
echo "Setting Cloudflare service token secrets from GitHub environment secrets..."
114+
echo "$CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID" | pnpm wrangler secret put CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID --env ${{ inputs.environment }}
115+
echo "$CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET" | pnpm wrangler secret put CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET --env ${{ inputs.environment }}
116+
else
117+
echo "Cloudflare service token secrets not provided in GitHub environment, skipping..."
118+
fi
119+
env:
120+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
121+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
122+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID: ${{ secrets.CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID }}
123+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET: ${{ secrets.CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET }}
124+
103125
- name: Deploy base app (with retry)
104126
run: |
105127
# Retry deployment up to 3 times with exponential backoff

.github/workflows/auto-deploy-preview.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ jobs:
1414
secrets:
1515
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
1616
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
17+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID: ${{ secrets.CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID }}
18+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET: ${{ secrets.CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET }}

.github/workflows/deploy-preview-manually.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ jobs:
2020
secrets:
2121
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
2222
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
23+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID: ${{ secrets.CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID }}
24+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET: ${{ secrets.CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET }}
25+

.github/workflows/deploy-production.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ jobs:
3737
secrets:
3838
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
3939
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
40+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID: ${{ secrets.CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID }}
41+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET: ${{ secrets.CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET }}

.github/workflows/validate-preview.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ jobs:
1414
secrets:
1515
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
1616
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
17+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID: ${{ secrets.CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID }}
18+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET: ${{ secrets.CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET }}

backend/clients/projects-client.ts

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@
77
* - Managing file uploads and downloads
88
*/
99

10-
export interface ProjectsConfig {
11-
baseUrl: string;
12-
bearerToken: string;
13-
}
14-
1510
class ProjectsError extends Error {
1611
statusCode: number | undefined;
1712
constructor(message: string, statusCode?: number) {
@@ -181,10 +176,42 @@ export interface FilePreloadResponse {
181176
* Main Projects Service Client
182177
*/
183178
export class ProjectsClient {
184-
private config: ProjectsConfig;
179+
private baseUrl: string;
180+
private bearerToken: string;
181+
private cloudflareServiceToken?: {
182+
clientId: string;
183+
clientSecret: string;
184+
};
185185

186-
constructor(config: ProjectsConfig) {
187-
this.config = config;
186+
constructor(
187+
env: {
188+
ANACONDA_PROJECTS_URL: string;
189+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID?: string;
190+
CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET?: string;
191+
},
192+
bearerToken: string
193+
) {
194+
this.baseUrl = env.ANACONDA_PROJECTS_URL;
195+
this.bearerToken = bearerToken;
196+
197+
// Add Cloudflare Access headers if available in environment
198+
if (env.CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID && env.CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET) {
199+
this.cloudflareServiceToken = {
200+
clientId: env.CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID,
201+
clientSecret: env.CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET,
202+
};
203+
console.log(
204+
"ProjectsClient: Cloudflare Access headers configured (client ID present)"
205+
);
206+
} else {
207+
console.log(
208+
"ProjectsClient: Cloudflare Access headers not configured",
209+
{
210+
hasClientId: !!env.CLOUDFLARE_SERVICE_TOKEN_CLIENT_ID,
211+
hasClientSecret: !!env.CLOUDFLARE_SERVICE_TOKEN_CLIENT_SECRET,
212+
}
213+
);
214+
}
188215
}
189216

190217
/**
@@ -202,23 +229,31 @@ export class ProjectsClient {
202229

203230
for (let attempt = 0; attempt < maxRetries; attempt++) {
204231
console.log(
205-
`ProjectsClient request attempt ${attempt + 1} for ${method} ${this.config.baseUrl}${path}`
232+
`ProjectsClient request attempt ${attempt + 1} for ${method} ${this.baseUrl}${path}`
206233
);
207234
try {
208-
const url = `${this.config.baseUrl}${path}`;
235+
const url = `${this.baseUrl}${path}`;
236+
237+
const headers: Record<string, string> = {
238+
"Content-Type": "application/json",
239+
"User-Agent": "intheloop",
240+
Authorization: `Bearer ${this.bearerToken}`,
241+
};
242+
243+
// Add Cloudflare Access headers if configured
244+
if (this.cloudflareServiceToken) {
245+
headers["CF-Access-Client-Id"] = this.cloudflareServiceToken.clientId;
246+
headers["CF-Access-Client-Secret"] = this.cloudflareServiceToken.clientSecret;
247+
}
209248

210249
const response = await fetch(url, {
211250
method,
212-
headers: {
213-
"Content-Type": "application/json",
214-
Authorization: `Bearer ${this.config.bearerToken}`,
215-
"User-Agent": "intheloop",
216-
},
251+
headers,
217252
body: body ? JSON.stringify(body) : undefined,
218253
});
219254

220255
console.log(
221-
`ProjectsClient response status: ${response.status} for ${method} ${this.config.baseUrl}${path}`
256+
`ProjectsClient response status: ${response.status} for ${method} ${this.baseUrl}${path}`
222257
);
223258

224259
if (!response.ok) {
@@ -330,7 +365,7 @@ export class ProjectsClient {
330365
const queryString = params.toString();
331366
if (queryString) url += `?${queryString}`;
332367
} else {
333-
url = nextPageUrl.replace(this.config.baseUrl, "");
368+
url = nextPageUrl.replace(this.baseUrl, "");
334369
}
335370

336371
let listResponse: ProjectListResponse =
@@ -457,3 +492,4 @@ export class ProjectsClient {
457492
);
458493
}
459494
}
495+

backend/middleware.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,7 @@ export const projectsClientMiddleware = createMiddleware<{
124124
if (c.env.PERMISSIONS_PROVIDER === "anaconda") {
125125
const bearerToken = getBearerToken(c.req);
126126
if (bearerToken) {
127-
const projectsClient = new ProjectsClient({
128-
baseUrl: c.env.ANACONDA_PROJECTS_URL,
129-
bearerToken,
130-
});
127+
const projectsClient = new ProjectsClient(c.env, bearerToken);
131128
c.set("projectsClient", projectsClient);
132129
}
133130
}

backend/notebook-permissions/factory.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { NoPermissionsProvider } from "./no-permissions.ts";
33
import type { PermissionsProvider } from "./types.ts";
44
import { RuntError, ErrorType, type Env } from "../types.ts";
55
import { AnacondaPermissionsProvider } from "./anaconda-permissions.ts";
6-
import { ProjectsClient } from "backend/clients/projects-client.ts";
6+
import {
7+
ProjectsClient,
8+
} from "backend/clients/projects-client.ts";
79

810
// Re-export providers and types for convenience
911
export { LocalPermissionsProvider } from "./local-permissions.ts";
@@ -25,11 +27,7 @@ export function createPermissionsProvider(
2527
case "anaconda":
2628
try {
2729
const client =
28-
projectsClient ||
29-
new ProjectsClient({
30-
baseUrl: env.ANACONDA_PROJECTS_URL,
31-
bearerToken: bearerToken,
32-
});
30+
projectsClient || new ProjectsClient(env, bearerToken);
3331
return new AnacondaPermissionsProvider(client, env.DB);
3432
} catch (error) {
3533
throw new RuntError(ErrorType.ServerMisconfigured, {

backend/selective-entry.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,7 @@ export default {
195195
// Initialize ProjectsClient per request (separate from auth)
196196
let projectsClient: ProjectsClient | undefined;
197197
if (env.PERMISSIONS_PROVIDER === "anaconda" && authToken) {
198-
projectsClient = new ProjectsClient({
199-
baseUrl: env.ANACONDA_PROJECTS_URL,
200-
bearerToken: authToken,
201-
});
198+
projectsClient = new ProjectsClient(env, authToken);
202199
}
203200

204201
// Create permissions provider

0 commit comments

Comments
 (0)