diff --git a/lib/installations.js b/lib/installations.js new file mode 100644 index 00000000..4361c6d0 --- /dev/null +++ b/lib/installations.js @@ -0,0 +1,45 @@ +const jwt = require('./jwt'); +const GitHubApi = require('github'); + +const github = new GitHubApi(); +const installations = {}; + +module.exports = { + load: function() { + github.authenticate({type: 'integration', token: jwt()}); + + github.integrations.getInstallations({per_page: 100}).then(data => { + data.forEach(this.register); + }); + }, + + register: function(installation) { + installations[installation.account.login] = installation; + }, + + unregister: function(installation) { + delete installations[installation.account.login]; + }, + + for: function(account) { + return installations[account]; + }, + + listen: function(webhook) { + webhook.on('integration_installation', function(event) { + if (event.payload.action == 'create') { + this.register(event.payload.installation); + } else if (event.payload.action == 'deleted') { + this.unregister(event.payload.installation); + } + }); + }, + + authAs: function(installation) { + const github = new GitHubApi(); + github.authenticate({type: 'integration', token: jwt()}); + return github.integrations.createInstallationToken({ + installation_id: installation.id + }); + } +} diff --git a/lib/jwt.js b/lib/jwt.js new file mode 100644 index 00000000..1a2ed67d --- /dev/null +++ b/lib/jwt.js @@ -0,0 +1,22 @@ +const fs = require('fs'); +const jwt = require('jsonwebtoken'); +const process = require('process'); + +// sign with RSA SHA256 +// FIXME: move to env var +const cert = fs.readFileSync('private-key.pem'); // get private key + +module.exports = generate; + +function generate () { + var payload = { + // issued at time + iat: Math.floor(new Date() / 1000), + // JWT expiration time + exp: Math.floor(new Date() / 1000) + 60 * 5, + // Integration's GitHub identifier + iss: process.env.INTEGRATION_ID + } + + return jwt.sign(payload, cert, { algorithm: 'RS256'}); +} diff --git a/package.json b/package.json index e4183973..a3b08ffd 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "github-webhook-handler": "^0.6.0", "handlebars": "^4.0.5", "js-yaml": "^3.6.1", - "jsdoc": "^3.4.2" + "jsdoc": "^3.4.2", + "jsonwebtoken": "^7.1.9" }, "devDependencies": { "mocha": "^3.0.2", diff --git a/server.js b/server.js index 1abc7dd6..49717512 100644 --- a/server.js +++ b/server.js @@ -6,13 +6,20 @@ const createHandler = require('github-webhook-handler'); const GitHubApi = require('github'); const Configuration = require('./lib/configuration'); const Dispatcher = require('./lib/dispatcher'); +const installations = require('./lib/installations'); const PORT = process.env.PORT || 3000; const webhook = createHandler({path: '/', secret: process.env.WEBHOOK_SECRET || 'development'}); +// Cache installations +installations.load(); +// Listen for new installations +installations.listen(webhook); + const github = new GitHubApi({ debug: true }); + github.authenticate({ type: 'oauth', token: process.env.GITHUB_TOKEN diff --git a/test/fixtures/webhook/installation.deleted.json b/test/fixtures/webhook/installation.deleted.json new file mode 100644 index 00000000..c321d7c9 --- /dev/null +++ b/test/fixtures/webhook/installation.deleted.json @@ -0,0 +1,46 @@ +{ + "action": "deleted", + "installation": { + "id": 1729, + "account": { + "login": "bkeepers-inc", + "id": 11724939, + "avatar_url": "https://avatars.githubusercontent.com/u/11724939?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/bkeepers-inc", + "html_url": "https://github.com/bkeepers-inc", + "followers_url": "https://api.github.com/users/bkeepers-inc/followers", + "following_url": "https://api.github.com/users/bkeepers-inc/following{/other_user}", + "gists_url": "https://api.github.com/users/bkeepers-inc/gists{/gist_id}", + "starred_url": "https://api.github.com/users/bkeepers-inc/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/bkeepers-inc/subscriptions", + "organizations_url": "https://api.github.com/users/bkeepers-inc/orgs", + "repos_url": "https://api.github.com/users/bkeepers-inc/repos", + "events_url": "https://api.github.com/users/bkeepers-inc/events{/privacy}", + "received_events_url": "https://api.github.com/users/bkeepers-inc/received_events", + "type": "Organization", + "site_admin": false + }, + "access_tokens_url": "https://api.github.com/installations/1729/access_tokens", + "repositories_url": "https://api.github.com/installation/repositories" + }, + "sender": { + "login": "bkeepers", + "id": 173, + "avatar_url": "https://avatars.githubusercontent.com/u/173?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/bkeepers", + "html_url": "https://github.com/bkeepers", + "followers_url": "https://api.github.com/users/bkeepers/followers", + "following_url": "https://api.github.com/users/bkeepers/following{/other_user}", + "gists_url": "https://api.github.com/users/bkeepers/gists{/gist_id}", + "starred_url": "https://api.github.com/users/bkeepers/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/bkeepers/subscriptions", + "organizations_url": "https://api.github.com/users/bkeepers/orgs", + "repos_url": "https://api.github.com/users/bkeepers/repos", + "events_url": "https://api.github.com/users/bkeepers/events{/privacy}", + "received_events_url": "https://api.github.com/users/bkeepers/received_events", + "type": "User", + "site_admin": true + } +} diff --git a/test/installation.js b/test/installation.js new file mode 100644 index 00000000..310e4660 --- /dev/null +++ b/test/installation.js @@ -0,0 +1,20 @@ +const expect = require('expect'); +const installations = require('../lib/installations'); +const payload = require ('./fixtures/webhook/installation.deleted'); + +describe('installation', () => { + describe('register', () => { + it('registers the installation', () => { + installations.register(payload.installation); + expect(installations.for('bkeepers-inc')).toEqual(payload.installation); + }) + }); + + describe('unregister', () => { + it('unregisters the installation', () => { + installations.register(payload.installation); + installations.unregister(payload.installation); + expect(installations.for('bkeepers-inc')).toBe(undefined); + }) + }); +});