fix: resolved trimming keys but keeping last line break for ssh keys and added skip encoding on integration sync

This commit is contained in:
Akhil Mohan
2023-09-25 13:23:38 +05:30
parent 85378e25aa
commit 4929022523
6 changed files with 159 additions and 108 deletions

View File

@@ -103,7 +103,10 @@ export const getSecretsBotHelper = async ({
environment: string;
secretPath: string;
}) => {
const content: Record<string, { value: string; comment?: string }> = {};
const content: Record<
string,
{ value: string; comment?: string; skipMultilineEncoding?: boolean }
> = {};
const key = await getKey({ workspaceId: workspaceId });
let folderId = "root";
@@ -165,6 +168,8 @@ export const getSecretsBotHelper = async ({
});
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = secret.skipMultilineEncoding;
});
});
@@ -194,6 +199,8 @@ export const getSecretsBotHelper = async ({
});
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = secret.skipMultilineEncoding;
});
await expandSecrets(workspaceId.toString(), key, content);

View File

@@ -1203,7 +1203,7 @@ const formatMultiValueEnv = (val?: string) => {
export const expandSecrets = async (
workspaceId: string,
rootEncKey: string,
secrets: Record<string, { value: string; comment?: string }>
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>
) => {
const expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {};
@@ -1221,7 +1221,10 @@ export const expandSecrets = async (
for (const key of Object.keys(secrets)) {
if (expandedSec?.[key]) {
secrets[key].value = formatMultiValueEnv(expandedSec[key]);
// should not do multi line encoding if user has set it to skip
secrets[key].value = secrets[key].skipMultilineEncoding
? expandedSec[key]
: formatMultiValueEnv(expandedSec[key]);
continue;
}
@@ -1236,7 +1239,9 @@ export const expandSecrets = async (
key
);
secrets[key].value = formatMultiValueEnv(expandedVal);
secrets[key].value = secrets[key].skipMultilineEncoding
? expandedVal
: formatMultiValueEnv(expandedVal);
}
return secrets;

View File

@@ -65,10 +65,10 @@ import sodium from "libsodium-wrappers";
import { standardRequest } from "../config/request";
const getSecretKeyValuePair = (
secrets: Record<string, { value: string; comment?: string } | null>
secrets: Record<string, { value: string | null; comment?: string } | null>
) =>
Object.keys(secrets).reduce<Record<string, string>>((prev, key) => {
if (secrets[key]) prev[key] = secrets[key]?.value || "";
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
prev[key] = secrets?.[key] === null ? null : secrets?.[key]?.value;
return prev;
}, {});
@@ -325,40 +325,42 @@ const syncSecretsGCPSecretManager = async ({
name: string;
createTime: string;
}
interface GCPSMListSecretsRes {
secrets?: GCPSecret[];
totalSize?: number;
nextPageToken?: string;
}
let gcpSecrets: GCPSecret[] = [];
const pageSize = 100;
let pageToken: string | undefined;
let hasMorePages = true;
const filterParam = integration.metadata.secretGCPLabel
? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}`
const filterParam = integration.metadata.secretGCPLabel
? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}`
: "";
while (hasMorePages) {
const params = new URLSearchParams({
pageSize: String(pageSize),
...(pageToken ? { pageToken } : {})
});
const res: GCPSMListSecretsRes = (await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`,
{
params,
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
const res: GCPSMListSecretsRes = (
await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
}
)).data;
)
).data;
if (res.secrets) {
const filteredSecrets = res.secrets?.filter((gcpSecret) => {
const arr = gcpSecret.name.split("/");
@@ -366,54 +368,58 @@ const syncSecretsGCPSecretManager = async ({
let isValid = true;
if (integration.metadata.secretPrefix && !key.startsWith(integration.metadata.secretPrefix)) {
if (
integration.metadata.secretPrefix &&
!key.startsWith(integration.metadata.secretPrefix)
) {
isValid = false;
}
if (integration.metadata.secretSuffix && !key.endsWith(integration.metadata.secretSuffix)) {
isValid = false;
}
return isValid;
});
gcpSecrets = gcpSecrets.concat(filteredSecrets);
}
if (!res.nextPageToken) {
hasMorePages = false;
}
pageToken = res.nextPageToken;
}
const res: { [key: string]: string; } = {};
const res: { [key: string]: string } = {};
interface GCPLatestSecretVersionAccess {
name: string;
payload: {
data: string;
}
};
}
for await (const gcpSecret of gcpSecrets) {
const arr = gcpSecret.name.split("/");
const key = arr[arr.length - 1];
const secretLatest: GCPLatestSecretVersionAccess = (await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
const secretLatest: GCPLatestSecretVersionAccess = (
await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
}
)).data;
)
).data;
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
}
for await (const key of Object.keys(secrets)) {
if (!(key in res)) {
// case: create secret
@@ -423,11 +429,14 @@ const syncSecretsGCPSecretManager = async ({
replication: {
automatic: {}
},
...(integration.metadata.secretGCPLabel ? {
labels: {
[integration.metadata.secretGCPLabel.labelName]: integration.metadata.secretGCPLabel.labelValue
}
} : {})
...(integration.metadata.secretGCPLabel
? {
labels: {
[integration.metadata.secretGCPLabel.labelName]:
integration.metadata.secretGCPLabel.labelValue
}
}
: {})
},
{
params: {
@@ -439,7 +448,7 @@ const syncSecretsGCPSecretManager = async ({
}
}
);
await standardRequest.post(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
{
@@ -456,7 +465,7 @@ const syncSecretsGCPSecretManager = async ({
);
}
}
for await (const key of Object.keys(res)) {
if (!(key in secrets)) {
// case: delete secret
@@ -489,7 +498,7 @@ const syncSecretsGCPSecretManager = async ({
}
}
}
}
};
/**
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
@@ -729,15 +738,12 @@ const syncSecretsAWSParameterStore = async ({
} = {};
if (parameterList) {
awsParameterStoreSecretsObj = parameterList.reduce(
(obj: any, secret: any) => {
return ({
...obj,
[secret.Name.substring(integration.path.length)]: secret
});
},
{}
);
awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => {
return {
...obj,
[secret.Name.substring(integration.path.length)]: secret
};
}, {});
}
// Identify secrets to create
@@ -1869,8 +1875,10 @@ const syncSecretsGitLab = async ({
value: string;
environment_scope: string;
}
const gitLabApiUrl = integrationAuth.url ? `${integrationAuth.url}/api` : INTEGRATION_GITLAB_API_URL;
const gitLabApiUrl = integrationAuth.url
? `${integrationAuth.url}/api`
: INTEGRATION_GITLAB_API_URL;
const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => {
const headers = {
@@ -1880,7 +1888,9 @@ const syncSecretsGitLab = async ({
};
let allEnvVariables: GitLabSecret[] = [];
let url: string | null = `${gitLabApiUrl}/v4/projects/${integrationAppId}/variables?per_page=100`;
let url:
| string
| null = `${gitLabApiUrl}/v4/projects/${integrationAppId}/variables?per_page=100`;
while (url) {
const response: any = await standardRequest.get(url, { headers });
@@ -1901,23 +1911,27 @@ const syncSecretsGitLab = async ({
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables
.filter(
(secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment
)
.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
.filter((gitLabSecret) => {
let isValid = true;
if (integration.metadata.secretPrefix && !gitLabSecret.key.startsWith(integration.metadata.secretPrefix)) {
if (
integration.metadata.secretPrefix &&
!gitLabSecret.key.startsWith(integration.metadata.secretPrefix)
) {
isValid = false;
}
if (integration.metadata.secretSuffix && !gitLabSecret.key.endsWith(integration.metadata.secretSuffix)) {
if (
integration.metadata.secretSuffix &&
!gitLabSecret.key.endsWith(integration.metadata.secretSuffix)
) {
isValid = false;
}
return isValid;
});
for await (const key of Object.keys(secrets)) {
const existingSecret = getSecretsRes.find((s: any) => s.key == key);
if (!existingSecret) {
@@ -2371,41 +2385,43 @@ const syncSecretsTeamCity = async ({
if (integration.targetEnvironment && integration.targetEnvironmentId) {
// case: sync to specific build-config in TeamCity project
const res = (await standardRequest.get<GetTeamCityBuildConfigParametersRes>(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
}
))
.data
.property
.filter((parameter) => !parameter.inherited)
.reduce((obj: any, secret: TeamCitySecret) => {
const secretName = secret.name.replace(/^env\./, "");
return {
...obj,
[secretName]: secret.value
};
}, {});
const res = (
await standardRequest.get<GetTeamCityBuildConfigParametersRes>(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
)
).data.property
.filter((parameter) => !parameter.inherited)
.reduce((obj: any, secret: TeamCitySecret) => {
const secretName = secret.name.replace(/^env\./, "");
return {
...obj,
[secretName]: secret.value
};
}, {});
for await (const key of Object.keys(secrets)) {
if (!(key in res) || (key in res && secrets[key].value !== res[key])) {
// case: secret does not exist in TeamCity or secret value has changed
// -> create/update secret
await standardRequest.post(`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{
name:`env.${key}`,
value: secrets[key].value
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
await standardRequest.post(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{
name: `env.${key}`,
value: secrets[key].value
},
});
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
}
@@ -3034,4 +3050,4 @@ const syncSecretsNorthflank = async ({
);
};
export { syncSecrets };
export { syncSecrets };

View File

@@ -257,7 +257,9 @@ export const CreateSecretRawV3 = z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/"),
secretValue: z.string().trim(),
secretValue: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
secretComment: z.string().trim(),
skipMultilineEncoding: z.boolean().optional(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL])
@@ -274,7 +276,9 @@ export const UpdateSecretByNameRawV3 = z.object({
body: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretValue: z.string().trim(),
secretValue: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
secretPath: z.string().trim().default("/"),
skipMultilineEncoding: z.boolean().optional(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED)

View File

@@ -11,6 +11,8 @@ import { secretKeys } from "@app/hooks/api/secrets/queries";
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
import { UserWsKeyPair, WsTag } from "@app/hooks/api/types";
import { secretSnapshotKeys } from "~/hooks/api/secretSnapshots/queries";
import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types";
import { SecretDetailSidebar } from "./SecretDetaiSidebar";
import { SecretItem } from "./SecretItem";
@@ -218,8 +220,13 @@ export const SecretListView = ({
queryClient.invalidateQueries(
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
);
queryClient.invalidateQueries(
secretSnapshotKeys.list({ workspaceId, environment, directory: secretPath })
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({ workspaceId, environment, directory: secretPath })
);
handlePopUpClose("secretDetail");
createNotification({
type: "success",
text: "Successfully saved secrets"
@@ -242,6 +249,12 @@ export const SecretListView = ({
queryClient.invalidateQueries(
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
);
queryClient.invalidateQueries(
secretSnapshotKeys.list({ workspaceId, environment, directory: secretPath })
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({ workspaceId, environment, directory: secretPath })
);
handlePopUpClose("deleteSecret");
handlePopUpClose("secretDetail");
createNotification({

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
import { z } from "zod";
export enum SecretActionType {
@@ -7,11 +8,16 @@ export enum SecretActionType {
}
export const formSchema = z.object({
key: z.string(),
value: z.string(),
idOverride: z.string().optional(),
valueOverride: z.string().optional(),
overrideAction: z.string().optional(),
key: z.string().trim(),
value: z.string().transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
idOverride: z.string().trim().optional(),
valueOverride: z
.string()
.optional()
.transform((val) =>
typeof val === "string" ? (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()) : val
),
overrideAction: z.string().trim().optional(),
comment: z.string().trim().optional(),
skipMultilineEncoding: z.boolean().optional(),
tags: z