Skip to content

Commit b94dd7b

Browse files
authored
Merge pull request #27 from OSS-Docs-Tools/allow_closing
Allows closing PR/issues by an existence check
2 parents 32204f0 + 7e54395 commit b94dd7b

File tree

3 files changed

+54
-9
lines changed

3 files changed

+54
-9
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ Then you should be good to go.
5959
6060
We force the use of [`pull_request_target`](https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/) as a workflow event to ensure that someone cannot change the CODEOWNER files at the same time as having that change be used to validate if they can merge.
6161

62+
### Issue / PR closing
63+
64+
Merging a PR has strict security requirements, but closing a PR or Issue can have a weaker one. Anyone with a GitHub login in the CODEOWNER has the ability to close any PR / Issue via a comment/review which includes:
65+
66+
```
67+
@github-actions close
68+
```
69+
6270
### Extras
6371

6472
You can use this label to set labels for specific sections of the codebase, by having square brackets to indicate labels to make: `[label]`

index.js

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const {readFileSync} = require("fs");
77

88
// Effectively the main function
99
async function run() {
10-
core.info("Running version 1.5.4")
10+
core.info("Running version 1.6.0")
1111

1212
// Tell folks they can merge
1313
if (context.eventName === "pull_request_target") {
@@ -20,7 +20,7 @@ async function run() {
2020
if (bodyLower.includes("lgtm")) {
2121
new Actor().mergeIfHasAccess();
2222
} else if (bodyLower.includes("@github-actions close")) {
23-
new Actor().closeIfHasAccess();
23+
new Actor().closePROrIssueIfInCodeowners();
2424
} else {
2525
console.log("Doing nothing because the body does not include a command")
2626
}
@@ -125,6 +125,7 @@ class Actor {
125125
this.octokit = getOctokit(process.env.GITHUB_TOKEN)
126126
this.thisRepo = { owner: context.repo.owner, repo: context.repo.repo }
127127
this.issue = context.payload.issue || context.payload.pull_request
128+
/** @type {string} - GitHub login */
128129
this.sender = context.payload.sender.login
129130
}
130131

@@ -195,11 +196,11 @@ class Actor {
195196
}
196197
}
197198

198-
async closeIfHasAccess() {
199-
const prInfo = await this.getTargetPRIfHasAccess()
200-
if (!prInfo) {
201-
return
202-
}
199+
async closePROrIssueIfInCodeowners() {
200+
// Because closing a PR/issue does not mutate the repo, we can use a weaker
201+
// authentication method: basically is the person in the codeowners? Then they can close
202+
// an issue or PR.
203+
if (!githubLoginIsInCodeowners(this.sender, this.cwd)) return
203204

204205
const { octokit, thisRepo, issue, sender } = this;
205206

@@ -230,6 +231,22 @@ function getFilesNotOwnedByCodeOwner(owner, files, cwd) {
230231
return filesWhichArentOwned
231232
}
232233

234+
235+
/**
236+
* This is a reasonable security measure for proving an account is specified in the codeowners
237+
* but _SHOULD NOT_ be used for authentication for something which mutates the repo,
238+
*
239+
* @param {string} login
240+
* @param {string} cwd
241+
*/
242+
function githubLoginIsInCodeowners(login, cwd) {
243+
const codeowners = new Codeowners(cwd);
244+
const contents = readFileSync(codeowners.codeownersFilePath, "utf8").toLowerCase()
245+
246+
return contents.includes("@" + login.toLowerCase() + " ") || contents.includes("@" + login.toLowerCase() + "\n")
247+
}
248+
249+
233250
/**
234251
*
235252
* @param {string[]} files
@@ -301,9 +318,11 @@ async function createOrAddLabel(octokit, repoDeets, labelConfig) {
301318
})
302319
}
303320

321+
// For tests
304322
module.exports = {
305323
getFilesNotOwnedByCodeOwner,
306-
findCodeOwnersForChangedFiles
324+
findCodeOwnersForChangedFiles,
325+
githubLoginIsInCodeowners
307326
}
308327

309328
// @ts-ignore

index.test.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { getFilesNotOwnedByCodeOwner, findCodeOwnersForChangedFiles } = require(".");
1+
const { getFilesNotOwnedByCodeOwner, findCodeOwnersForChangedFiles, githubLoginIsInCodeowners } = require(".");
22

33
test("determine who owns a set of files", () => {
44
const noFiles = findCodeOwnersForChangedFiles(["root-codeowners/one.two.js"], "./test-code-owners-repo");
@@ -35,3 +35,21 @@ test("deciding if someone has access to merge", () => {
3535
expect(filesNotInCodeowners).toEqual(["random-path/file.ts"]);
3636
});
3737

38+
describe(githubLoginIsInCodeowners, () => {
39+
test("allows folks found in the codeowners", () => {
40+
const ortaIn = githubLoginIsInCodeowners("orta", ".");
41+
expect(ortaIn).toEqual(true);
42+
});
43+
test("ignores case", () => {
44+
const ortaIn = githubLoginIsInCodeowners("OrTa", ".");
45+
expect(ortaIn).toEqual(true);
46+
});
47+
test("denies other accounts", () => {
48+
const noDogMan = githubLoginIsInCodeowners("dogman", ".");
49+
expect(noDogMan).toEqual(false);
50+
});
51+
test("denies subsets of existing accounts", () => {
52+
const noOrt = githubLoginIsInCodeowners("ort", ".");
53+
expect(noOrt).toEqual(false);
54+
});
55+
})

0 commit comments

Comments
 (0)