Skip to content

Commit be071a2

Browse files
jedwards1211babblebey
andauthoredSep 2, 2024··
feat: verify OAuth scopes of classic GitHub PATs (#897)
* fix: verify OAuth scopes of classic GitHub PATs * fix: make EGHNOPERMISSION error message clearer * chore: add comment about x-oauth-scopes header * test: fix failing test * test: add integration test for no maintain permission --------- Co-authored-by: Olabode Lawal-Shittabey <babblebey@gmail.com>
1 parent 8061687 commit be071a2

File tree

4 files changed

+420
-19
lines changed

4 files changed

+420
-19
lines changed
 

‎lib/definitions/errors.js

+15-3
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,24 @@ If you are using [GitHub Enterprise](https://enterprise.github.com) please make
178178

179179
export function EGHNOPERMISSION({ owner, repo }) {
180180
return {
181-
message: `The GitHub token doesn't allow to push on the repository ${owner}/${repo}.`,
181+
message: `The GitHub token doesn't allow to push to and maintain the repository ${owner}/${repo}.`,
182182
details: `The user associated with the [GitHub token](${linkify(
183183
"README.md#github-authentication",
184-
)}) configured in the \`GH_TOKEN\` or \`GITHUB_TOKEN\` environment variable must allows to push to the repository ${owner}/${repo}.
184+
)}) configured in the \`GH_TOKEN\` or \`GITHUB_TOKEN\` environment variable must have permission to push to and maintain the repository ${owner}/${repo}.
185185
186-
Please make sure the GitHub user associated with the token is an [owner](https://help.github.com/articles/permission-levels-for-a-user-account-repository/#owner-access-on-a-repository-owned-by-a-user-account) or a [collaborator](https://help.github.com/articles/permission-levels-for-a-user-account-repository/#collaborator-access-on-a-repository-owned-by-a-user-account) if the repository belong to a user account or has [write permissions](https://help.github.com/articles/managing-team-access-to-an-organization-repository) if the repository [belongs to an organization](https://help.github.com/articles/repository-permission-levels-for-an-organization).`,
186+
Please make sure the GitHub user associated with the token is an [owner](https://help.github.com/articles/permission-levels-for-a-user-account-repository/#owner-access-on-a-repository-owned-by-a-user-account) or a [collaborator](https://help.github.com/articles/permission-levels-for-a-user-account-repository/#collaborator-access-on-a-repository-owned-by-a-user-account) if the repository belongs to a user account or has [write permissions](https://help.github.com/articles/managing-team-access-to-an-organization-repository) if the repository [belongs to an organization](https://help.github.com/articles/repository-permission-levels-for-an-organization).`,
187+
};
188+
}
189+
190+
export function EGHNOSCOPE({ scopes }) {
191+
return {
192+
message: `The GitHub token doesn't have the necessary OAuth scopes to write contents, issues, and pull requests.`,
193+
details: `The [GitHub token](${linkify(
194+
"README.md#github-authentication",
195+
)}) configured in the \`GH_TOKEN\` or \`GITHUB_TOKEN\` environment variable must have the correct scopes.
196+
${scopes ? `\nThe token you used has scopes: ${scopes.join(", ")}\n` : ""}
197+
For classic PATs, make sure the token has the \`repo\` scope if the repository is private, or \`public_repo\` scope otherwise.
198+
For fine-grained PATs, make sure the token has the \`content: write\`, \`issues: write\`, and \`pull_requests: write\` scopes on the repository.`,
187199
};
188200
}
189201

‎lib/verify.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,21 @@ export default async function verify(pluginConfig, context, { Octokit }) {
105105
);
106106
try {
107107
const {
108-
data: { permissions, clone_url },
108+
headers,
109+
data: { private: _private, permissions, clone_url },
109110
} = await octokit.request("GET /repos/{owner}/{repo}", { repo, owner });
111+
112+
// GitHub only returns this header if the token is a classic PAT
113+
if (headers?.["x-oauth-scopes"]) {
114+
const scopes = headers["x-oauth-scopes"].split(/\s*,\s*/g);
115+
if (
116+
!scopes.includes("repo") &&
117+
(_private || !scopes.includes("public_repo"))
118+
) {
119+
errors.push(getError("EGHNOSCOPE", { scopes }));
120+
}
121+
}
122+
110123
// Verify if Repository Name wasn't changed
111124
const parsedCloneUrl = parseGithubUrl(clone_url);
112125
if (
@@ -122,7 +135,7 @@ export default async function verify(pluginConfig, context, { Octokit }) {
122135
// Do not check for permissions in GitHub actions, as the provided token is an installation access token.
123136
// octokit.request("GET /repos/{owner}/{repo}", {repo, owner}) does not return the "permissions" key in that case.
124137
// But GitHub Actions have all permissions required for @semantic-release/github to work
125-
if (!env.GITHUB_ACTION && !permissions?.push) {
138+
if (!env.GITHUB_ACTION && !(permissions?.push && permissions?.maintain)) {
126139
// If authenticated as GitHub App installation, `push` will always be false.
127140
// We send another request to check if current authentication is an installation.
128141
// Note: we cannot check if the installation has all required permissions, it's

‎test/integration.test.js

+63-5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ test("Verify GitHub auth", async (t) => {
2929
.getOnce(`https://api.github.local/repos/${owner}/${repo}`, {
3030
permissions: {
3131
push: true,
32+
maintain: true,
3233
},
3334
clone_url: `https://api.github.local/${owner}/${repo}.git`,
3435
});
@@ -49,6 +50,43 @@ test("Verify GitHub auth", async (t) => {
4950
t.true(fetch.done());
5051
});
5152

53+
test("Throws when GitHub user lacks maintain permission", async (t) => {
54+
const owner = "test_user";
55+
const repo = "test_repo";
56+
const env = { GITHUB_TOKEN: "github_token" };
57+
const options = {
58+
repositoryUrl: `git+https://othertesturl.com/${owner}/${repo}.git`,
59+
};
60+
61+
const fetch = fetchMock
62+
.sandbox()
63+
.getOnce(`https://api.github.local/repos/${owner}/${repo}`, {
64+
permissions: {
65+
push: true,
66+
maintain: false,
67+
},
68+
clone_url: `https://api.github.local/${owner}/${repo}.git`,
69+
});
70+
71+
const {
72+
errors: [error],
73+
} = await t.throwsAsync(
74+
t.context.m.verifyConditions(
75+
{},
76+
{ cwd, env, options, logger: t.context.logger },
77+
{
78+
Octokit: TestOctokit.defaults((options) => ({
79+
...options,
80+
request: { ...options.request, fetch },
81+
})),
82+
},
83+
),
84+
);
85+
86+
t.is(error.code, "EGHNOPERMISSION");
87+
t.true(fetch.done());
88+
});
89+
5290
test("Verify GitHub auth with publish options", async (t) => {
5391
const owner = "test_user";
5492
const repo = "test_repo";
@@ -62,6 +100,7 @@ test("Verify GitHub auth with publish options", async (t) => {
62100
.get(`https://api.github.local/repos/${owner}/${repo}`, {
63101
permissions: {
64102
push: true,
103+
maintain: true,
65104
},
66105
clone_url: `https://api.github.local/${owner}/${repo}.git`,
67106
});
@@ -102,6 +141,7 @@ test("Verify GitHub auth and assets config", async (t) => {
102141
.getOnce(`https://api.github.local/repos/${owner}/${repo}`, {
103142
permissions: {
104143
push: true,
144+
maintain: true,
105145
},
106146
clone_url: `https://api.github.local/${owner}/${repo}.git`,
107147
});
@@ -208,6 +248,7 @@ test("Publish a release with an array of assets", async (t) => {
208248
.getOnce(`https://api.github.local/repos/${owner}/${repo}`, {
209249
permissions: {
210250
push: true,
251+
maintain: true,
211252
},
212253
clone_url: `https://api.github.local/${owner}/${repo}.git`,
213254
})
@@ -303,6 +344,7 @@ test("Publish a release with release information in assets", async (t) => {
303344
.getOnce(`https://api.github.local/repos/${owner}/${repo}`, {
304345
permissions: {
305346
push: true,
347+
maintain: true,
306348
},
307349
clone_url: `https://api.github.local/${owner}/${repo}.git`,
308350
})
@@ -376,6 +418,7 @@ test("Update a release", async (t) => {
376418
.getOnce(`https://api.github.local/repos/${owner}/${repo}`, {
377419
permissions: {
378420
push: true,
421+
maintain: true,
379422
},
380423
clone_url: `https://api.github.local/${owner}/${repo}.git`,
381424
})
@@ -442,7 +485,10 @@ test("Comment and add labels on PR included in the releases", async (t) => {
442485
.get(
443486
`https://api.github.local/repos/${owner}/${repo}`,
444487
{
445-
permissions: { push: true },
488+
permissions: {
489+
push: true,
490+
maintain: true,
491+
},
446492
full_name: `${owner}/${repo}`,
447493
clone_url: `htttps://api.github.local/${owner}/${repo}.git`,
448494
},
@@ -550,7 +596,10 @@ test("Open a new issue with the list of errors", async (t) => {
550596
.get(
551597
`https://api.github.local/repos/${owner}/${repo}`,
552598
{
553-
permissions: { push: true },
599+
permissions: {
600+
push: true,
601+
maintain: true,
602+
},
554603
full_name: `${owner}/${repo}`,
555604
clone_url: `htttps://api.github.local/${owner}/${repo}.git`,
556605
},
@@ -645,7 +694,10 @@ test("Verify, release and notify success", async (t) => {
645694
.get(
646695
`https://api.github.local/repos/${owner}/${repo}`,
647696
{
648-
permissions: { push: true },
697+
permissions: {
698+
push: true,
699+
maintain: true,
700+
},
649701
full_name: `${owner}/${repo}`,
650702
clone_url: `htttps://api.github.local/${owner}/${repo}.git`,
651703
},
@@ -811,7 +863,10 @@ test("Verify, update release and notify success", async (t) => {
811863
.get(
812864
`https://api.github.local/repos/${owner}/${repo}`,
813865
{
814-
permissions: { push: true },
866+
permissions: {
867+
push: true,
868+
maintain: true,
869+
},
815870
full_name: `${owner}/${repo}`,
816871
clone_url: `htttps://api.github.local/${owner}/${repo}.git`,
817872
},
@@ -949,7 +1004,10 @@ test("Verify and notify failure", async (t) => {
9491004
.get(
9501005
`https://api.github.local/repos/${owner}/${repo}`,
9511006
{
952-
permissions: { push: true },
1007+
permissions: {
1008+
push: true,
1009+
maintain: true,
1010+
},
9531011
full_name: `${owner}/${repo}`,
9541012
clone_url: `htttps://api.github.local/${owner}/${repo}.git`,
9551013
},

‎test/verify.test.js

+327-9
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.