From 0d1cd1d2f02524e2ebe279fb1bbd29031e88f0a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Soko=C5=82owski?= Date: Fri, 20 Aug 2021 11:38:08 +0200 Subject: [PATCH] feat: add organizations whitelist to GitHub OAuth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently CodiMD does not support limiting access of GitHub OAuth users based on their organization membership. This is a very useful functionality for teams that want to limit write access to their notes. I've implemented a crude solution to this problem, which most probably requires some adjusments to make it better. I'm not sure if this implementation is kosher, but it definitely works on my deployment. Open to suggestions on how I can improve it. Signed-off-by: Jakub SokoĊ‚owski --- app.json | 8 ++++++++ config.json.example | 2 ++ lib/auth/github/index.js | 36 ++++++++++++++++++++++++++++++++++-- lib/config/default.js | 4 +++- lib/config/environment.js | 4 +++- scalingo.json | 8 ++++++++ 6 files changed, 58 insertions(+), 4 deletions(-) diff --git a/app.json b/app.json index 7c43a3da..b30d4876 100644 --- a/app.json +++ b/app.json @@ -80,6 +80,14 @@ "description": "GitHub API client secret", "required": false }, + "CMD_GITHUB_ORGANIZATIONS": { + "description": "GitHub whitelist of orgs", + "required": false + }, + "CMD_GITHUB_SCOPES": { + "description": "GitHub OAuth API scopes", + "required": false + }, "CMD_BITBUCKET_CLIENTID": { "description": "Bitbucket API client id", "required": false diff --git a/config.json.example b/config.json.example index 11422652..ea87aa2c 100644 --- a/config.json.example +++ b/config.json.example @@ -54,6 +54,8 @@ "github": { "clientID": "change this", "clientSecret": "change this" + "organizations": ["names of github organizations allowed, optional"], + "scopes": ["defaults to 'read:user' scope for auth user"], }, "gitlab": { "baseURL": "change this", diff --git a/lib/auth/github/index.js b/lib/auth/github/index.js index 8f05f122..fed850b3 100644 --- a/lib/auth/github/index.js +++ b/lib/auth/github/index.js @@ -1,12 +1,17 @@ 'use strict' const Router = require('express').Router +const request = require('request') const passport = require('passport') const GithubStrategy = require('passport-github').Strategy +const { InternalOAuthError } = require('passport-oauth2') const config = require('../../config') const response = require('../../response') const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') const { URL } = require('url') +const { promisify } = require('util') + +const rp = promisify(request) const githubAuth = module.exports = Router() @@ -15,20 +20,47 @@ function githubUrl (path) { } passport.use(new GithubStrategy({ + scope: (config.github.organizations ? + config.github.scopes.concat(['read:org']) : config.github.scope), clientID: config.github.clientID, clientSecret: config.github.clientSecret, callbackURL: config.serverURL + '/auth/github/callback', authorizationURL: githubUrl('login/oauth/authorize'), tokenURL: githubUrl('login/oauth/access_token'), userProfileURL: githubUrl('api/v3/user') -}, passportGeneralCallback)) +}, async (accessToken, refreshToken, profile, done) => { + if (!config.github.organizations) { + return passportGeneralCallback(accessToken, refreshToken, profile, done) + } + const { statusCode, body: data } = await rp({ + url: `https://api.github.com/user/orgs`, + method: 'GET', json: true, timeout: 2000, + headers: { + 'Authorization': `token ${accessToken}`, + 'User-Agent': 'nodejs-http' + } + }) + if (statusCode != 200) { + return done(InternalOAuthError( + `Failed to query organizations for user: ${profile.username}` + )) + } + const orgs = data.map(({login}) => login) + for (const org of orgs) { + if (config.github.organizations.includes(org)) { + return passportGeneralCallback(accessToken, refreshToken, profile, done) + } + } + return done(InternalOAuthError( + `User orgs not whitelisted: ${profile.username} (${orgs.join(',')})` + )) +})) githubAuth.get('/auth/github', function (req, res, next) { setReturnToFromReferer(req) passport.authenticate('github')(req, res, next) }) -// github auth callback githubAuth.get('/auth/github/callback', passport.authenticate('github', { successReturnToOrRedirect: config.serverURL + '/', diff --git a/lib/config/default.js b/lib/config/default.js index 488363b6..d276e46e 100644 --- a/lib/config/default.js +++ b/lib/config/default.js @@ -115,7 +115,9 @@ module.exports = { github: { enterpriseURL: undefined, // if you use github.com, not need to specify clientID: undefined, - clientSecret: undefined + clientSecret: undefined, + organizations: [], + scopes: ['read:user'] }, gitlab: { baseURL: undefined, diff --git a/lib/config/environment.js b/lib/config/environment.js index b0124a0d..5515d23c 100644 --- a/lib/config/environment.js +++ b/lib/config/environment.js @@ -69,7 +69,9 @@ module.exports = { github: { enterpriseURL: process.env.CMD_GITHUB_ENTERPRISE_URL, clientID: process.env.CMD_GITHUB_CLIENTID, - clientSecret: process.env.CMD_GITHUB_CLIENTSECRET + clientSecret: process.env.CMD_GITHUB_CLIENTSECRET, + organizations: toArrayConfig(process.env.CMD_GITHUB_ORGANIZATIONS), + scopes: toArrayConfig(process.env.CMD_GITHUB_SCOPES), }, bitbucket: { clientID: process.env.CMD_BITBUCKET_CLIENTID, diff --git a/scalingo.json b/scalingo.json index ab93f031..e3b86301 100644 --- a/scalingo.json +++ b/scalingo.json @@ -80,6 +80,14 @@ "description": "GitHub API client secret", "required": false }, + "CMD_GITHUB_ORGANIZATIONS": { + "description": "GitHub whitelist of orgs", + "required": false + }, + "CMD_GITHUB_SCOPES": { + "description": "GitHub OAuth API scopes", + "required": false + }, "CMD_BITBUCKET_CLIENTID": { "description": "Bitbucket API client id", "required": false