From 504d067aba140c7c8bd618c77cd591485458f141 Mon Sep 17 00:00:00 2001 From: Jon Sharratt Date: Tue, 21 Feb 2017 20:06:36 +0000 Subject: [PATCH 1/3] Add option to restrict orgs This allows users wanting to use public github the ability to limit access to their npm registry by whitelisting orgs via environment variable YITH_RESTRICTED_ORGS. --- serverless.yml | 1 + src/authorizers/github.js | 32 +++++++++++++++++++++++++++++++- src/user/put.js | 2 +- test/user/put.test.js | 8 ++++---- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/serverless.yml b/serverless.yml index 41c56df..ceeb22c 100644 --- a/serverless.yml +++ b/serverless.yml @@ -17,6 +17,7 @@ provider: region: ${env:CODEBOX_REGION} environment: admins: ${env:CODEBOX_ADMINS} + restrictedOrgs: ${env:CODEBOX_RESTRICTED_ORGS} registry: ${env:CODEBOX_REGISTRY} githubUrl: ${env:CODEBOX_GITHUB_URL} githubClientId: ${env:CODEBOX_GITHUB_CLIENT_ID} diff --git a/src/authorizers/github.js b/src/authorizers/github.js index 99f0741..e2201e0 100644 --- a/src/authorizers/github.js +++ b/src/authorizers/github.js @@ -81,12 +81,42 @@ export default async ({ methodArn, authorizationToken }, context, callback) => { }); let isAdmin = false; + let effect = 'Allow'; + let restrictedOrgs = []; + + if (process.env.restrictedOrgs) { + restrictedOrgs = process.env.restrictedOrgs.split(','); + } + + if (restrictedOrgs.length) { + try { + github.authenticate({ + type: 'token', + token, + }); + + const orgs = await github.users.getOrgMemberships({ + state: 'active', + }); + + const usersOrgs = orgs.filter(org => restrictedOrgs.indexOf(org.organization.login) > -1); + effect = usersOrgs.length ? 'Allow' : 'Deny'; + } catch (githubError) { + return callback(null, generatePolicy({ + token: tokenParts[1], + effect: 'Deny', + methodArn, + isAdmin: false, + })); + } + } + if (process.env.admins) { isAdmin = process.env.admins.split(',').indexOf(user.login) > -1; } const policy = generatePolicy({ - effect: 'Allow', + effect, methodArn, token, isAdmin, diff --git a/src/user/put.js b/src/user/put.js index 0039625..f08eae0 100644 --- a/src/user/put.js +++ b/src/user/put.js @@ -7,7 +7,7 @@ export default async ({ body }, context, callback) => { password, } = JSON.parse(body); - const scopes = ['user:email']; + const scopes = ['user:email', 'read:org']; const nameParts = name.split('.'); const username = nameParts[0]; const otp = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; diff --git a/test/user/put.test.js b/test/user/put.test.js index 969d036..4d759b3 100644 --- a/test/user/put.test.js +++ b/test/user/put.test.js @@ -65,7 +65,7 @@ describe('PUT /registry/-/user/{id}', () => { await subject(event, stub(), callback); assert(getCreateAuthStub.calledWithExactly({ - scopes: ['user:email'], + scopes: ['user:email', 'read:org'], client_id: 'foo-client-id', client_secret: 'bar-secret', note: 'codebox private npm registry', @@ -124,7 +124,7 @@ describe('PUT /registry/-/user/{id}', () => { await subject(event, stub(), callback); assert(getCreateAuthStub.calledWithExactly({ - scopes: ['user:email'], + scopes: ['user:email', 'read:org'], client_id: 'foo-client-id', client_secret: 'bar-secret', note: 'codebox private npm registry', @@ -213,7 +213,7 @@ describe('PUT /registry/-/user/{id}', () => { await subject(event, stub(), callback); assert(createAuthStub.calledWithExactly({ - scopes: ['user:email'], + scopes: ['user:email', 'read:org'], client_id: 'foo-client-id', client_secret: 'bar-secret', note: 'codebox private npm registry', @@ -227,7 +227,7 @@ describe('PUT /registry/-/user/{id}', () => { await subject(event, stub(), callback); assert(getCreateAuthStub.calledWithExactly({ - scopes: ['user:email'], + scopes: ['user:email', 'read:org'], client_id: 'foo-client-id', client_secret: 'bar-secret', note: 'codebox private npm registry', From 804d5ca6995c1434afca8986ff8ab1d14ac61482 Mon Sep 17 00:00:00 2001 From: Jon Sharratt Date: Sun, 5 Mar 2017 12:12:07 +0000 Subject: [PATCH 2/3] Ensure test coverage for restricted orgs --- src/user/put.js | 7 +- test/authorizers/github.test.js | 185 +++++++++++++++++++++++++++++++- test/user/put.test.js | 1 + 3 files changed, 191 insertions(+), 2 deletions(-) diff --git a/src/user/put.js b/src/user/put.js index f08eae0..ab5e935 100644 --- a/src/user/put.js +++ b/src/user/put.js @@ -7,7 +7,12 @@ export default async ({ body }, context, callback) => { password, } = JSON.parse(body); - const scopes = ['user:email', 'read:org']; + const scopes = ['user:email']; + + if (process.env.restrictedOrgs) { + scopes.push('read:org'); + } + const nameParts = name.split('.'); const username = nameParts[0]; const otp = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; diff --git a/test/authorizers/github.test.js b/test/authorizers/github.test.js index c6838f1..6259c58 100644 --- a/test/authorizers/github.test.js +++ b/test/authorizers/github.test.js @@ -13,7 +13,6 @@ describe('GitHub Authorizer', () => { githubClientId: 'foo-client-id', githubSecret: 'bar-secret', githubUrl: 'https://example.com', - admins: '', }; process.env = env; @@ -119,6 +118,190 @@ describe('GitHub Authorizer', () => { }); describe('valid access token', () => { + context('is in restricted org', () => { + let authStub; + let getOrgMembershipsStub; + + beforeEach(() => { + process.env.admins = ''; + process.env.restrictedOrgs = 'foo-org'; + + event = { + authorizationToken: 'Bearer foo-valid-token', + methodArn: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry', + }; + + gitHubSpy = spy(() => { + gitHubInstance = createStubInstance(GitHub); + authStub = stub(); + getOrgMembershipsStub = stub().returns([{ + organization: { + login: 'foo-org', + }, + }]); + + const checkAuthStub = stub().returns({ + user: { + login: 'foo-user', + avatar_url: 'https://example.com', + }, + created_at: '2001-01-01T00:00:00Z', + updated_at: '2001-02-01T00:00:00Z', + }); + + gitHubInstance.authenticate = authStub; + gitHubInstance.authorization = { + check: checkAuthStub, + }; + gitHubInstance.users = { + getOrgMemberships: getOrgMembershipsStub, + }; + + return gitHubInstance; + }); + + subject.__Rewire__({ + GitHub: gitHubSpy, + }); + }); + + it('should get users organizations', async () => { + await subject(event, stub(), callback); + + assert(getOrgMembershipsStub.calledWithExactly({ + state: 'active', + })); + }); + + it('should only allow get access', async () => { + await subject(event, stub(), callback); + + assert(callback.calledWithExactly(null, { + principalId: 'foo-valid-token', + policyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'execute-api:Invoke', + Effect: 'Allow', + Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry*', + }, + { + Action: 'execute-api:Invoke', + Effect: 'Deny', + Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/PUT/registry*', + }, + { + Action: 'execute-api:Invoke', + Effect: 'Deny', + Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/DELETE/registry*', + }, + ], + }, + context: { + username: 'foo-user', + avatar: 'https://example.com', + createdAt: '2001-01-01T00:00:00Z', + updatedAt: '2001-02-01T00:00:00Z', + }, + })); + }); + + afterEach(() => { + subject.__ResetDependency__('GitHub'); + }); + }); + + context('not in restricted org', () => { + let authStub; + let getOrgMembershipsStub; + + beforeEach(() => { + process.env.admins = ''; + process.env.restrictedOrgs = 'foo-org'; + + event = { + authorizationToken: 'Bearer foo-valid-token', + methodArn: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry', + }; + + gitHubSpy = spy(() => { + gitHubInstance = createStubInstance(GitHub); + authStub = stub(); + getOrgMembershipsStub = stub().returns([]); + + const checkAuthStub = stub().returns({ + user: { + login: 'foo-user', + avatar_url: 'https://example.com', + }, + created_at: '2001-01-01T00:00:00Z', + updated_at: '2001-02-01T00:00:00Z', + }); + + gitHubInstance.authenticate = authStub; + gitHubInstance.authorization = { + check: checkAuthStub, + }; + gitHubInstance.users = { + getOrgMemberships: getOrgMembershipsStub, + }; + + return gitHubInstance; + }); + + subject.__Rewire__({ + GitHub: gitHubSpy, + }); + }); + + it('should get users organizations', async () => { + await subject(event, stub(), callback); + + assert(getOrgMembershipsStub.calledWithExactly({ + state: 'active', + })); + }); + + it('should deny get, put and delete', async () => { + await subject(event, stub(), callback); + + assert(callback.calledWithExactly(null, { + principalId: 'foo-valid-token', + policyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'execute-api:Invoke', + Effect: 'Deny', + Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/GET/registry*', + }, + { + Action: 'execute-api:Invoke', + Effect: 'Deny', + Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/PUT/registry*', + }, + { + Action: 'execute-api:Invoke', + Effect: 'Deny', + Resource: 'arn:aws:execute-api:foo-region:bar-account:baz-api/foo-stage/DELETE/registry*', + }, + ], + }, + context: { + username: 'foo-user', + avatar: 'https://example.com', + createdAt: '2001-01-01T00:00:00Z', + updatedAt: '2001-02-01T00:00:00Z', + }, + })); + }); + + afterEach(() => { + subject.__ResetDependency__('GitHub'); + }); + }); + context('not an adminstrator', () => { let authStub; let checkAuthStub; diff --git a/test/user/put.test.js b/test/user/put.test.js index 4d759b3..e7b27b0 100644 --- a/test/user/put.test.js +++ b/test/user/put.test.js @@ -13,6 +13,7 @@ describe('PUT /registry/-/user/{id}', () => { githubClientId: 'foo-client-id', githubSecret: 'bar-secret', githubUrl: 'https://example.com', + restrictedOrgs: 'foo-org', }; process.env = env; From 1cddc66aee208e0ace40ce07f15c79dd7679e12f Mon Sep 17 00:00:00 2001 From: Jon Sharratt Date: Sun, 5 Mar 2017 12:15:42 +0000 Subject: [PATCH 3/3] Update README for CODEBOX_RESTRICTED_ORGS env var --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2bb2ec4..39d1a78 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ export CODEBOX_BUCKET="my-npm-registry-storage" # The name of the bucket in whic export CODEBOX_GITHUB_URL="https://api.github.com/" # The GitHub / GitHub Enterprise **api** url export CODEBOX_GITHUB_CLIENT_ID="client_id" # The client id for your GitHub application export CODEBOX_GITHUB_SECRET="secret" # The secret for your GitHub application +export CODEBOX_RESTRICTED_ORGS="" # OPTIONAL: Comma seperated list of github organisations to only allow access to users in that org (e.g. "craftship,myorg"). Useful if using public GitHub for authentication, as by default all authenticated users would have access. ``` * `serverless deploy --stage prod` (pick which ever stage you wish) * `npm set registry ` - `` being the base url shown in the terminal after deployment completes, such as: