Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <url>` - `<url>` being the base url shown in the terminal after deployment completes, such as:
Expand Down
1 change: 1 addition & 0 deletions serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
32 changes: 31 additions & 1 deletion src/authorizers/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/user/put.js
Original file line number Diff line number Diff line change
Expand Up @@ -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] : '';
Expand Down
185 changes: 184 additions & 1 deletion test/authorizers/github.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ describe('GitHub Authorizer', () => {
githubClientId: 'foo-client-id',
githubSecret: 'bar-secret',
githubUrl: 'https://example.com',
admins: '',
};

process.env = env;
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 5 additions & 4 deletions test/user/put.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down