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: 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..ab5e935 100644 --- a/src/user/put.js +++ b/src/user/put.js @@ -8,6 +8,11 @@ export default async ({ body }, context, callback) => { } = JSON.parse(body); 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 969d036..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; @@ -65,7 +66,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 +125,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 +214,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 +228,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',