Skip to content

Commit b652da7

Browse files
committed
fix: address github scope review feedback
1 parent fa08627 commit b652da7

4 files changed

Lines changed: 43 additions & 9 deletions

File tree

packages/lib/src/usecases/auth-github.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
normalizeGithubScopes
2222
} from "./github-scope-policy.js"
2323
import type { GithubTokenValidationResult } from "./github-token-validation.js"
24-
import { validateGithubToken } from "./github-token-validation.js"
24+
import { githubInvalidTokenMessage, validateGithubToken } from "./github-token-validation.js"
2525
import { resolvePathFromCwd } from "./path-helpers.js"
2626
import { withFsPathContext } from "./runtime.js"
2727
import { ensureStateDotDockerGitRepo } from "./state-repo-github.js"
@@ -204,7 +204,10 @@ export const runGithubRemoveDeleteRepoScope = (
204204
export const rejectGithubTokenWithRepositoryDeleteScope = (token: string): Effect.Effect<void, AuthError> =>
205205
validateGithubToken(token).pipe(
206206
Effect.flatMap((validation) => {
207-
if (validation.status !== "valid" || validation.oauthScopes === null) {
207+
if (validation.status === "invalid") {
208+
return Effect.fail(new AuthError({ message: githubInvalidTokenMessage }))
209+
}
210+
if (validation.status === "unknown" || validation.oauthScopes === null) {
208211
return Effect.fail(new AuthError({ message: githubUnverifiedTokenScopesMessage }))
209212
}
210213
return hasGithubRepositoryDeleteScope(validation.oauthScopes)

packages/lib/src/usecases/github-scope-policy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const defaultGithubScopes: ReadonlyArray<string> = ["repo", "workflow", "read:org"]
1+
export const defaultGithubScopes: ReadonlyArray<string> = Object.freeze(["repo", "workflow", "read:org"])
22
export const githubRepositoryDeleteScope = "delete_repo"
33
export const githubForbiddenDeleteRepoScopeMessage = [
44
"GitHub auth token includes forbidden OAuth scope: delete_repo.",

packages/lib/tests/usecases/auth-container-paths.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ describe("auth container paths", () => {
190190
const recorded: Array<RecordedCommand> = []
191191
const executor = makeFakeExecutor(recorded)
192192
const fetchMock = vi.fn<typeof globalThis.fetch>(() =>
193-
Promise.resolve(githubUserResponse("repo, workflow, read:org"))
193+
Effect.runPromise(Effect.succeed(githubUserResponse("repo, workflow, read:org")))
194194
)
195195

196196
yield* _(
@@ -287,7 +287,7 @@ describe("auth container paths", () => {
287287
const recorded: Array<RecordedCommand> = []
288288
const executor = makeFakeExecutor(recorded)
289289
const fetchMock = vi.fn<typeof globalThis.fetch>(() =>
290-
Promise.resolve(githubUserResponse("repo, workflow"))
290+
Effect.runPromise(Effect.succeed(githubUserResponse("repo, workflow")))
291291
)
292292

293293
yield* _(
@@ -329,7 +329,7 @@ describe("auth container paths", () => {
329329
const recorded: Array<RecordedCommand> = []
330330
const executor = makeFakeExecutor(recorded)
331331
const fetchMock = vi.fn<typeof globalThis.fetch>(() =>
332-
Promise.resolve(githubUserResponse("repo, delete_repo"))
332+
Effect.runPromise(Effect.succeed(githubUserResponse("repo, delete_repo")))
333333
)
334334

335335
const failure = yield* _(
@@ -371,7 +371,7 @@ describe("auth container paths", () => {
371371
const recorded: Array<RecordedCommand> = []
372372
const executor = makeFakeExecutor(recorded)
373373
const fetchMock = vi.fn<typeof globalThis.fetch>(() =>
374-
Promise.resolve(githubUserResponse(null))
374+
Effect.runPromise(Effect.succeed(githubUserResponse(null)))
375375
)
376376

377377
const failure = yield* _(
@@ -411,7 +411,7 @@ describe("auth container paths", () => {
411411
const fs = yield* _(FileSystem.FileSystem)
412412
const envPath = `${root}/.docker-git/.orch/env/global.env`
413413
const fetchMock = vi.fn<typeof globalThis.fetch>(() =>
414-
Promise.resolve(githubUserResponse("repo, delete_repo"))
414+
Effect.runPromise(Effect.succeed(githubUserResponse("repo, delete_repo")))
415415
)
416416

417417
const failure = yield* _(
@@ -448,7 +448,7 @@ describe("auth container paths", () => {
448448
const fs = yield* _(FileSystem.FileSystem)
449449
const envPath = `${root}/.docker-git/.orch/env/global.env`
450450
const fetchMock = vi.fn<typeof globalThis.fetch>(() =>
451-
Promise.resolve(githubUserResponse(null))
451+
Effect.runPromise(Effect.succeed(githubUserResponse(null)))
452452
)
453453

454454
const failure = yield* _(

packages/lib/tests/usecases/github-scope-policy.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fc from "fast-check"
12
import { describe, expect, it } from "vitest"
23

34
import {
@@ -8,6 +9,21 @@ import {
89
} from "../../src/usecases/github-scope-policy.js"
910

1011
describe("github scope policy", () => {
12+
const deleteRepoScopeArbitrary = fc.constantFrom("delete_repo", "DELETE_REPO", "Delete_Repo", " delete_repo ")
13+
const separatorArbitrary = fc.constantFrom(",", " ", "\n", "\t", ", ", " , ")
14+
const scopeTokenArbitrary = fc.oneof(fc.string(), deleteRepoScopeArbitrary)
15+
const scopeListInputArbitrary = fc.array(scopeTokenArbitrary, { maxLength: 20 }).chain((scopes) =>
16+
fc.array(separatorArbitrary, { minLength: scopes.length, maxLength: scopes.length }).map((separators) =>
17+
scopes.map((scope, index) => `${scope}${separators[index] ?? ""}`).join("")
18+
)
19+
)
20+
const nullableScopeInputArbitrary = fc.oneof(fc.constant(null), fc.constant(undefined), fc.string(), scopeListInputArbitrary)
21+
const forbiddenOnlyInputArbitrary = fc.array(deleteRepoScopeArbitrary, { minLength: 1, maxLength: 20 }).chain((scopes) =>
22+
fc.array(separatorArbitrary, { minLength: scopes.length, maxLength: scopes.length }).map((separators) =>
23+
scopes.map((scope, index) => `${scope}${separators[index] ?? ""}`).join("")
24+
)
25+
)
26+
1127
it("preserves default safe scopes", () => {
1228
expect(normalizeGithubScopes(null)).toEqual(defaultGithubScopes)
1329
expect(normalizeGithubScopes("")).toEqual(defaultGithubScopes)
@@ -25,6 +41,21 @@ describe("github scope policy", () => {
2541
expect(normalizeGithubScopes("delete_repo DELETE_REPO")).toEqual(defaultGithubScopes)
2642
})
2743

44+
it("preserves the no-delete-repo invariant for arbitrary scope inputs", () => {
45+
fc.assert(
46+
fc.property(nullableScopeInputArbitrary, (input) => {
47+
expect(
48+
normalizeGithubScopes(input).some((scope) => scope.trim().toLowerCase() === "delete_repo")
49+
).toBe(false)
50+
})
51+
)
52+
fc.assert(
53+
fc.property(forbiddenOnlyInputArbitrary, (input) => {
54+
expect(normalizeGithubScopes(input)).toEqual(defaultGithubScopes)
55+
})
56+
)
57+
})
58+
2859
it("parses GitHub OAuth scope headers and detects repository deletion", () => {
2960
expect(parseGithubOauthScopesHeader(null)).toBe(null)
3061
expect(parseGithubOauthScopesHeader(undefined)).toBe(null)

0 commit comments

Comments
 (0)