Skip to content

Commit

Permalink
Merge pull request #321 from cardstack/github-user-groups
Browse files Browse the repository at this point in the history
GitHub user permissions support and user caching
  • Loading branch information
habdelra committed Sep 25, 2018
2 parents fefa681 + 82e29b9 commit 2f88aae
Show file tree
Hide file tree
Showing 8 changed files with 595 additions and 56 deletions.
2 changes: 1 addition & 1 deletion packages/github-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This README outlines the details of collaborating on this Ember addon.

To run the demo app and interact with GitHub, you need to register your app via https://github.com/settings/developers and get a client ID & secret. Set them via the environment variables `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`. This allows this plugin to act as an OAuth2 client that speaks to GitHub on behalf of users who authorize it.

For the present, you also need a user token that the Hub can use to represent itself. You can use a [Personal Access Token](https://github.com/settings/tokens). Set is via the environment variable `GITHUB_TOKEN`.
For the present, you also need a user token that the Hub can use to represent itself. You can use a [Personal Access Token](https://github.com/settings/tokens). Make sure that this token has `repo` and `user` scope, as well as push permissions on the GitHub repos that you configure to use with the github-auth plugin datasource. This allows the plugin to have sufficient privileges to be able to query the repo for collaborator permissions. Set is via the environment variable `GITHUB_TOKEN`.

* `ember serve`
* Visit your app at [http://localhost:4200](http://localhost:4200).
Expand Down
3 changes: 2 additions & 1 deletion packages/github-auth/cardstack/authenticator.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ module.exports = class {
attributes: {
name: user.name,
email: user.email,
"avatar-url": user.avatar_url
"avatar-url": user.avatar_url,
permissions: user.permissions || []
}
}
};
Expand Down
78 changes: 75 additions & 3 deletions packages/github-auth/cardstack/searcher.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
const request = require('./lib/request');
const { get, groupBy } = require('lodash');

const githubPermissions = {
admin: ['read', 'write', 'admin'],
write: ['read', 'write'],
read: ['read']
};

module.exports = class GitHubSearcher {
static create(...args) {
return new this(...args);
}
constructor({ token, dataSource }) {
constructor(opts) {
let { token, dataSource, permissions } = opts;
this.token = token;
this.dataSource = dataSource;
this.permissions = permissions;
this.cacheMaxAge = opts['cache-max-age'];
}

async get(session, branch, type, id, next) {
let result = await await next();
if (result) {
return result;
}

if (type === 'github-users') {
return this._getUser(id);
}
return next();
}

async search(session, branch, query, next) {
Expand All @@ -33,6 +47,64 @@ module.exports = class GitHubSearcher {
}
};
let response = await request(options);
return await this.dataSource.rewriteExternalUser(response.body);
let userData = response.body;
if (!userData) { return; }

userData.permissions = await this._getPermissions(userData.login);
let user = await this.dataSource.rewriteExternalUser(userData);

let maxAge = this.cacheMaxAge;
if (maxAge == null) {
let cacheControl = get(response, 'response.headers.cache-control') ||
get(response, 'response.headers.Cache-Control');
if (cacheControl) {
let match = /max-age=(\d+)/.exec(cacheControl);
if (match && match.length > 1) {
maxAge = parseInt(match[1], 10);
}
}
}

if (maxAge) {
user.meta = user.meta || {};
user.meta['cardstack-cache-control'] = { 'max-age': maxAge };
}

return user;
}

async _getPermissions(username) {
if (!username || !this.permissions || !this.permissions.length) { return; }

let permissions = [];
let repos = groupBy(this.permissions, 'repo');

for (let repo of Object.keys(repos)) {
let options = {
hostname: 'api.github.com',
port: 443,
path: `/repos/${repo}/collaborators/${username}/permission`,
method: 'GET',
headers: {
'Accept': 'application/json',
'User-Agent': '@cardstack/github-auth',
Authorization: `token ${this.token}`
}
};
let response = await request(options);
let userPermission = get(response, 'body.permission');
if (!userPermission) { continue; }

let userExpandedPermissions = githubPermissions[userPermission];
if (!userExpandedPermissions) { continue; }

for (let repoPermission of repos[repo]) {
if (userExpandedPermissions.includes(repoPermission.permission)) {
permissions.push(repoPermission);
}
}
}

return permissions;
}
};
10 changes: 9 additions & 1 deletion packages/github-auth/cardstack/static-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ module.exports = function({ dataSource, provideUserSchema }) {
'fields': { data: [
{ type: 'fields', id: 'name' },
{ type: 'fields', id: 'email' },
{ type: 'fields', id: 'avatar-url' }
{ type: 'fields', id: 'avatar-url' },
{ type: 'fields', id: 'permissions' }
] }
}
},
Expand All @@ -45,5 +46,12 @@ module.exports = function({ dataSource, provideUserSchema }) {
'field-type': '@cardstack/core-types::string'
}
},
{
type: 'fields',
id: 'permissions',
attributes: {
'field-type': '@cardstack/core-types::object'
}
},
];
};
48 changes: 31 additions & 17 deletions packages/github-auth/node-tests/authenticator-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const JSONAPIFactory = require('@cardstack/test-support/jsonapi-factory');
const {
validAccessToken,
githubUser,
githubUsersResponse
githubUsersResponse,
githubReadPermissions
} = require('./fixtures/github-responses');

