UNPKG

wuffle

Version:

A multi-repository task board for GitHub issues

318 lines (231 loc) 7.01 kB
import TreeCache from './TreeCache.js'; // 9 days const TTL = 1000 * 60 * 60 * 24 * 9; /** * This component provides the functionality to filter * issues based on user views. * * @constructor * * @param {import('../../types.js').Logger} logger * @param {import('../github-client/GithubClient.js').default} githubClient * @param {import('../../events.js').default} events * @param {import('../webhook-events/WebhookEvents.js').default} webhookEvents */ export default function UserAccess( logger, githubClient, events, webhookEvents ) { const log = logger.child({ name: 'wuffle:user-access' }); const cache = new TreeCache(TTL); function getIssueRepository(issue) { const { key, repository } = issue; if (!repository) { throw new Error(`missing repository meta-data for issue (key=${ key })`); } return repository; } async function fetchUserInstallations(user) { const octokit = await githubClient.getUserScoped(user); return octokit.paginate( octokit.apps.listInstallationsForAuthenticatedUser ); } async function fetchUserRepositories(user) { const installations = await getUserInstallations(user); const repositoriesByInstallation = await Promise.all( installations.map(installation => getUserRepositoriesForInstallation(user, installation)) ); return [].concat(...repositoriesByInstallation); } async function fetchUserRepositoriesForInstallation(user, installation) { const octokit = await githubClient.getUserScoped(user); return octokit.paginate( octokit.apps.listInstallationReposForAuthenticatedUser, { installation_id: installation.id, per_page: 100 }, response => response.data ); } function getUserRepositoriesForInstallation(user, installation) { return cache.get(`login=${user.login}:installation_repositories=${installation.id}`, () => { return fetchUserRepositoriesForInstallation(user, installation); }); } function getUserInstallations(user) { return cache.get(`login=${user.login}:installations`, () => { return fetchUserInstallations(user); }); } function getUserRepositories(user) { return cache.get(`login=${user.login}:repositories`, () => { return fetchUserRepositories(user); }); } function getUserVisibleRepositoryNames(user) { return getUserRepositories(user).then(repositories => repositories.map(repo => { return repo.full_name; })); } /** * Show publicly accessible issues only. */ function filterPublic(issue) { return !getIssueRepository(issue).private; } /** * Filter issues and PRs that are member of the given * repositories. */ async function createMemberFilter(repositories) { const repositoryMap = repositories.reduce((map, repo) => { map[repo] = true; return map; }, {}); return function filterPrivate(issue) { if (filterPublic(issue)) { return true; } const repository = getIssueRepository(issue); return fullName(repository) in repositoryMap; }; } function createReadFilter(user) { const { login } = user; const t = Date.now(); log.debug({ login }, 'creating read filter'); return getUserVisibleRepositoryNames(user).then(repositoryNames => { log.debug({ login, repositories: repositoryNames }, 'creating member filter'); return createMemberFilter(repositoryNames); }).finally(() => { log.info({ login, t: Date.now() - t }, 'created read filter'); }); } function getReadFilter(user) { if (!user) { return Promise.resolve(filterPublic); } const { login } = user; return cache.get(`login=${login}:read_filter`, () => createReadFilter(user)); } function canWrite(user, repoAndOwner) { const { login } = user; const { repo, owner } = repoAndOwner; return githubClient.getOrgScoped(owner) .then(octokit => { return octokit.repos.getCollaboratorPermissionLevel({ repo, owner, username: login }); }).then(res => { const { permission } = res.data; return ( permission === 'write' || permission === 'admin' ); }).catch(err => { log.warn({ login, owner, repo, err }, 'failed to determine write status'); return false; }); } // api //////////////////// this.getReadFilter = getReadFilter; this.canWrite = canWrite; // behavior /////////////// if (process.env.NODE_ENV !== 'test') { events.once('wuffle.start', function() { setInterval(() => { cache.evict(); }, 1000 * 60 * 15); }); } // https://developer.github.com/v3/activity/events/types/#githubappauthorizationevent webhookEvents.on([ 'github_app_authorization.revoked' ], (context) => { const { sender: { login } } = context.payload; cache.invalidate(`login=${login}:*`); }); // https://developer.github.com/v3/activity/events/types/#installationrepositoriesevent webhookEvents.on([ 'installation_repositories.added', 'installation_repositories.removed' ], function(context) { const { installation: { id: installation_id } } = context.payload; const installationMatches = cache.match('login=*:installations'); for (const match of installationMatches) { const { match: [ login ], value: installations } = match; const isInstallationMember = installations.find( installation => installation.id === installation_id ); if (isInstallationMember) { cache.invalidate(`login=${login}:installation_repositories=${installation_id}`); cache.invalidate(`login=${login}:repositories`); cache.invalidate(`login=${login}:read_filter`); } } }); // https://developer.github.com/v3/activity/events/types/#installationevent webhookEvents.on([ 'installation.created', 'installation.deleted' ], function(context) { cache.invalidate('login=*:installations'); cache.invalidate('login=*:installation_repositories=*'); cache.invalidate('login=*:repositories'); cache.invalidate('login=*:read_filter'); }); // https://developer.github.com/v3/activity/events/types/#memberevent webhookEvents.on([ 'member' ], function(context) { const { member: { login } } = context.payload; cache.invalidate(`login=${login}:installations`); cache.invalidate(`login=${login}:installation_repositories=*`); cache.invalidate(`login=${login}:repositories`); cache.invalidate(`login=${login}:read_filter`); }); } // helpers ////////////// function fullName(repository) { return `${repository.owner.login}/${repository.name}`; }