Fix errors and complete v1 full-loop of GitHub integration with repository secrets

This commit is contained in:
Tuan Dang
2022-12-21 23:15:22 -05:00
parent 54b0285cbd
commit 7763e33de6
11 changed files with 119 additions and 155 deletions
+2
View File
@@ -50,9 +50,11 @@ SMTP_PASSWORD=
CLIENT_ID_HEROKU= CLIENT_ID_HEROKU=
CLIENT_ID_VERCEL= CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY= CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB=
CLIENT_SECRET_HEROKU= CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL= CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY= CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB=
# Sentry (optional) for monitoring errors # Sentry (optional) for monitoring errors
SENTRY_DSN= SENTRY_DSN=
-7
View File
@@ -3921,7 +3921,6 @@
"dependencies": { "dependencies": {
"anymatch": "~3.1.2", "anymatch": "~3.1.2",
"braces": "~3.0.2", "braces": "~3.0.2",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2", "glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0", "is-binary-path": "~2.1.0",
"is-glob": "~4.0.1", "is-glob": "~4.0.1",
@@ -5133,7 +5132,6 @@
"minimist": "^1.2.5", "minimist": "^1.2.5",
"neo-async": "^2.6.0", "neo-async": "^2.6.0",
"source-map": "^0.6.1", "source-map": "^0.6.1",
"uglify-js": "^3.1.4",
"wordwrap": "^1.0.0" "wordwrap": "^1.0.0"
}, },
"bin": { "bin": {
@@ -5791,7 +5789,6 @@
"@types/node": "*", "@types/node": "*",
"anymatch": "^3.0.3", "anymatch": "^3.0.3",
"fb-watchman": "^2.0.0", "fb-watchman": "^2.0.0",
"fsevents": "^2.3.2",
"graceful-fs": "^4.2.9", "graceful-fs": "^4.2.9",
"jest-regex-util": "^29.2.0", "jest-regex-util": "^29.2.0",
"jest-util": "^29.3.1", "jest-util": "^29.3.1",
@@ -6595,11 +6592,9 @@
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.11.0.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.11.0.tgz",
"integrity": "sha512-9l9n4Nk2BYZzljW3vHah3Z0rfS5npKw6ktnkmFgTcnzaXH1DRm3pDl6VMHu84EVb1lzmSaJC4OzWZqTkB5i2wg==", "integrity": "sha512-9l9n4Nk2BYZzljW3vHah3Z0rfS5npKw6ktnkmFgTcnzaXH1DRm3pDl6VMHu84EVb1lzmSaJC4OzWZqTkB5i2wg==",
"dependencies": { "dependencies": {
"@aws-sdk/credential-providers": "^3.186.0",
"bson": "^4.7.0", "bson": "^4.7.0",
"denque": "^2.1.0", "denque": "^2.1.0",
"mongodb-connection-string-url": "^2.5.4", "mongodb-connection-string-url": "^2.5.4",
"saslprep": "^1.0.3",
"socks": "^2.7.1" "socks": "^2.7.1"
}, },
"engines": { "engines": {
@@ -7682,7 +7677,6 @@
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@colors/colors": "1.5.0",
"string-width": "^4.2.0" "string-width": "^4.2.0"
}, },
"engines": { "engines": {
@@ -8478,7 +8472,6 @@
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"encoding": "^0.1.13",
"minipass": "^3.1.6", "minipass": "^3.1.6",
"minipass-sized": "^1.0.3", "minipass-sized": "^1.0.3",
"minizlib": "^2.1.2" "minizlib": "^2.1.2"
+2 -3
View File
@@ -14,11 +14,10 @@ const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!; const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!; const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!; const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
const CLIENT_ID_GITHUB = const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
process.env.CLIENT_ID_GITHUB! || 'e787fc24bcec43ecd5d5';
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!; const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB! || '407f32da788f63559abd662c6de08bb2911ca8ae'; const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!;
const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!; const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!;
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com'; const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
const POSTHOG_PROJECT_API_KEY = const POSTHOG_PROJECT_API_KEY =
+13 -5
View File
@@ -13,6 +13,10 @@ import {
INTEGRATION_GITHUB_API_URL INTEGRATION_GITHUB_API_URL
} from '../variables'; } from '../variables';
interface GitHubApp {
name: string;
}
/** /**
* Return list of names of apps for integration named [integration] * Return list of names of apps for integration named [integration]
* @param {Object} obj * @param {Object} obj
@@ -186,13 +190,17 @@ const getAppsGithub = async ({
auth: accessToken auth: accessToken
}); });
const repos = await octokit.request( const repos = (await octokit.request(
'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}', 'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}',
{} {}
); )).data;
apps = repos.map((a: any) => {
a.name; apps = repos
}); .filter((a:any) => a.permissions.admin === true)
.map((a: any) => ({
name: a.name
})
);
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
+6 -45
View File
@@ -9,14 +9,15 @@ import {
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITHUB_API_URL,
ACTION_PUSH_TO_HEROKU ACTION_PUSH_TO_HEROKU
} from '../variables'; } from '../variables';
import { import {
SITE_URL, SITE_URL,
CLIENT_ID_VERCEL, CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY, CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB, CLIENT_ID_GITHUB,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL, CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY, CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB CLIENT_SECRET_GITHUB
@@ -117,43 +118,13 @@ const exchangeCode = async ({
* @returns {String} obj2.refreshToken - refresh token for Heroku API * @returns {String} obj2.refreshToken - refresh token for Heroku API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token * @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/ */
<<<<<<< HEAD
const exchangeCodeHeroku = async ({ code }: { code: string }) => {
let res: ExchangeCodeHerokuResponse;
const accessExpiresAt = new Date();
try {
res = (
await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_secret: OAUTH_CLIENT_SECRET_HEROKU
} as any)
)
).data;
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Heroku');
}
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
};
};
=======
const exchangeCodeHeroku = async ({ const exchangeCodeHeroku = async ({
code code
}: { }: {
code: string; code: string;
}) => { }) => {
let res: ExchangeCodeHerokuResponse; let res: ExchangeCodeHerokuResponse;
let accessExpiresAt = new Date(); const accessExpiresAt = new Date();
try { try {
res = (await axios.post( res = (await axios.post(
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
@@ -179,7 +150,6 @@ const exchangeCodeHeroku = async ({
accessExpiresAt accessExpiresAt
}); });
} }
>>>>>>> 5444382d5ae1fabf1107434a856b58b9f09c67f6
/** /**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel * Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
@@ -286,7 +256,6 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
*/ */
const exchangeCodeGithub = async ({ code }: { code: string }) => { const exchangeCodeGithub = async ({ code }: { code: string }) => {
let res: ExchangeCodeGithubResponse; let res: ExchangeCodeGithubResponse;
let res2;
try { try {
res = ( res = (
await axios.get(INTEGRATION_GITHUB_TOKEN_URL, { await axios.get(INTEGRATION_GITHUB_TOKEN_URL, {
@@ -295,29 +264,21 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
client_secret: CLIENT_SECRET_GITHUB, client_secret: CLIENT_SECRET_GITHUB,
code: code, code: code,
redirect_uri: `${SITE_URL}/github` redirect_uri: `${SITE_URL}/github`
},
headers: {
Accept: 'application/json'
} }
}) })
).data; ).data;
res2 = (
await axios.get(INTEGRATION_GITHUB_TOKEN_URL, {
params: {
Authorization: `Bearer ${res.access_token}`
}
})
).data;
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Github'); throw new Error('Failed OAuth2 code-token exchange with Github');
} }
// TODO: Check actual response and fix next line
const accountId = res2.user;
return { return {
accessToken: res.access_token, accessToken: res.access_token,
user: accountId,
refreshToken: null, refreshToken: null,
accessExpiresAt: null accessExpiresAt: null
}; };
-13
View File
@@ -52,18 +52,6 @@ const exchangeRefreshHeroku = async ({
}: { }: {
refreshToken: string; refreshToken: string;
}) => { }) => {
<<<<<<< HEAD
let accessToken;
try {
const res = await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_secret: OAUTH_CLIENT_SECRET_HEROKU
} as any)
);
=======
let accessToken; let accessToken;
try { try {
const res = await axios.post( const res = await axios.post(
@@ -74,7 +62,6 @@ const exchangeRefreshHeroku = async ({
client_secret: CLIENT_SECRET_HEROKU client_secret: CLIENT_SECRET_HEROKU
} as any) } as any)
); );
>>>>>>> 5444382d5ae1fabf1107434a856b58b9f09c67f6
accessToken = res.data.access_token; accessToken = res.data.access_token;
} catch (err) { } catch (err) {
+78 -72
View File
@@ -1,7 +1,9 @@
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest'; import { Octokit } from '@octokit/rest';
import * as sodium from 'libsodium-wrappers'; // import * as sodium from 'libsodium-wrappers';
import sodium from 'libsodium-wrappers';
// const sodium = require('libsodium-wrappers');
import { IIntegration, IIntegrationAuth } from '../models'; import { IIntegration, IIntegrationAuth } from '../models';
import { import {
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
@@ -66,11 +68,11 @@ const syncSecrets = async ({
case INTEGRATION_GITHUB: case INTEGRATION_GITHUB:
await syncSecretsGitHub({ await syncSecretsGitHub({
integration, integration,
integrationAuth,
secrets, secrets,
accessToken accessToken
}); });
break; break;
}
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@@ -486,7 +488,7 @@ const syncSecretsNetlify = async ({
} }
/** /**
* Sync/push [secrets] to GitHub site [app] * Sync/push [secrets] to GitHub [repo]
* @param {Object} obj * @param {Object} obj
* @param {IIntegration} obj.integration - integration details * @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details * @param {IIntegrationAuth} obj.integrationAuth - integration auth details
@@ -494,106 +496,110 @@ const syncSecretsNetlify = async ({
*/ */
const syncSecretsGitHub = async ({ const syncSecretsGitHub = async ({
integration, integration,
integrationAuth,
secrets, secrets,
accessToken accessToken
}: { }: {
integration: IIntegration; integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any; secrets: any;
accessToken: string; accessToken: string;
}) => { }) => {
try { try {
const deleteSecrets: Array<string> = [];
interface GitHubRepoKey {
key_id: string;
key: string;
}
interface GitHubSecret {
name: string;
created_at: string;
updated_at: string;
}
interface GitHubSecretRes {
[index: string]: GitHubSecret;
}
const deleteSecrets: GitHubSecret[] = [];
const octokit = new Octokit({ const octokit = new Octokit({
auth: accessToken auth: accessToken
}); });
const loggedInUser = await octokit.request('GET /user', {}); const user = (await octokit.request('GET /user', {})).data;
// TODO: Check loggedInUser.login == repo owner
const repoPublicKey = await octokit.request( const repoPublicKey: GitHubRepoKey = (await octokit.request(
'GET /repos/{owner}/{repo}/actions/secrets/public-key', 'GET /repos/{owner}/{repo}/actions/secrets/public-key',
{ {
owner: loggedInUser.login, owner: user.login,
repo: integration.app repo: integration.app
} }
).key; )).data;
const userRepos = await octokit.request('GET /user/repos', {}); // // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
const encryptedSecrets: GitHubSecretRes = (await octokit.request(
// Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
const encryptedSecrets = await octokit.request(
'GET /repos/{owner}/{repo}/actions/secrets', 'GET /repos/{owner}/{repo}/actions/secrets',
{ {
owner: loggedInUser.name, owner: user.login,
repo: integration.app repo: integration.app
} }
); ))
.data
Object.keys(secrets).map((key) => { .secrets
if (!(key in encryptedSecrets)) { .reduce((obj: any, secret: any) => ({
deleteSecrets.push(key); ...obj,
} [secret.name]: secret
}); }), {});
if (!Object.values(userRepos).includes(integration.app)) { Object.keys(encryptedSecrets).map(async (key) => {
if (deleteSecrets.length == 0) { if (!(key in secrets)) {
throw new Error('Failed to sync secrets to Github'); await octokit.request(
}
}
// Sync/push all secrets
for (const i in secrets) {
let encryptedSecret;
sodium.ready.then(() => {
// Convert Secret & Base64 key to Uint8Array.
const binkey = sodium.from_base64(
repoPublicKey,
sodium.base64_variants.ORIGINAL
);
const binsec = sodium.from_string(secrets[i]);
//Encrypt the secret using LibSodium
const encBytes = sodium.crypto_box_seal(binsec, binkey);
// Convert encrypted Uint8Array to Base64
encryptedSecret = sodium.to_base64(
encBytes,
sodium.base64_variants.ORIGINAL
);
});
const res = await octokit.request(
'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}',
{
owner: loggedInUser.login,
repo: integration.app,
secret_name: Object.keys(secrets[i]),
encrypted_value: encryptedSecret,
key_id: '' //TODO: Not sure if we need this? https://docs.github.com/en/rest/actions/secrets?apiVersion=2022-11-28#create-or-update-a-repository-secret
}
);
}
// Delete secrets
if (deleteSecrets.length > 0) {
for (const i in deleteSecrets) {
const res = await octokit.request(
'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}', 'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}',
{ {
owner: loggedInUser.login, owner: user.login,
repo: integration.app, repo: integration.app,
secret_name: secrets[i] secret_name: key
} }
); );
} }
} });
Object.keys(secrets).map((key) => {
// let encryptedSecret;
sodium.ready.then(async () => {
// convert secret & base64 key to Uint8Array.
const binkey = sodium.from_base64(
repoPublicKey.key,
sodium.base64_variants.ORIGINAL
);
const binsec = sodium.from_string(secrets[key]);
// encrypt secret using libsodium
const encBytes = sodium.crypto_box_seal(binsec, binkey);
// convert encrypted Uint8Array to base64
const encryptedSecret = sodium.to_base64(
encBytes,
sodium.base64_variants.ORIGINAL
);
await octokit.request(
'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}',
{
owner: user.login,
repo: integration.app,
secret_name: key,
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
}
);
});
});
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to sync secrets to Github'); throw new Error('Failed to sync secrets to GitHub');
} }
}; };
export { syncSecrets }; export { syncSecrets };
+12 -1
View File
@@ -1,6 +1,7 @@
import { import {
CLIENT_ID_HEROKU, CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY, CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SLUG_VERCEL CLIENT_SLUG_VERCEL
} from '../config'; } from '../config';
@@ -31,7 +32,7 @@ const INTEGRATION_GITHUB_TOKEN_URL =
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com'; const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com'; const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com'; const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
const INTEGRATION_GITHUB_API_URL = ' https://api.github.com/'; const INTEGRATION_GITHUB_API_URL = 'https://api.github.com';
const INTEGRATION_OPTIONS = [ const INTEGRATION_OPTIONS = [
{ {
@@ -62,6 +63,16 @@ const INTEGRATION_OPTIONS = [
clientId: CLIENT_ID_NETLIFY, clientId: CLIENT_ID_NETLIFY,
docsLink: '' docsLink: ''
}, },
{
name: 'GitHub',
slug: 'github',
image: 'GitHub',
isAvailable: true,
type: 'oauth2',
clientId: CLIENT_ID_GITHUB,
docsLink: ''
},
{ {
name: 'Google Cloud Platform', name: 'Google Cloud Platform',
slug: 'gcp', slug: 'gcp',
@@ -203,7 +203,7 @@ const Integration = ({
context: integrationContext ? reverseContextNetlifyMapping[integrationContext] : null, context: integrationContext ? reverseContextNetlifyMapping[integrationContext] : null,
siteId siteId
}); });
router.reload(); router.reload();
}} }}
color="mineshaft" color="mineshaft"
+5 -8
View File
@@ -123,6 +123,8 @@ export default function Integrations() {
* @returns * @returns
*/ */
const handleIntegrationOption = async ({ integrationOption }) => { const handleIntegrationOption = async ({ integrationOption }) => {
console.log('handleIntegrationOption', integrationOption);
try { try {
// generate CSRF token for OAuth2 code-token exchange integrations // generate CSRF token for OAuth2 code-token exchange integrations
@@ -137,16 +139,11 @@ export default function Integrations() {
window.location = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`; window.location = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break; break;
case 'Netlify': case 'Netlify':
window.location = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${integrationOption.redirectURL}&state=${state}`;
break;
case 'Github':
window.location = `https://github.com.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${integrationOption.redirectURL}&state=${state}`;
break;
case 'Fly.io':
console.log('fly.io');
setIntegrationAccessTokenDialogOpen(true);
window.location = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`; window.location = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`;
break; break;
case 'GitHub':
window.location = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`;
break;
// case 'Fly.io': // case 'Fly.io':
// console.log('fly.io'); // console.log('fly.io');
// setIntegrationAccessTokenDialogOpen(true); // setIntegrationAccessTokenDialogOpen(true);
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB