Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration for GitHub environment secrets #1194

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions backend/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -2554,6 +2554,26 @@
}
}
},
"/api/v1/integration-auth/{integrationAuthId}/github/repositories": {
"get": {
"description": "",
"parameters": [
{
"name": "integrationAuthId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v1/folders/": {
"post": {
"summary": "Create folder",
Expand Down
73 changes: 73 additions & 0 deletions backend/src/controllers/v1/integrationAuthController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { getIntegrationAuthAccessHelper } from "../../helpers";
import { Octokit } from "@octokit/rest";

/***
* Return integration authorization with id [integrationAuthId]
Expand Down Expand Up @@ -1048,6 +1049,78 @@ export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: R
});
};

/**
* Return list of repositories for github environment integration
* @param req
* @param res
* @returns
*/
export const getIntegrationAuthGithubEnvironmentRepositories = async (req: Request, res: Response) => {
interface GitHubApp {
id: string;
name: string;
permissions: {
admin: boolean;
};
owner: {
login: string;
};
}

const {
params: { integrationAuthId }
} = await validateRequest(reqValidator.GetIntegrationAuthGitHubEnvironmentRepositoriesV1, req);

const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new Types.ObjectId(integrationAuthId)
});

const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});

ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);

const octokit = new Octokit({
auth: accessToken
});

const getAllRepos = async () => {
let repos: GitHubApp[] = [];
let page = 1;
const per_page = 100;
let hasMore = true;

while (hasMore) {
const response = await octokit.request(
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
{
per_page,
page
}
);

if (response.data.length > 0) {
repos = repos.concat(response.data);
page++;
} else {
hasMore = false;
}
}

return repos;
};

const repos = await getAllRepos();
return res.status(200).send({
repos: repos.filter((repo: GitHubApp) => repo.permissions.admin === true)
});
};

/**
* Return list of secret groups for Northflank project with id [appId]
* @param req
Expand Down
40 changes: 38 additions & 2 deletions backend/src/integrations/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ const getApps = async ({
break;
case INTEGRATION_GITHUB:
apps = await getAppsGithub({
accessToken
accessToken,
workspaceSlug
});
break;
case INTEGRATION_GITLAB:
Expand Down Expand Up @@ -444,10 +445,11 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
* Return list of repositories for Github integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Github API
* @param {String} obj.workspaceSlug - Workspace identifier for fetching BitBucket repositories
Shraeyas marked this conversation as resolved.
Show resolved Hide resolved
* @returns {Object[]} apps - names of Github sites
* @returns {String} apps.name - name of Github site
*/
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
const getAppsGithub = async ({ accessToken, workspaceSlug }: { accessToken: string, workspaceSlug?: string }) => {
interface GitHubApp {
id: string;
name: string;
Expand All @@ -463,6 +465,40 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
auth: accessToken
});

if(workspaceSlug && workspaceSlug !== "none") {
// get github environments if workflowSlug is set
const { data } = await octokit.request("GET /user", {
headers: {
"X-GitHub-Api-Version": "2022-11-28"
}
})

const { login: username } = data;
try {
const {data} = await octokit.request("GET /repos/{owner}/{repo}/environments", {
owner: username,
repo: workspaceSlug,
headers: {
"X-GitHub-Api-Version": "2022-11-28"
}
})
const {environments} = data;
let apps: any = [];
if(environments) {
apps = environments
.map((a: any) => {
return {
appId: String(a.id),
name: a.name
};
});
}
return apps;
} catch(e) {
return []
}
}

const getAllRepos = async () => {
let repos: GitHubApp[] = [];
let page = 1;
Expand Down
199 changes: 139 additions & 60 deletions backend/src/integrations/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1398,78 +1398,157 @@ const syncSecretsGitHub = async ({
auth: accessToken
});

// const user = (await octokit.request('GET /user', {})).data;
const repoPublicKey: GitHubRepoKey = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", {
owner: integration.owner,
repo: integration.app
})
).data;
if(!integration.targetEnvironment) {
// const user = (await octokit.request('GET /user', {})).data;
const repoPublicKey: GitHubRepoKey = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", {
owner: integration.owner,
repo: integration.app
})
).data;

// Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
let encryptedSecrets: GitHubSecretRes = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner,
repo: integration.app
})
).data.secrets.reduce(
(obj: any, secret: any) => ({
...obj,
[secret.name]: secret
}),
{}
);
// Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
let encryptedSecrets: GitHubSecretRes = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner,
repo: integration.app
})
).data.secrets.reduce(
(obj: any, secret: any) => ({
...obj,
[secret.name]: secret
}),
{}
);

encryptedSecrets = Object.keys(encryptedSecrets).reduce(
(
result: {
[key: string]: GitHubSecret;
encryptedSecrets = Object.keys(encryptedSecrets).reduce(
(
result: {
[key: string]: GitHubSecret;
},
key
) => {
if (
(appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) &&
(appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)
) {
result[key] = encryptedSecrets[key];
}
return result;
},
key
) => {
if (
(appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) &&
(appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)
) {
result[key] = encryptedSecrets[key];
{}
);

Object.keys(encryptedSecrets).map(async (key) => {
if (!(key in secrets)) {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner,
repo: integration.app,
secret_name: key
});
}
return result;
},
{}
);
});

Object.keys(encryptedSecrets).map(async (key) => {
if (!(key in secrets)) {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner,
repo: integration.app,
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].value);

// 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: integration.owner,
repo: integration.app,
secret_name: key,
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
});
});
}
});
});
} else {
// integration with github environments
const repository_id = Number(integration.targetEnvironmentId)
const environment_name = integration.app
const envPublicKey: GitHubRepoKey = (
await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets/public-key", {
repository_id,
environment_name
})
).data

// Get local copy of encrypted secrets
let encryptedSecrets: GitHubSecretRes = (
await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
repository_id,
environment_name
})).data.secrets.reduce(
(obj: any, secret: any) => ({
...obj,
[secret.name]: secret
}),
{}
)

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].value);
encryptedSecrets = Object.keys(encryptedSecrets).reduce(
(
result: {
[key: string]: GitHubSecret;
},
key
) => {
if (
(appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) &&
(appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)
) {
result[key] = encryptedSecrets[key];
}
return result;
},
{}
);

Object.keys(encryptedSecrets).map(async (key) => {
if (!(key in secrets)) {
await octokit.request("DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}", {
repository_id,
environment_name,
secret_name: key,
headers: {
"X-GitHub-Api-Version": "2022-11-28"
}
})
}
});

// encrypt secret using libsodium
const encBytes = sodium.crypto_box_seal(binsec, binkey);
Object.keys(secrets).map((key) => {
sodium.ready.then(async () => {
// convert secret & base64 key to Uint8Array.
const binkey = sodium.from_base64(envPublicKey.key, sodium.base64_variants.ORIGINAL);
const binsec = sodium.from_string(secrets[key].value);

// convert encrypted Uint8Array to base64
const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL);
// encrypt secret using libsodium
const encBytes = sodium.crypto_box_seal(binsec, binkey);

await octokit.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner,
repo: integration.app,
secret_name: key,
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
// convert encrypted Uint8Array to base64
const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL);

await octokit.request("PUT /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}", {
repository_id,
environment_name,
secret_name: key,
encrypted_value: encryptedSecret,
key_id: envPublicKey.key_id
})
});
});
});
}

};

/**
Expand Down
Loading