describe('github-auth/authenticator', function() {
Expand All @@ -25,12 +26,21 @@ describe('github-auth/authenticator', function() {
mayLogin: true
});

factory.addResource('grants')
.withRelated('who', [{ type: 'groups', id: 'everyone' }])
.withRelated('types', [{ type: 'content-types', id: 'github-users' }])
.withAttributes({
'may-read-resource': true,
'may-read-fields': true
});

factory.addResource('data-sources', 'github').withAttributes({
sourceType: '@cardstack/github-auth',
params: {
'client-id': 'mock-github-client-id',
'client-secret': 'mock-github-client-secret',
token: 'mock-github-token'
token: 'mock-github-token',
permissions: [{ repo: 'cardstack/repo1', permission: 'read' }]
}
});

Expand All @@ -44,36 +54,40 @@ describe('github-auth/authenticator', function() {
await destroyDefaultEnvironment(env);
}

before(setup);
after(teardown);
beforeEach(setup);
afterEach(teardown);

it('returns github-users document for an authenticated github session', async function() {
let { login:id } = githubUser;

it('returns token for an authenticated github session', async function() {
nock('https://github.com')
.post('/login/oauth/access_token')
.reply(200, validAccessToken);

nock('https://api.github.com')
.get('/user')
.reply(function() {
return [ 200, githubUser, {
'Cache-Control': 'private, max-age=60, s-maxage=60'
}];
});
.reply(200, githubUser);

nock('https://api.github.com')
.get('/users/habdelra')
.reply(function() {
return [ 200, githubUsersResponse, {
'Cache-Control': 'private, max-age=60, s-maxage=60'
}];
});
.get(`/users/${id}`)
.reply(200, githubUsersResponse);

nock('https://api.github.com')
.get(`/repos/cardstack/repo1/collaborators/${id}/permission`)
.reply(200, githubReadPermissions);

let response = await request.post('/auth/github').send({ authorizationCode: 'authToken' });

expect(response).hasStatus(200);
expect(response.body).has.deep.property('data.type', 'github-users');
expect(response.body).has.deep.property('data.id', 'habdelra');
expect(response.body).has.deep.property('data.id', id);
expect(response.body).has.deep.property('data.meta.source', 'github');
expect(response.body.data.attributes).to.deep.equal({
"name": "Hassan Abdel-Rahman",
"email": "hassan@cardstack.com",
"avatar-url": "https://avatars2.githubusercontent.com/u/61075?v=4",
"permissions": [{ "repo": "cardstack/repo1", "permission": "read" }]
});
expect(response.body.data.meta.token).is.ok;
expect(response.body.data.meta.validUntil).is.ok;
});
Expand Down
94 changes: 93 additions & 1 deletion packages/github-auth/node-tests/fixtures/github-responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,97 @@ module.exports = {
"collaborators": 0,
"private_repos": 0
}
}
},
githubAdminPermissions: {
"permission": "admin",
"user": {
"login": "habdelra",
"id": 61076,
"node_id": "MDQ6VXNlcjYxMDc1",
"avatar_url": "https://avatars2.githubusercontent.com/u/61075?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/habdelra",
"html_url": "https://github.com/habdelra",
"followers_url": "https://api.github.com/users/habdelra/followers",
"following_url": "https://api.github.com/users/habdelra/following{/other_user}",
"gists_url": "https://api.github.com/users/habdelra/gists{/gist_id}",
"starred_url": "https://api.github.com/users/habdelra/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/habdelra/subscriptions",
"organizations_url": "https://api.github.com/users/habdelra/orgs",
"repos_url": "https://api.github.com/users/habdelra/repos",
"events_url": "https://api.github.com/users/habdelra/events{/privacy}",
"received_events_url": "https://api.github.com/users/habdelra/received_events",
"type": "User",
"site_admin": false
}
},
githubWritePermissions: {
"permission": "write",
"user": {
"login": "habdelra",
"id": 61076,
"node_id": "MDQ6VXNlcjYxMDc1",
"avatar_url": "https://avatars2.githubusercontent.com/u/61075?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/habdelra",
"html_url": "https://github.com/habdelra",
"followers_url": "https://api.github.com/users/habdelra/followers",
"following_url": "https://api.github.com/users/habdelra/following{/other_user}",
"gists_url": "https://api.github.com/users/habdelra/gists{/gist_id}",
"starred_url": "https://api.github.com/users/habdelra/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/habdelra/subscriptions",
"organizations_url": "https://api.github.com/users/habdelra/orgs",
"repos_url": "https://api.github.com/users/habdelra/repos",
"events_url": "https://api.github.com/users/habdelra/events{/privacy}",
"received_events_url": "https://api.github.com/users/habdelra/received_events",
"type": "User",
"site_admin": false
}
},
githubReadPermissions: {
"permission": "read",
"user": {
"login": "habdelra",
"id": 61076,
"node_id": "MDQ6VXNlcjYxMDc1",
"avatar_url": "https://avatars2.githubusercontent.com/u/61075?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/habdelra",
"html_url": "https://github.com/habdelra",
"followers_url": "https://api.github.com/users/habdelra/followers",
"following_url": "https://api.github.com/users/habdelra/following{/other_user}",
"gists_url": "https://api.github.com/users/habdelra/gists{/gist_id}",
"starred_url": "https://api.github.com/users/habdelra/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/habdelra/subscriptions",
"organizations_url": "https://api.github.com/users/habdelra/orgs",
"repos_url": "https://api.github.com/users/habdelra/repos",
"events_url": "https://api.github.com/users/habdelra/events{/privacy}",
"received_events_url": "https://api.github.com/users/habdelra/received_events",
"type": "User",
"site_admin": false
}
},
githubNoPermissions: {
"permission": "none",
"user": {
"login": "habdelra",
"id": 61076,
"node_id": "MDQ6VXNlcjYxMDc1",
"avatar_url": "https://avatars2.githubusercontent.com/u/61075?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/habdelra",
"html_url": "https://github.com/habdelra",
"followers_url": "https://api.github.com/users/habdelra/followers",
"following_url": "https://api.github.com/users/habdelra/following{/other_user}",
"gists_url": "https://api.github.com/users/habdelra/gists{/gist_id}",
"starred_url": "https://api.github.com/users/habdelra/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/habdelra/subscriptions",
"organizations_url": "https://api.github.com/users/habdelra/orgs",
"repos_url": "https://api.github.com/users/habdelra/repos",
"events_url": "https://api.github.com/users/habdelra/events{/privacy}",
"received_events_url": "https://api.github.com/users/habdelra/received_events",
"type": "User",
"site_admin": false
}
},
};

0 comments on commit 2f88aae

Please sign in to comment.