History

Launch built-in Google OAuth for Calendar and Sheets

Rhys Sullivan committed 2 months ago

Commit ac3bea51 parent 67ee8a013 files · +313 −10Browse files

Showing the changes to e2e (2 of 13 files). Show all

Unified · Split

2 files
  1. e2e/scenarios/first-party-oauth.test.ts +117 −0
  2. e2e/setup/cloud.boot.ts +5 −0
e2e/scenarios/first-party-oauth.test.ts+117−0
@@ -62,12 +62,45 @@ const githubShapedIntegrationSpec = { · 61 unmodified lines
6262 ],
6363 } as const;
6464
65+const googleShapedIntegrationSpec = (scopes: readonly string[]) => ({
66+ spec: {
67+ kind: "blob" as const,
68+ value: JSON.stringify({
69+ openapi: "3.0.3",
70+ info: { title: "Google-shaped API", version: "1.0.0" },
71+ paths: {
72+ "/resource": {
73+ get: {
74+ operationId: "getResource",
75+ tags: ["default"],
76+ responses: { "200": { description: "a Google resource" } },
77+ },
78+ },
79+ },
80+ }),
81+ },
82+ baseUrl: "https://www.googleapis.com",
83+ authenticationTemplate: [
84+ {
85+ slug: "oauth",
86+ kind: "oauth2" as const,
87+ authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
88+ tokenUrl: "https://oauth2.googleapis.com/token",
89+ scopes,
90+ },
91+ ],
92+});
93+
6594 scenario(
6695 "First-party OAuth · the host-declared GitHub app is listed and drives the authorize redirect",
6796 {},
6897 Effect.scoped(
6998 Effect.gen(function* () {
7099 const target = yield* Target;
100+ // First-party registrations are a cloud-host capability. This scenario
101+ // intentionally does not apply to self-host, whose operator supplies
102+ // their own OAuth apps through its existing registration flow.
103+ if (target.name !== "cloud") return;
71104 const { client: makeApiClient } = yield* Api;
72105 const identity = yield* target.newIdentity();
73106 const client = yield* makeApiClient(api, identity);
@@ -133,3 +166,87 @@ scenario( · 59 unmodified lines
133166 }),
134167 ),
135168 );
169+
170+scenario(
171+ "First-party OAuth · Google offers Calendar and Sheets but refuses Gmail scopes",
172+ {},
173+ Effect.scoped(
174+ Effect.gen(function* () {
175+ const target = yield* Target;
176+ if (target.name !== "cloud") return;
177+ const { client: makeApiClient } = yield* Api;
178+ const identity = yield* target.newIdentity();
179+ const client = yield* makeApiClient(api, identity);
180+
181+ const clients = yield* client.oauth.listClients();
182+ const google = clients.find((candidate) => String(candidate.slug) === "first-party:google");
183+ expect(google, "the env-declared first-party Google app is listed").toBeDefined();
184+ expect(google?.origin.kind).toBe("first_party");
185+ if (google?.origin.kind !== "first_party") return;
186+ expect(google.origin.allowedScopes).toContain("https://www.googleapis.com/auth/calendar");
187+ expect(google.origin.allowedScopes).toContain("https://www.googleapis.com/auth/spreadsheets");
188+ expect(google.origin.allowedScopes).not.toContain("https://mail.google.com/");
189+ expect(google.origin.allowedScopes).not.toContain("https://www.googleapis.com/auth/drive");
190+
191+ const calendar = IntegrationSlug.make(unique("google_calendar"));
192+ yield* client.openapi.addSpec({
193+ payload: {
194+ ...googleShapedIntegrationSpec([
195+ "openid",
196+ "email",
197+ "profile",
198+ "https://www.googleapis.com/auth/calendar",
199+ ]),
200+ slug: calendar,
201+ },
202+ });
203+ const started = yield* client.oauth.start({
204+ payload: {
205+ client: OAuthClientSlug.make("first-party:google"),
206+ clientOwner: "org",
207+ owner: "org",
208+ name: ConnectionName.make("calendar"),
209+ integration: calendar,
210+ template: AuthTemplateSlug.make("oauth"),
211+ },
212+ });
213+ expect(started.status).toBe("redirect");
214+ const authorizationUrl = started.status === "redirect" ? started.authorizationUrl : "";
215+ const authorize = new URL(authorizationUrl);
216+ expect(authorize.origin + authorize.pathname).toBe(
217+ "https://accounts.google.com/o/oauth2/v2/auth",
218+ );
219+ expect(authorize.searchParams.get("client_id")).toBe("e2e-first-party-google");
220+ expect(authorize.searchParams.get("access_type")).toBe("offline");
221+ expect(new Set(authorize.searchParams.get("scope")?.split(" ") ?? [])).toEqual(
222+ new Set(["openid", "email", "profile", "https://www.googleapis.com/auth/calendar"]),
223+ );
224+
225+ const gmail = IntegrationSlug.make(unique("google_gmail"));
226+ yield* client.openapi.addSpec({
227+ payload: {
228+ ...googleShapedIntegrationSpec([
229+ "openid",
230+ "email",
231+ "profile",
232+ "https://mail.google.com/",
233+ ]),
234+ slug: gmail,
235+ },
236+ });
237+ const blocked = yield* client.oauth
238+ .start({
239+ payload: {
240+ client: OAuthClientSlug.make("first-party:google"),
241+ clientOwner: "org",
242+ owner: "org",
243+ name: ConnectionName.make("gmail"),
244+ integration: gmail,
245+ template: AuthTemplateSlug.make("oauth"),
246+ },
247+ })
248+ .pipe(Effect.flip);
249+ expect(blocked).toBeDefined();
250+ }),
251+ ),
252+);
e2e/setup/cloud.boot.ts+5−0
@@ -104,6 +104,11 @@ export const bootCloud = async (options: CloudBootOptions): Promise<CloudBooted> · 103 unmodified lines
104104 // complete authorize → token dance instead of stopping at github.com.
105105 FIRST_PARTY_GITHUB_AUTHORIZE_URL: process.env.FIRST_PARTY_GITHUB_AUTHORIZE_URL,
106106 FIRST_PARTY_GITHUB_TOKEN_URL: process.env.FIRST_PARTY_GITHUB_TOKEN_URL,
107+ // Fake Google registration for redirect-only first-party coverage. The
108+ // scenario never visits Google or exchanges a code; it proves cloud env,
109+ // scope policy, and authorization URL construction end to end.
110+ FIRST_PARTY_GOOGLE_CLIENT_ID: "e2e-first-party-google",
111+ FIRST_PARTY_GOOGLE_CLIENT_SECRET: "e2e-first-party-google-secret",
107112 // Shrink the per-org hourly execution cap (prod default 1000) to a number
108113 // the rate-limit-backstop scenario can actually exhaust with real
109114 // executions — but see execution-limits.ts: it must stay above every other

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t