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
+13 -5
View File
@@ -13,6 +13,10 @@ import {
INTEGRATION_GITHUB_API_URL
} from '../variables';
interface GitHubApp {
name: string;
}
/**
* Return list of names of apps for integration named [integration]
* @param {Object} obj
@@ -186,13 +190,17 @@ const getAppsGithub = async ({
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}',
{}
);
apps = repos.map((a: any) => {
a.name;
});
)).data;
apps = repos
.filter((a:any) => a.permissions.admin === true)
.map((a: any) => ({
name: a.name
})
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
+6 -45
View File
@@ -9,14 +9,15 @@ import {
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITHUB_API_URL,
ACTION_PUSH_TO_HEROKU
} from '../variables';
import {
SITE_URL,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB
@@ -117,43 +118,13 @@ const exchangeCode = async ({
* @returns {String} obj2.refreshToken - refresh token for Heroku API
* @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 ({
code
}: {
code: string;
}) => {
let res: ExchangeCodeHerokuResponse;
let accessExpiresAt = new Date();
const accessExpiresAt = new Date();
try {
res = (await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
@@ -179,7 +150,6 @@ const exchangeCodeHeroku = async ({
accessExpiresAt
});
}
>>>>>>> 5444382d5ae1fabf1107434a856b58b9f09c67f6
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
@@ -286,7 +256,6 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
*/
const exchangeCodeGithub = async ({ code }: { code: string }) => {
let res: ExchangeCodeGithubResponse;
let res2;
try {
res = (
await axios.get(INTEGRATION_GITHUB_TOKEN_URL, {
@@ -295,29 +264,21 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
client_secret: CLIENT_SECRET_GITHUB,
code: code,
redirect_uri: `${SITE_URL}/github`
},
headers: {
Accept: 'application/json'
}
})
).data;
res2 = (
await axios.get(INTEGRATION_GITHUB_TOKEN_URL, {
params: {
Authorization: `Bearer ${res.access_token}`
}
})
).data;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Github');
}
// TODO: Check actual response and fix next line
const accountId = res2.user;
return {
accessToken: res.access_token,
user: accountId,
refreshToken: null,
accessExpiresAt: null
};
-13
View File
@@ -52,18 +52,6 @@ const exchangeRefreshHeroku = async ({
}: {
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;
try {
const res = await axios.post(
@@ -74,7 +62,6 @@ const exchangeRefreshHeroku = async ({
client_secret: CLIENT_SECRET_HEROKU
} as any)
);
>>>>>>> 5444382d5ae1fabf1107434a856b58b9f09c67f6
accessToken = res.data.access_token;
} catch (err) {
+78 -72
View File
@@ -1,7 +1,9 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
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 {
INTEGRATION_HEROKU,
@@ -66,11 +68,11 @@ const syncSecrets = async ({
case INTEGRATION_GITHUB:
await syncSecretsGitHub({
integration,
integrationAuth,
secrets,
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
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 {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
@@ -494,106 +496,110 @@ const syncSecretsNetlify = async ({
*/
const syncSecretsGitHub = async ({
integration,
integrationAuth,
secrets,
accessToken
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any;
accessToken: string;
}) => {
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({
auth: accessToken
});
const loggedInUser = await octokit.request('GET /user', {});
// TODO: Check loggedInUser.login == repo owner
const repoPublicKey = await octokit.request(
const user = (await octokit.request('GET /user', {})).data;
const repoPublicKey: GitHubRepoKey = (await octokit.request(
'GET /repos/{owner}/{repo}/actions/secrets/public-key',
{
owner: loggedInUser.login,
owner: user.login,
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 = await octokit.request(
// // 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 /repos/{owner}/{repo}/actions/secrets',
{
owner: loggedInUser.name,
owner: user.login,
repo: integration.app
}
);
Object.keys(secrets).map((key) => {
if (!(key in encryptedSecrets)) {
deleteSecrets.push(key);
}
});
if (!Object.values(userRepos).includes(integration.app)) {
if (deleteSecrets.length == 0) {
throw new Error('Failed to sync secrets to Github');
}
}
// 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(
))
.data
.secrets
.reduce((obj: any, secret: any) => ({
...obj,
[secret.name]: secret
}), {});
Object.keys(encryptedSecrets).map(async (key) => {
if (!(key in secrets)) {
await octokit.request(
'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}',
{
owner: loggedInUser.login,
owner: user.login,
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) {
Sentry.setUser(null);
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 };