79 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			79 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| // Imports
 | |
| import * as jwt from 'jsonwebtoken';
 | |
| import {GithubPullRequests} from '../common/github-pull-requests';
 | |
| import {GithubTeams} from '../common/github-teams';
 | |
| import {assertNotMissingOrEmpty} from '../common/utils';
 | |
| import {UploadError} from './upload-error';
 | |
| 
 | |
| // Interfaces - Types
 | |
| interface JwtPayload {
 | |
|   slug: string;
 | |
|   'pull-request': number;
 | |
| }
 | |
| 
 | |
| // Classes
 | |
| export class BuildVerifier {
 | |
|   // Properties - Protected
 | |
|   protected githubPullRequests: GithubPullRequests;
 | |
|   protected githubTeams: GithubTeams;
 | |
| 
 | |
|   // Constructor
 | |
|   constructor(protected secret: string, githubToken: string, protected repoSlug: string, organization: string,
 | |
|               protected allowedTeamSlugs: string[]) {
 | |
|     assertNotMissingOrEmpty('secret', secret);
 | |
|     assertNotMissingOrEmpty('githubToken', githubToken);
 | |
|     assertNotMissingOrEmpty('repoSlug', repoSlug);
 | |
|     assertNotMissingOrEmpty('organization', organization);
 | |
|     assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join(''));
 | |
| 
 | |
|     this.githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
 | |
|     this.githubTeams = new GithubTeams(githubToken, organization);
 | |
|   }
 | |
| 
 | |
|   // Methods - Public
 | |
|   public getPrAuthorTeamMembership(pr: number): Promise<{author: string, isMember: boolean}> {
 | |
|     return Promise.resolve().
 | |
|       then(() => this.githubPullRequests.fetch(pr)).
 | |
|       then(prInfo => prInfo.user.login).
 | |
|       then(author => this.githubTeams.isMemberBySlug(author, this.allowedTeamSlugs).
 | |
|         then(isMember => ({author, isMember})));
 | |
|   }
 | |
| 
 | |
|   public verify(expectedPr: number, authHeader: string): Promise<void> {
 | |
|     return Promise.resolve().
 | |
|       then(() => this.extractJwtString(authHeader)).
 | |
|       then(jwtString => this.verifyJwt(expectedPr, jwtString)).
 | |
|       then(jwtPayload => this.verifyPr(jwtPayload['pull-request'])).
 | |
|       catch(err => { throw new UploadError(403, `Error while verifying upload for PR ${expectedPr}: ${err}`); });
 | |
|   }
 | |
| 
 | |
|   // Methods - Protected
 | |
|   protected extractJwtString(input: string): string {
 | |
|     return input.replace(/^token +/i, '');
 | |
|   }
 | |
| 
 | |
|   protected verifyJwt(expectedPr: number, token: string): Promise<JwtPayload> {
 | |
|     return new Promise((resolve, reject) => {
 | |
|       jwt.verify(token, this.secret, {issuer: 'Travis CI, GmbH'}, (err, payload: JwtPayload) => {
 | |
|         if (err) {
 | |
|           reject(err.message || err);
 | |
|         } else if (payload.slug !== this.repoSlug) {
 | |
|           reject(`jwt slug invalid. expected: ${this.repoSlug}`);
 | |
|         } else if (payload['pull-request'] !== expectedPr) {
 | |
|           reject(`jwt pull-request invalid. expected: ${expectedPr}`);
 | |
|         } else {
 | |
|           resolve(payload);
 | |
|         }
 | |
|       });
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   protected verifyPr(pr: number): Promise<void> {
 | |
|     return this.getPrAuthorTeamMembership(pr).
 | |
|       then(({author, isMember}) => isMember ? Promise.resolve() : Promise.reject(
 | |
|         `User '${author}' is not an active member of any of the following teams: ` +
 | |
|         `${this.allowedTeamSlugs.join(', ')}`,
 | |
|       ));
 | |
|   }
 | |
| }
 |