-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Audit application installations #27
base: main
Are you sure you want to change the base?
Changes from all commits
70376d3
41b1163
8f2007e
0cfa87d
c684dd9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -124,6 +124,47 @@ does have the side effect that, if an internal username is connected to multiple | |
GitHub usernames, as demonstrated above, all of those GitHub usernames will be | ||
made admins of the org. | ||
|
||
### [[applications]] | ||
|
||
Each `[[applications]]` section represents one org-level application | ||
installation. These entries are used to ensure that no applications get added to | ||
or removed from the org unexpectedly. You can also ensure that they only have | ||
the permissions you are expecting, and receive the events you are expecting. | ||
|
||
Similar to the `[[teams]]` above, applications are specified as an [array of | ||
tables]. In addition, the applications use a sub-table for the permissions, | ||
making a single application specification look like the following: | ||
|
||
```toml | ||
[[applications]] | ||
appId = 42 | ||
appSlug = "infinite-improbability-drive" | ||
repositorySelection = "selected" | ||
events = ["issues", "discussion", "pull_request"] | ||
|
||
[applications.permissions] | ||
# This applies to the application directly above, infinite-improbability-drive | ||
checks = "read" | ||
issues = "read" | ||
metadata = "read" | ||
deployments = "write" | ||
``` | ||
|
||
Given the flexibility of TOML, you can also specify the `permissions` table | ||
in-line, depending on your preference. That would look like this: | ||
|
||
```toml | ||
[[applications]] | ||
appId = 42 | ||
appSlug = "infinite-improbability-drive" | ||
repositorySelection = "selected" | ||
events = ["issues", "discussion", "pull_request"] | ||
permissions = {checks = "read", issues = "read", metadata = "read", deployments = "write"} | ||
``` | ||
|
||
Both of the above are wholly equivalent; it's simply a matter of preference | ||
which way you would like to specify them. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for folks that might not be so intimately familiar with toml, I'd recommend giving an example of how you might define multiple applications There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm, that's a good call. I do have a link to the TOML documentation which does have examples, but it's likely people won't click through. I also want to avoid making this document into a TOML tutorial, or too long. I think I'll add a second application to the |
||
|
||
## Development | ||
|
||
### Running Tests | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
|
||
const fs = require('fs').promises; | ||
const { graphql } = require('@octokit/graphql'); | ||
const { request } = require('@octokit/request'); | ||
const TOML = require('@iarna/toml'); | ||
const typedefs = require('./typedefs'); | ||
|
||
|
@@ -94,12 +95,39 @@ async function retrieveOrgInfo(orgName, token) { | |
// eslint-disable-next-line id-length | ||
requiresTwoFactorAuthentication: organization.requiresTwoFactorAuthentication, | ||
twitterUsername: organization.twitterUsername, | ||
websiteUrl: organization.websiteUrl | ||
websiteUrl: organization.websiteUrl, | ||
applications: await retrieveOrgApplications(orgName, token) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMHO don't do inline Also, given #26, you might start to think about how the different rulesets will need to define which information they'll need to function, how to make sure you're only running each data-fetch once, and how the data gets provided to the different rules. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not an inline As far as #26 though yeah I should probably make this line There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There's not a technical reason, it works, but it can be hard to debug and hard to reason about imagine code like: const stuff = {
foo: await foo(),
bar: await bar(),
fizzyLiftingDrinks: fizzyLiftingDrinks(),
fizzbuzz: await fizzbuzz()
}; it can be hard to debug if there are issues. its hard to reason about the ordering of the promises and from looking at it it looks at first glance that it might be run in parallel, but that wouldn't be the case. Is the lack of Pulling the awaits out make it easier to reason about and the order more apparent and you could force it to be parallel if you wanted. |
||
}; | ||
console.log(`${Object.keys(result.members).length} total members retrieved.`); | ||
return result; | ||
} | ||
|
||
/** | ||
* Retrieve a list of all applications installed to the requested org | ||
* | ||
* @param {string} orgName - The login name of the org to retrieve applications for | ||
* @param {string} token - A personal access token for interacting with the API | ||
* @returns {typedefs.AppSet} - A list of all applications installed on the organization | ||
*/ | ||
async function retrieveOrgApplications(orgName, token) { | ||
const response = await request('GET /orgs/{org}/installations', { | ||
headers: { authorization: `token ${token}` }, | ||
org: orgName | ||
}); | ||
const result = response.data.installations.reduce((apps, app) => { | ||
apps.push({ | ||
appId: app.app_id, | ||
appSlug: app.app_slug, | ||
repositorySelection: app.repository_selection, | ||
permissions: app.permissions, | ||
events: app.events | ||
}); | ||
return apps; | ||
}, []); | ||
console.log(`Loaded ${result.length} application installations for ${orgName}.`); | ||
return result; | ||
} | ||
|
||
/** | ||
* Load an org's expected configuration from a TOML config file | ||
* | ||
|
@@ -132,4 +160,4 @@ async function loadMembershipConfig(fileName) { | |
return config; | ||
} | ||
|
||
module.exports = { retrieveOrgInfo, loadMembershipConfig }; | ||
module.exports = { retrieveOrgInfo, retrieveOrgApplications, loadMembershipConfig }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
'use strict'; | ||
|
||
const assume = require('assume'); | ||
const fs = require('fs').promises; | ||
const loaders = require('../src/lib/loaders'); | ||
const nock = require('nock'); | ||
const path = require('path'); | ||
const sinon = require('sinon'); | ||
|
||
|
||
describe('Loaders', function () { | ||
|
||
before(function () { | ||
sinon.stub(console, 'log'); | ||
}); | ||
|
||
after(function () { | ||
sinon.restore(); | ||
}); | ||
Comment on lines
+13
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMHO make these be before/after - before/after the scope they exist in (in this case before & after everything within the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm yeah probably a good call. My thought here was mostly to just stop console logs from happening and since I am not actually checking these logs I only wanted just a single call as opposed to many. But I could see wanting to check the log values later on. |
||
|
||
describe('retrieveOrgApplications', function () { | ||
let scope; | ||
|
||
before(async function () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same with these, make it beforeEach/afterEach |
||
const response = JSON.parse(await fs.readFile(path.resolve(__dirname, 'responses/installations.json'))); | ||
scope = nock( | ||
'https://api.github.com/', | ||
{ reqheaders: { authorization: 'token 12345' } } | ||
).get('/orgs/foo/installations').reply(200, response).persist(); | ||
}); | ||
|
||
after(function () { | ||
nock.cleanAll(); | ||
}); | ||
|
||
it('calls the proper REST endpoint', async function () { | ||
await loaders.retrieveOrgApplications('foo', '12345'); | ||
scope.done(); | ||
}); | ||
|
||
it('returns only the relevant information', async function () { | ||
const apps = await loaders.retrieveOrgApplications('foo', '12345'); | ||
|
||
assume(apps).eqls([ | ||
{ | ||
appId: 12, | ||
appSlug: 'heart-of-gold', | ||
repositorySelection: 'all', | ||
permissions: { | ||
foo: 'read', | ||
bar: 'write', | ||
baz: 'read' | ||
}, | ||
events: ['push', 'release'] | ||
}, | ||
{ | ||
appId: 13, | ||
appSlug: 'infinite-improbability-drive', | ||
repositorySelection: 'selected', | ||
permissions: { | ||
foo: 'write', | ||
bar: 'read', | ||
baz: 'write' | ||
}, | ||
events: ['issue', 'discussion', 'pull_request'] | ||
} | ||
]); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as a consumer of this library, where do I find the values to use for these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm yeah good question. The app id and slug I was only able to find after actually querying our org. I honestly don't know of a better way to find them, so that may have to be our recommendation for now: run the tool and plug the data into your config. Which gives me a great idea for a future feature:
orglinter --generate-config
It could query the org and produce a valid config file based on its current state.Oh as for the
repositorySelection
I can document that "selected" and "all" are the only values. I think. I haven't been able to find solid documentation on that either. The REST API docs are terrible!