Skip to content

Commit

Permalink
feat: add github-server-app plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
aarlaud committed May 15, 2024
1 parent 9bf4293 commit cb7da75
Show file tree
Hide file tree
Showing 9 changed files with 495 additions and 30 deletions.
4 changes: 4 additions & 0 deletions config.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@
},
"required": {
"GITHUB": "ghe.yourdomain.com",
"GITHUB_APP_ID": "APP_ID",
"GITHUB_APP_PRIVATE_PEM_PATH": "abc",
"GITHUB_APP_CLIENT_ID": "123",
"GITHUB_APP_INSTALLATION_ID": "123",
"BROKER_CLIENT_URL": "https://<broker.client.hostname>:<port>"
}
},
Expand Down
16 changes: 15 additions & 1 deletion defaultFilters/github-server-app.json
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,16 @@
}
],
"private": [
{
"//": "look up repositories installation can access",
"method": "GET",
"path": "/installation/repositories",
"origin": "https://${GITHUB_API}",
"auth": {
"scheme": "bearer",
"token": "${ACCESS_TOKEN}"
}
},
{
"//": "search for user's repos",
"method": "GET",
Expand Down Expand Up @@ -1287,7 +1297,11 @@
"//": "get details of the repo",
"method": "GET",
"path": "/repos/:name/:repo",
"origin": "https://${GITHUB_API}"
"origin": "https://${GITHUB_API}",
"auth": {
"scheme": "bearer",
"token": "${ACCESS_TOKEN}"
}
},
{
"//": "get the details of the commit to determine its SHA",
Expand Down
5 changes: 4 additions & 1 deletion lib/client/brokerClientPlugins/pluginManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ export const runStartupPlugins = async (clientOpts) => {
string,
BrokerPlugin[]
>;
const connectionsKeys = Object.keys(clientOpts.config.connections);

const connectionsKeys = clientOpts.config.connections
? Object.keys(clientOpts.config.connections)
: [];

for (const connectionKey of connectionsKeys) {
if (
Expand Down
228 changes: 200 additions & 28 deletions lib/client/brokerClientPlugins/plugins/githubServerAppAuth.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
// import { PostFilterPreparedRequest } from '../../../common/relay/prepareRequest';
import { readFileSync } from 'node:fs';
import BrokerPlugin from '../abstractBrokerPlugin';

import { createPrivateKey } from 'node:crypto';
import { sign } from 'jsonwebtoken';
import { PostFilterPreparedRequest } from '../../../common/relay/prepareRequest';
import { makeRequestToDownstream } from '../../../common/http/request';
export class Plugin extends BrokerPlugin {
// Plugin Code and Name must be unique across all plugins.
pluginCode = 'GITHUB_SERVER_APP';
pluginCode = 'GITHUB_SERVER_APP_PLUGIN';
pluginName = 'Github Server App Authentication Plugin';
description = `
Plugin to retrieve and manage credentials for Brokered Github Server App installs
`;
version = '0.1';
applicableBrokerTypes = ['github-server-app']; // Must match broker types
JWT_TTL = 10 * 60 * 1000;

// Provide a way to include specific conditional logic to execute
isPluginActive(): boolean {
Expand All @@ -23,38 +28,205 @@ export class Plugin extends BrokerPlugin {
// Function running upon broker client startup
// Useful for credentials retrieval, initial setup, etc...
async startUp(connectionConfig): Promise<void> {
this.logger.info({ plugin: this.pluginName }, 'Running Startup');
this.logger.info(
{ config: connectionConfig },
'Connection Config passed to the plugin',
);
// const data = {
// install_id: connectionConfig.GITHUB_APP_INSTALL_ID,
// client_id: connectionConfig.GITHUB_CLIENT_ID,
// client_secret: connectionConfig.GITHUB_CLIENT_SECRET,
// };
// const formData = new URLSearchParams(data);

// this.request = {
// url: `https://${connectionConfig.GITHUB_API}/oauth/path`,
// headers: {
// 'Content-Type': 'application/x-www-form-urlencoded',
// },
// method: 'POST',
// body: formData.toString(),
// };
// const response = await this.makeRequestToDownstream(this.request);
// if (response.statusCode && response.statusCode > 299) {
// throw Error('Error making request');
// }
try {
this.logger.info({ plugin: this.pluginName }, 'Running Startup');
this.logger.debug(
{ plugin: this.pluginCode, config: connectionConfig },
'Connection Config passed to the plugin',
);

// Generate the JWT
const now = Date.now();
connectionConfig.JWT_TOKEN = this._getJWT(
Math.floor(now / 1000), // Current time in seconds
connectionConfig.GITHUB_APP_PRIVATE_PEM_PATH,
connectionConfig.GITHUB_APP_CLIENT_ID,
);
this._setJWTLifecycleHandler(now, connectionConfig);

connectionConfig.accessToken = await this._getAccessToken(
connectionConfig.GITHUB_API,
connectionConfig.GITHUB_APP_INSTALLATION_ID,
connectionConfig.JWT_TOKEN,
);
connectionConfig.ACCESS_TOKEN = JSON.parse(
connectionConfig.accessToken,
).token;

this._setAccessTokenLifecycleHandler(connectionConfig);
} catch (err) {
this.logger.err(
{ err },
`Error in ${this.pluginName}-${this.pluginCode} startup.`,
);
throw Error(
`Error in ${this.pluginName}-${this.pluginCode} startup. ${err}`,
);
}
}

_getJWT(
nowInSeconds: number,
privatePemPath: string,
githubAppClientId: string,
): string {
// Read the contents of the PEM file
const privatePem = readFileSync(privatePemPath, 'utf8');
const privateKey = createPrivateKey(privatePem);

const payload = {
iat: nowInSeconds - 60, // Issued at time (60 seconds in the past)
exp: nowInSeconds + this.JWT_TTL / 1000, // Expiration time (10 minutes from now)
iss: githubAppClientId, // GitHub App's client ID
};

// Generate the JWT
return sign(payload, privateKey, { algorithm: 'RS256' });
}

_setJWTLifecycleHandler(now: number, connectionConfig) {
try {
if (connectionConfig.JWT_TOKEN) {
let timeoutHandlerId;
let timeoutHandler = async () => {};
timeoutHandler = async () => {
try {
this.logger.debug(
{ plugin: this.pluginCode },
'Refreshing github app JWT token',
);
clearTimeout(timeoutHandlerId);
const timeoutHandlerNow = Date.now();
connectionConfig.JWT_TOKEN = await this._getJWT(
Math.floor(timeoutHandlerNow / 1000),
connectionConfig.GITHUB_APP_PRIVATE_PEM_PATH,
connectionConfig.GITHUB_APP_CLIENT_ID,
);
timeoutHandlerId = setTimeout(
timeoutHandler,
this._getTimeDifferenceInMsToFutureDate(
timeoutHandlerNow + this.JWT_TTL,
) - 10000,
);
connectionConfig.jwtTimeoutHandlerId = timeoutHandlerId;
} catch (err) {
this.logger.error(
{ plugin: this.pluginCode, err },
`Error refreshing JWT`,
);
throw new Error(`${err}`);
}
};

timeoutHandlerId = setTimeout(
timeoutHandler,
this._getTimeDifferenceInMsToFutureDate(now + this.JWT_TTL) - 10000,
);
connectionConfig.jwtTimeoutHandlerId = timeoutHandlerId;
}
} catch (err) {
this.logger.error(
{ plugin: this.pluginCode, err },
`Error setting JWT lifecycle handler.`,
);
throw new Error(`${err}`);
}
}

async _getAccessToken(
endpointHostname: string,
githubAppInstallationId: string,
jwtToken: string,
) {
try {
const request: PostFilterPreparedRequest = {
url: `https://${endpointHostname}/app/installations/${githubAppInstallationId}/access_tokens`,
headers: {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
Authorization: `Bearer ${jwtToken}`,
'user-agent': 'Snyk Broker Github App Plugin',
},
method: 'POST',
};

const oauthResponse = await makeRequestToDownstream(request);
const accessToken = oauthResponse.body ?? '';
return accessToken;
} catch (err) {
this.logger.error(
{ plugin: this.pluginCode, err },
`Error getting access token`,
);
throw err;
}
}

_setAccessTokenLifecycleHandler(connectionConfig) {
if (connectionConfig.accessToken) {
let timeoutHandlerId;
let timeoutHandler = async () => {};
timeoutHandler = async () => {
try {
this.logger.debug(
{ plugin: this.pluginCode },
'Refreshing github app access token',
);
clearTimeout(timeoutHandlerId);
connectionConfig.accessToken = await this._getAccessToken(
connectionConfig.GITHUB_API,
connectionConfig.GITHUB_APP_INSTALLATION_ID,
connectionConfig.JWT_TOKEN,
);
connectionConfig.ACCESS_TOKEN = JSON.parse(
connectionConfig.accessToken,
).token;
this.logger.debug(
{ plugin: this.pluginCode },
`Refreshed access token expires at ${
JSON.parse(connectionConfig.accessToken).expires_at
}`,
);
timeoutHandlerId = setTimeout(
timeoutHandler,
this._getTimeDifferenceInMsToFutureDate(
JSON.parse(connectionConfig.accessToken).expires_at,
) - 10000,
);
connectionConfig.accessTokenTimeoutHandlerId = timeoutHandlerId;
} catch (err) {
this.logger.error(
{ plugin: this.pluginCode, err },
`Error setting Access Token lifecycle handler.`,
);
throw new Error(`${err}`);
}
};
timeoutHandlerId = setTimeout(
timeoutHandler,
this._getTimeDifferenceInMsToFutureDate(
JSON.parse(connectionConfig.accessToken).expires_at,
) - 10000,
);
connectionConfig.accessTokenTimeoutHandlerId = timeoutHandlerId;
}
}
_getTimeDifferenceInMsToFutureDate(targetDate) {
const currentDate = new Date();
const futureDate = new Date(targetDate);
const timeDifference = futureDate.getTime() - currentDate.getTime();
return timeDifference;
}

// Hook to run pre requests operations - Optional. Uncomment to enable
// async preRequest(
// connectionConfiguration: Record<string, any>,
// postFilterPreparedRequest:PostFilterPreparedRequest,
// postFilterPreparedRequest: PostFilterPreparedRequest,
// ) {
// this.logger.debug({ plugin: this.pluginName, connection: connectionConfiguration }, 'Running prerequest plugin');
// this.logger.debug(
// { plugin: this.pluginName, connection: connectionConfiguration },
// 'Running prerequest plugin',
// );
// return postFilterPreparedRequest;
// }
}
Loading

0 comments on commit cb7da75

Please sign in to comment.