Changed frontend to use the new secrets routes

This commit is contained in:
Vladyslav Matsiiako
2023-01-09 13:14:07 -08:00
parent b6189a90f4
commit 486aa139c2
23 changed files with 648 additions and 390 deletions
@@ -273,7 +273,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
}); });
await Secret.bulkWrite(ops); await Secret.bulkWrite(ops);
let newSecretsObj: { [key: string]: PatchSecret } = {}; const newSecretsObj: { [key: string]: PatchSecret } = {};
req.body.secrets.forEach((secret: PatchSecret) => { req.body.secrets.forEach((secret: PatchSecret) => {
newSecretsObj[secret.id] = secret; newSecretsObj[secret.id] = secret;
}); });
@@ -323,7 +323,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
// group secrets into workspaces so updated secrets can // group secrets into workspaces so updated secrets can
// be logged and snapshotted separately for each workspace // be logged and snapshotted separately for each workspace
let workspaceSecretObj: any = {}; const workspaceSecretObj: any = {};
req.secrets.forEach((s: any) => { req.secrets.forEach((s: any) => {
if (s.workspace.toString() in workspaceSecretObj) { if (s.workspace.toString() in workspaceSecretObj) {
workspaceSecretObj[s.workspace.toString()].push(s); workspaceSecretObj[s.workspace.toString()].push(s);
@@ -399,7 +399,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
// group secrets into workspaces so deleted secrets can // group secrets into workspaces so deleted secrets can
// be logged and snapshotted separately for each workspace // be logged and snapshotted separately for each workspace
let workspaceSecretObj: any = {}; const workspaceSecretObj: any = {};
req.secrets.forEach((s: any) => { req.secrets.forEach((s: any) => {
if (s.workspace.toString() in workspaceSecretObj) { if (s.workspace.toString() in workspaceSecretObj) {
workspaceSecretObj[s.workspace.toString()].push(s); workspaceSecretObj[s.workspace.toString()].push(s);
@@ -445,7 +445,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
} }
}); });
return res.status(400).send({ return res.status(200).send({
secrets: req.secrets secrets: req.secrets
}); });
} }
+1 -1
View File
@@ -138,7 +138,7 @@ router.patch(
router.delete( router.delete(
'/', '/',
[ [
check('secretIds') body('secretIds')
.exists() .exists()
.custom((value) => { .custom((value) => {
// case: delete 1 secret // case: delete 1 secret
+4 -4
View File
@@ -7,12 +7,12 @@ import {
} from '../config'; } from '../config';
import { getLogger } from '../utils/logger'; import { getLogger } from '../utils/logger';
if(TELEMETRY_ENABLED){ if(!TELEMETRY_ENABLED){
getLogger("backend-main").info([ getLogger("backend-main").info([
"", "",
"Infisical collects telemetry data about general usage.", "To improve, Infisical collects telemetry data about general usage.",
"The data helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth for investors as we support Infisical as open-source software.", "This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
"To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the environment variables", "To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
].join('\n')) ].join('\n'))
} }
+1 -1
View File
@@ -34,7 +34,7 @@ interface ToggleProps {
* @param {string} obj.value - value of a certain secret * @param {string} obj.value - value of a certain secret
* @param {number} obj.pos - position of a certain secret * @param {number} obj.pos - position of a certain secret
#TODO: make the secret id persistent? #TODO: make the secret id persistent?
* @param {string} obj.id - id of a certain secret * @param {string} obj.id - id of a certain secret (NOTE: THIS IS THE ID OF THE MAIN SECRET - NOT OF AN OVERRIDE)
* @param {function} obj.deleteOverride - a function that deleted an override for a certain secret * @param {function} obj.deleteOverride - a function that deleted an override for a certain secret
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden. * @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually * @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
@@ -6,10 +6,10 @@ import { useTranslation } from "next-i18next";
const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => { const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => {
const { t } = useTranslation(); const { t } = useTranslation();
return <div className={`relative mt-4 px-4 pt-4`}> return <div className={`relative mt-4 px-4 pt-6`}>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.comments")}</p> <p className='text-sm text-bunker-300 pl-0.5'>{t("dashboard:sidebar.comments")}</p>
<textarea <textarea
className="bg-bunker-800 h-32 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70" className="bg-bunker-800 placeholder:text-bunker-400 h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
value={comment} value={comment}
onChange={(e) => modifyComment(e.target.value, position)} onChange={(e) => modifyComment(e.target.value, position)}
placeholder="Leave any comments here..." placeholder="Leave any comments here..."
@@ -0,0 +1,75 @@
import { Fragment } from 'react';
import { useTranslation } from "next-i18next";
import { faDownload } from '@fortawesome/free-solid-svg-icons';
import { Menu, Transition } from '@headlessui/react';
import Button from '../basic/buttons/Button';
import downloadDotEnv from '../utilities/secrets/downloadDotEnv';
import downloadYaml from '../utilities/secrets/downloadYaml';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This is the menu that is used to download secrets as .env ad .yml files (in future we may have more options)
* @param {object} obj
* @param {SecretDataProps[]} obj.data - secrets that we want to downlaod
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
*/
const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
const { t } = useTranslation();
return <Menu
as="div"
className="relative inline-block text-left"
>
<Menu.Button
as="div"
className="inline-flex w-full justify-center text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
>
<Button
color="mineshaft"
size="icon-md"
icon={faDownload}
onButtonPressed={() => {}}
/>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
<Menu.Item>
<Button
color="mineshaft"
onButtonPressed={() => downloadDotEnv({ data, env })}
size="md"
text="Download as .env"
/>
</Menu.Item>
<Menu.Item>
<Button
color="mineshaft"
onButtonPressed={() => downloadYaml({ data, env })}
size="md"
text="Download as .yml"
/>
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
}
export default DownloadSecretMenu;
+1 -1
View File
@@ -158,7 +158,7 @@ const SideBar = ({
</div> </div>
</div> </div>
<SecretVersionList secretId={data[0]?.id} /> <SecretVersionList secretId={data[0]?.id} />
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} /> <CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
</div> </div>
)} )}
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}> <div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
@@ -1,35 +1,46 @@
import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm'; import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm';
import login1 from '~/pages/api/auth/Login1'; import login1 from '~/pages/api/auth/Login1';
import login2 from '~/pages/api/auth/Login2'; import login2 from '~/pages/api/auth/Login2';
import addSecrets from '~/pages/api/files/AddSecrets';
import getOrganizations from '~/pages/api/organization/getOrgs'; import getOrganizations from '~/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects'; import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects';
import pushKeys from './secrets/pushKeys'; import encryptSecrets from './secrets/encryptSecrets';
import Telemetry from './telemetry/Telemetry'; import Telemetry from './telemetry/Telemetry';
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage'; import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
import SecurityClient from './SecurityClient'; import SecurityClient from './SecurityClient';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
const nacl = require('tweetnacl'); const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util'); nacl.util = require('tweetnacl-util');
const jsrp = require('jsrp'); const jsrp = require('jsrp');
const client = new jsrp.client(); const client = new jsrp.client();
/** /**
* This function loggs in the user (whether it's right after signup, or a normal login) * This function logs in the user (whether it's right after signup, or a normal login)
* @param {*} email * @param {string} email - email of the user logging in
* @param {*} password * @param {string} password - password of the user logging in
* @param {*} setErrorLogin * @param {function} setErrorLogin - function that visually dispay an error is something is wrong
* @param {*} router * @param {*} router
* @param {*} isSignUp * @param {boolean} isSignUp - whether this log in is a part of signup
* @param {boolean} isLogin - ?
* @returns * @returns
*/ */
const attemptLogin = async ( const attemptLogin = async (
email, email: string,
password, password: string,
setErrorLogin, setErrorLogin: (value: boolean) => void,
router, router: any,
isSignUp, isSignUp: boolean,
isLogin isLogin: boolean
) => { ) => {
try { try {
const telemetry = new Telemetry().getInstance(); const telemetry = new Telemetry().getInstance();
@@ -76,7 +87,7 @@ const attemptLogin = async (
}); });
const userOrgs = await getOrganizations(); const userOrgs = await getOrganizations();
const userOrgsData = userOrgs.map((org) => org._id); const userOrgsData = userOrgs.map((org: { _id: string; }) => org._id);
let orgToLogin; let orgToLogin;
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) { if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
@@ -90,7 +101,7 @@ const attemptLogin = async (
orgId: orgToLogin orgId: orgToLogin
}); });
orgUserProjects = orgUserProjects?.map((project) => project._id); orgUserProjects = orgUserProjects?.map((project: { _id: string; }) => project._id);
let projectToLogin; let projectToLogin;
if ( if (
orgUserProjects.includes(localStorage.getItem('projectData.id')) orgUserProjects.includes(localStorage.getItem('projectData.id'))
@@ -104,26 +115,7 @@ const attemptLogin = async (
console.log('ERROR: User likely has no projects. ', error); console.log('ERROR: User likely has no projects. ', error);
} }
} }
// If user is logging in for the first time, add the example keys
if (isSignUp) {
await pushKeys({
obj: {
sDATABASE_URL: [
'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
'This is an example of secret referencing.'
],
sDB_USERNAME: ['OVERRIDE_THIS', ''],
sDB_PASSWORD: ['OVERRIDE_THIS', ''],
pDB_USERNAME: ['user1234', 'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need.'],
pDB_PASSWORD: ['example_password', 'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need.'],
sTWILIO_AUTH_TOKEN: ['example_twillio_token', ''],
sWEBSITE_URL: ['http://localhost:3000', ''],
},
workspaceId: projectToLogin,
env: 'Development'
});
}
if (email) { if (email) {
telemetry.identify(email); telemetry.identify(email);
telemetry.capture('User Logged In'); telemetry.capture('User Logged In');
@@ -133,6 +125,7 @@ const attemptLogin = async (
router.push('/dashboard/'); router.push('/dashboard/');
} }
} catch (error) { } catch (error) {
console.log(error)
setErrorLogin(true); setErrorLogin(true);
console.log('Login response not available'); console.log('Login response not available');
} }
@@ -0,0 +1,33 @@
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This function downloads the secrets as a .env file
* @param {object} obj
* @param {SecretDataProps[]} obj.data - secrets that we want to check for overrides
* @returns
*/
const checkOverrides = async ({ data }: { data: SecretDataProps[]; }) => {
let secrets : SecretDataProps[] = data!.map((secret) => Object.create(secret));
const overridenSecrets = data!.filter(
(secret) => secret.type === 'personal'
);
if (overridenSecrets.length) {
overridenSecrets.forEach((secret) => {
const index = secrets!.findIndex(
(_secret) => _secret.key === secret.key && _secret.type === 'shared'
);
secrets![index].value = secret.value;
});
secrets = secrets!.filter((secret) => secret.type === 'shared');
}
return secrets;
}
export default checkOverrides;
@@ -0,0 +1,46 @@
import { envMapping } from "../../../public/data/frequentConstants";
import checkOverrides from './checkOverrides';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This function downloads the secrets as a .env file
* @param {object} obj
* @param {SecretDataProps[]} obj.data - secrets that we want to download
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
*/
const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
if (!data) return;
const secrets = await checkOverrides({ data });
const file = secrets!
.map(
(item: SecretDataProps) =>
`${
item.comment
? item.comment
.split('\n')
.map((comment) => '# '.concat(comment))
.join('\n') + '\n'
: ''
}` + [item.key, item.value].join('=')
)
.join('\n');
const blob = new Blob([file]);
const fileDownloadUrl = URL.createObjectURL(blob);
const alink = document.createElement('a');
alink.href = fileDownloadUrl;
alink.download = envMapping[env] + '.env';
alink.click();
}
export default downloadDotEnv;
@@ -0,0 +1,52 @@
import YAML from 'yaml';
import { YAMLSeq } from 'yaml/types'
import { envMapping } from "../../../public/data/frequentConstants";
import checkOverrides from './checkOverrides';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This function downloads the secrets as a .yml file
* @param {object} obj
* @param {SecretDataProps[]} obj.data - secrets that we want to download
* @param {string} obj.env - used for naming the file
* @returns
*/
const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
if (!data) return;
const doc = new YAML.Document();
doc.contents = new YAMLSeq()
const secrets = await checkOverrides({ data });
secrets.forEach((secret) => {
const pair = YAML.createNode({ [secret.key]: secret.value });
pair.commentBefore = secret.comment
.split('\n')
.map((line) => (line ? ' '.concat(line) : ''))
.join('\n');
doc.add(pair);
});
const file = doc
.toString()
.split('\n')
.map((line) => (line.startsWith('-') ? line.replace('- ', '') : line))
.join('\n');
const blob = new Blob([file]);
const fileDownloadUrl = URL.createObjectURL(blob);
const alink = document.createElement('a');
alink.href = fileDownloadUrl;
alink.download = envMapping[env] + '.yml';
alink.click();
}
export default downloadYaml;
@@ -0,0 +1,116 @@
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
const crypto = require("crypto");
const {
decryptAssymmetric,
encryptSymmetric,
} = require("../cryptography/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
interface EncryptedSecretProps {
id: string;
createdAt: string;
environment: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
type: "personal" | "shared";
}
/**
* Encypt secrets before pushing the to the DB
* @param {object} obj
* @param {object} obj.secretsToEncrypt - secrets that we want to encrypt
* @param {object} obj.workspaceId - the id of a project in which we are encrypting secrets
* @returns
*/
const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsToEncrypt: SecretDataProps[]; workspaceId: string; env: string; }) => {
const sharedKey = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
let randomBytes: string;
if (Object.keys(sharedKey).length > 0) {
// case: a (shared) key exists for the workspace
randomBytes = decryptAssymmetric({
ciphertext: sharedKey.latestKey.encryptedKey,
nonce: sharedKey.latestKey.nonce,
publicKey: sharedKey.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY,
});
} else {
// case: a (shared) key does not exist for the workspace
randomBytes = crypto.randomBytes(16).toString("hex");
}
const secrets = secretsToEncrypt.map((secret) => {
// encrypt key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag,
} = encryptSymmetric({
plaintext: secret.key,
key: randomBytes,
});
// encrypt value
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag,
} = encryptSymmetric({
plaintext: secret.value,
key: randomBytes,
});
// encrypt comment
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag,
} = encryptSymmetric({
plaintext: secret.comment ?? '',
key: randomBytes,
});
const result: EncryptedSecretProps = {
id: secret.id,
createdAt: '',
environment: env,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
type: secret.type,
};
return result;
});
return secrets;
}
export default encryptSecrets;
@@ -1,7 +1,7 @@
import getSecrets from '~/pages/api/files/GetSecrets'; import getSecrets from '~/pages/api/files/GetSecrets';
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
import { envMapping } from '../../../public/data/frequentConstants'; import { envMapping } from '../../../public/data/frequentConstants';
import guidGenerator from '../randomId';
const { const {
decryptAssymmetric, decryptAssymmetric,
@@ -10,6 +10,22 @@ const {
const nacl = require('tweetnacl'); const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util'); nacl.util = require('tweetnacl-util');
interface EncryptedSecretProps {
_id: string;
createdAt: string;
environment: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
type: "personal" | "shared";
}
interface SecretProps { interface SecretProps {
key: string; key: string;
value: string; value: string;
@@ -18,107 +34,102 @@ interface SecretProps {
id: string; id: string;
} }
interface Props { interface FunctionProps {
env: keyof typeof envMapping; env: keyof typeof envMapping;
setFileState: any;
setIsKeyAvailable: any; setIsKeyAvailable: any;
setData: any; setData: any;
workspaceId: string; workspaceId: string;
} }
/**
* Gets the secrets for a certain project
* @param {object} obj
* @param {string} obj.env - environment for which we are getting secrets
* @param {boolean} obj.isKeyAvailable - if a person is able to create new key pairs
* @param {function} obj.setData - state function that manages the state of secrets in the dashboard
* @param {string} obj.workspaceId - id of a workspace for which we are getting secrets
*/
const getSecretsForProject = async ({ const getSecretsForProject = async ({
env, env,
setFileState,
setIsKeyAvailable, setIsKeyAvailable,
setData, setData,
workspaceId workspaceId
}: Props) => { }: FunctionProps) => {
try { try {
let file; let encryptedSecrets;
try { try {
file = await getSecrets(workspaceId, envMapping[env]); encryptedSecrets = await getSecrets(workspaceId, envMapping[env]);
setFileState(file);
} catch (error) { } catch (error) {
console.log('ERROR: Not able to access the latest file'); console.log('ERROR: Not able to access the latest version of secrets');
} }
const latestKey = await getLatestFileKey({ workspaceId })
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs // This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
setIsKeyAvailable(!file.key ? file.secrets.length == 0 : true); setIsKeyAvailable(!latestKey ? encryptedSecrets.length == 0 : true);
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
const tempFileState: SecretProps[] = []; const tempDecryptedSecrets: SecretProps[] = [];
if (file.key) { if (latestKey) {
// assymmetrically decrypt symmetric key with local private key // assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({ const key = decryptAssymmetric({
ciphertext: file.key.encryptedKey, ciphertext: latestKey.latestKey.encryptedKey,
nonce: file.key.nonce, nonce: latestKey.latestKey.nonce,
publicKey: file.key.sender.publicKey, publicKey: latestKey.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY privateKey: PRIVATE_KEY
}); });
file.secrets.map((secretPair: any) => { // decrypt secret keys, values, and comments
// decrypt .env file with symmetric key encryptedSecrets.map((secret: EncryptedSecretProps) => {
const plainTextKey = decryptSymmetric({ const plainTextKey = decryptSymmetric({
ciphertext: secretPair.secretKey.ciphertext, ciphertext: secret.secretKeyCiphertext,
iv: secretPair.secretKey.iv, iv: secret.secretKeyIV,
tag: secretPair.secretKey.tag, tag: secret.secretKeyTag,
key key
}); });
const plainTextValue = decryptSymmetric({ const plainTextValue = decryptSymmetric({
ciphertext: secretPair.secretValue.ciphertext, ciphertext: secret.secretValueCiphertext,
iv: secretPair.secretValue.iv, iv: secret.secretValueIV,
tag: secretPair.secretValue.tag, tag: secret.secretValueTag,
key key
}); });
let plainTextComment; let plainTextComment;
if (secretPair.secretComment.ciphertext) { if (secret.secretCommentCiphertext) {
plainTextComment = decryptSymmetric({ plainTextComment = decryptSymmetric({
ciphertext: secretPair.secretComment.ciphertext, ciphertext: secret.secretCommentCiphertext,
iv: secretPair.secretComment.iv, iv: secret.secretCommentIV,
tag: secretPair.secretComment.tag, tag: secret.secretCommentTag,
key key
}); });
} else { } else {
plainTextComment = ""; plainTextComment = "";
} }
tempFileState.push({ tempDecryptedSecrets.push({
id: secretPair._id, id: secret._id,
key: plainTextKey, key: plainTextKey,
value: plainTextValue, value: plainTextValue,
type: secretPair.type, type: secret.type,
comment: plainTextComment comment: plainTextComment
}); });
}); });
} }
setFileState(tempFileState);
setData( const result = tempDecryptedSecrets.map((secret, index) => {
tempFileState.map((line, index) => {
return {
id: line['id'],
pos: index,
key: line['key'],
value: line['value'],
type: line['type'],
comment: line['comment']
};
})
);
return tempFileState.map((line, index) => {
return { return {
id: line['id'], id: secret['id'],
pos: index, pos: index,
key: line['key'], key: secret['key'],
value: line['value'], value: secret['value'],
type: line['type'], type: secret['type'],
comment: line['comment'] comment: secret['comment']
}; };
}); });
setData(result);
return result;
} catch (error) { } catch (error) {
console.log('Something went wrong during accessing or decripting secrets.'); console.log('Something went wrong during accessing or decripting secrets.');
} }
@@ -1,126 +0,0 @@
import uploadSecrets from "~/pages/api/files/UploadSecrets";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import getWorkspaceKeys from "~/pages/api/workspace/getWorkspaceKeys";
import { envMapping } from "../../../public/data/frequentConstants";
const crypto = require("crypto");
const {
decryptAssymmetric,
encryptSymmetric,
encryptAssymmetric,
} = require("../cryptography/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
export interface IK {
publicKey: string;
userId: string;
}
/**
* This function pushes the keys to the database after decrypting them end-to-end
* @param {object} obj
* @param {object} obj.obj - object with all the key pairs
* @param {object} obj.workspaceId - the id of a project to which a user is pushing
* @param {object} obj.env - which environment a user is pushing to
*/
const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: string; env: string; }) => {
const sharedKey = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
let randomBytes: string;
if (Object.keys(sharedKey).length > 0) {
// case: a (shared) key exists for the workspace
randomBytes = decryptAssymmetric({
ciphertext: sharedKey.latestKey.encryptedKey,
nonce: sharedKey.latestKey.nonce,
publicKey: sharedKey.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY,
});
} else {
// case: a (shared) key does not exist for the workspace
randomBytes = crypto.randomBytes(16).toString("hex");
}
const secrets = Object.keys(obj).map((key) => {
// encrypt key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag,
} = encryptSymmetric({
plaintext: key.slice(1),
key: randomBytes,
});
// encrypt value
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag,
} = encryptSymmetric({
plaintext: obj[key as keyof typeof obj][0],
key: randomBytes,
});
// encrypt comment
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag,
} = encryptSymmetric({
plaintext: obj[key as keyof typeof obj][1],
key: randomBytes,
});
const visibility = key.charAt(0) == "p" ? "personal" : "shared";
return {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash: crypto.createHash("sha256").update(key.slice(1)).digest("hex"),
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash: crypto.createHash("sha256").update(obj[key as keyof typeof obj][0]).digest("hex"),
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash: crypto.createHash("sha256").update(obj[key as keyof typeof obj][1]).digest("hex"),
type: visibility,
};
});
// obtain public keys of all receivers (i.e. members in workspace)
const publicKeys = await getWorkspaceKeys({
workspaceId,
});
// assymmetrically encrypt key with each receiver public keys
const keys = publicKeys.map((k: IK) => {
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: k.publicKey,
privateKey: PRIVATE_KEY,
});
return {
encryptedKey: ciphertext,
nonce,
userId: k.userId,
};
});
// send payload
await uploadSecrets({
workspaceId,
secrets,
keys,
environment: envMapping[env as keyof typeof envMapping],
});
};
export default pushKeys;
@@ -1,79 +0,0 @@
import publicKeyInfical from '~/pages/api/auth/publicKeyInfisical';
import changeHerokuConfigVars from '~/pages/api/integrations/ChangeHerokuConfigVars';
const crypto = require('crypto');
const {
encryptSymmetric,
encryptAssymmetric
} = require('../cryptography/crypto');
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
interface Props {
obj: Record<string, string>;
integrationId: string;
}
const pushKeysIntegration = async ({ obj, integrationId }: Props) => {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
const randomBytes = crypto.randomBytes(16).toString('hex');
const secrets = Object.keys(obj).map((key) => {
// encrypt key
const {
ciphertext: ciphertextKey,
iv: ivKey,
tag: tagKey
} = encryptSymmetric({
plaintext: key,
key: randomBytes
});
// encrypt value
const {
ciphertext: ciphertextValue,
iv: ivValue,
tag: tagValue
} = encryptSymmetric({
plaintext: obj[key],
key: randomBytes
});
const visibility = 'shared';
return {
ciphertextKey,
ivKey,
tagKey,
hashKey: crypto.createHash('sha256').update(key).digest('hex'),
ciphertextValue,
ivValue,
tagValue,
hashValue: crypto.createHash('sha256').update(obj[key]).digest('hex'),
type: visibility
};
});
// obtain public keys of all receivers (i.e. members in workspace)
const publicKeyInfisical = await publicKeyInfical();
const publicKey = (await publicKeyInfisical.json()).publicKey;
// assymmetrically encrypt key with each receiver public keys
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey,
privateKey: PRIVATE_KEY
});
const key = {
encryptedKey: ciphertext,
nonce
};
changeHerokuConfigVars({ integrationId, key, secrets });
};
export default pushKeysIntegration;
-1
View File
@@ -20,7 +20,6 @@ const getActionData = async ({ actionId }: workspaceProps) => {
} }
} }
).then(async (res) => { ).then(async (res) => {
console.log(188, res)
if (res && res.status == 200) { if (res && res.status == 200) {
return (await res.json()).action; return (await res.json()).action;
} else { } else {
@@ -0,0 +1,30 @@
import SecurityClient from '~/utilities/SecurityClient';
/**
* This function performs a rollback of secrets in a certain project
* @param {object} obj
* @param {string} obj.workspaceId - id of the project for which we are rolling back data
* @param {number} obj.version - version to which we are rolling back
* @returns
*/
const performSecretRollback = async ({ workspaceId, version }: { workspaceId: string; version: number; }) => {
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + "/secret-snapshots/rollback", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
version
})
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json());
} else {
console.log('Failed to perform the secret rollback');
}
});
};
export default performSecretRollback;
@@ -111,7 +111,7 @@ const PITRecoverySidebar = ({
} }
}) })
setSnapshotData({ id: secretSnapshotData._id, createdAt: secretSnapshotData.createdAt, secretVersions: decryptedSecretVersions }) setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: decryptedSecretVersions })
} }
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}> return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
+48
View File
@@ -0,0 +1,48 @@
import SecurityClient from '~/utilities/SecurityClient';
interface EncryptedSecretProps {
id: string;
createdAt: string;
environment: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
type: "personal" | "shared";
}
/**
* This function adds secrets to a certain project
* @param {object} obj
* @param {EncryptedSecretProps} obj.secrets - the ids of secrets that we want to add
* @param {string} obj.env - the environment to which we are adding secrets
* @param {string} obj.workspaceId - the project to which we are adding secrets
* @returns
*/
const addSecrets = async ({ secrets, env, workspaceId }: { secrets: EncryptedSecretProps[]; env: string; workspaceId: string; }) => {
return SecurityClient.fetchCall('/api/v2/secrets', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
environment: env,
workspaceId,
secrets
})
}
).then(async (res) => {
if (res && res.status == 200) {
return await res.json();
} else {
console.log('Failed to add certain project secrets');
}
});
};
export default addSecrets;
+27
View File
@@ -0,0 +1,27 @@
import SecurityClient from '~/utilities/SecurityClient';
/**
* This function deletes certain secrets from a certain project
* @param {string[]} secretIds - the ids of secrets that we want to be deleted
* @returns
*/
const deleteSecrets = async ({ secretIds }: { secretIds: string[] }) => {
return SecurityClient.fetchCall('/api/v2/secrets', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
secretIds
})
}
).then(async (res) => {
if (res && res.status == 200) {
return await res.json();
} else {
console.log('Failed to delete certain project secrets');
}
});
};
export default deleteSecrets;
+4 -6
View File
@@ -1,19 +1,17 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '~/utilities/SecurityClient';
/** /**
* This function fetches the encrypted secrets from the .env file * This function fetches the encrypted secrets for a certain project
* @param {string} workspaceId - project is for which a user is trying to get secrets * @param {string} workspaceId - project is for which a user is trying to get secrets
* @param {string} env - environment of a project for which a user is trying ot get secrets * @param {string} env - environment of a project for which a user is trying ot get secrets
* @returns * @returns
*/ */
const getSecrets = async (workspaceId: string, env: string) => { const getSecrets = async (workspaceId: string, env: string) => {
return SecurityClient.fetchCall( return SecurityClient.fetchCall(
'/api/v1/secret/' + '/api/v2/secrets?' +
workspaceId +
'?' +
new URLSearchParams({ new URLSearchParams({
environment: env, environment: env,
channel: 'web' workspaceId
}), }),
{ {
method: 'GET', method: 'GET',
@@ -23,7 +21,7 @@ const getSecrets = async (workspaceId: string, env: string) => {
} }
).then(async (res) => { ).then(async (res) => {
if (res && res.status == 200) { if (res && res.status == 200) {
return await res.json(); return (await res.json()).secrets;
} else { } else {
console.log('Failed to get project secrets'); console.log('Failed to get project secrets');
} }
+44
View File
@@ -0,0 +1,44 @@
import SecurityClient from '~/utilities/SecurityClient';
interface EncryptedSecretProps {
id: string;
createdAt: string;
environment: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
type: "personal" | "shared";
}
/**
* This function updates certain secrets in a certain project
* @param {object} obj
* @param {EncryptedSecretProps[]} obj.secrets - the ids of secrets that we want to update
* @returns
*/
const updateSecrets = async ({ secrets }: { secrets: EncryptedSecretProps[] }) => {
return SecurityClient.fetchCall('/api/v2/secrets', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
secrets
})
}
).then(async (res) => {
if (res && res.status == 200) {
return await res.json();
} else {
console.log('Failed to update certain project secrets');
}
});
};
export default updateSecrets;
+69 -78
View File
@@ -9,7 +9,6 @@ import {
faArrowLeft, faArrowLeft,
faCheck, faCheck,
faClockRotateLeft, faClockRotateLeft,
faDownload,
faEye, faEye,
faEyeSlash, faEyeSlash,
faFolderOpen, faFolderOpen,
@@ -18,28 +17,32 @@ import {
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import getProjectSercetSnapshotsCount from 'ee/api/secrets/GetProjectSercetSnapshotsCount'; import getProjectSercetSnapshotsCount from 'ee/api/secrets/GetProjectSercetSnapshotsCount';
import performSecretRollback from 'ee/api/secrets/PerformSecretRollback';
import PITRecoverySidebar from 'ee/components/PITRecoverySidebar'; import PITRecoverySidebar from 'ee/components/PITRecoverySidebar';
import Button from '~/components/basic/buttons/Button'; import Button from '~/components/basic/buttons/Button';
import ListBox from '~/components/basic/Listbox'; import ListBox from '~/components/basic/Listbox';
import BottonRightPopup from '~/components/basic/popups/BottomRightPopup'; import BottonRightPopup from '~/components/basic/popups/BottomRightPopup';
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
import DownloadSecretMenu from '~/components/dashboard/DownloadSecretsMenu';
import DropZone from '~/components/dashboard/DropZone'; import DropZone from '~/components/dashboard/DropZone';
import KeyPair from '~/components/dashboard/KeyPair'; import KeyPair from '~/components/dashboard/KeyPair';
import SideBar from '~/components/dashboard/SideBar'; import SideBar from '~/components/dashboard/SideBar';
import NavHeader from '~/components/navigation/NavHeader'; import NavHeader from '~/components/navigation/NavHeader';
import encryptSecrets from '~/components/utilities/secrets/encryptSecrets';
import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject'; import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject';
import pushKeys from '~/components/utilities/secrets/pushKeys';
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps'; import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
import guidGenerator from '~/utilities/randomId'; import guidGenerator from '~/utilities/randomId';
import { envMapping, reverseEnvMapping } from '../../public/data/frequentConstants'; import { envMapping, reverseEnvMapping } from '../../public/data/frequentConstants';
import addSecrets from '../api/files/AddSecrets';
import deleteSecrets from '../api/files/DeleteSecrets';
import updateSecrets from '../api/files/UpdateSecrets';
import getUser from '../api/user/getUser'; import getUser from '../api/user/getUser';
import checkUserAction from '../api/userActions/checkUserAction'; import checkUserAction from '../api/userActions/checkUserAction';
import registerUserAction from '../api/userActions/registerUserAction'; import registerUserAction from '../api/userActions/registerUserAction';
import getWorkspaces from '../api/workspace/getWorkspaces'; import getWorkspaces from '../api/workspace/getWorkspaces';
const queryString = require("query-string");
interface SecretDataProps { interface SecretDataProps {
type: 'personal' | 'shared'; type: 'personal' | 'shared';
@@ -50,9 +53,18 @@ interface SecretDataProps {
comment: string; comment: string;
} }
interface overrideProps {
id: string;
keyName: string;
value: string;
pos: number;
comment: string;
}
interface SnapshotProps { interface SnapshotProps {
id: string; id: string;
createdAt: string; createdAt: string;
version: number;
secretVersions: { secretVersions: {
id: string; id: string;
pos: number; pos: number;
@@ -87,7 +99,7 @@ function findDuplicates(arr: any[]) {
*/ */
export default function Dashboard() { export default function Dashboard() {
const [data, setData] = useState<SecretDataProps[] | null>(); const [data, setData] = useState<SecretDataProps[] | null>();
const [fileState, setFileState] = useState<SecretDataProps[]>([]); const [initialData, setInitialData] = useState<SecretDataProps[]>([]);
const [buttonReady, setButtonReady] = useState(false); const [buttonReady, setButtonReady] = useState(false);
const router = useRouter(); const router = useRouter();
const [workspaceId, setWorkspaceId] = useState(''); const [workspaceId, setWorkspaceId] = useState('');
@@ -194,11 +206,11 @@ export default function Dashboard() {
const dataToSort = await getSecretsForProject({ const dataToSort = await getSecretsForProject({
env, env,
setFileState,
setIsKeyAvailable, setIsKeyAvailable,
setData, setData,
workspaceId: String(router.query.id) workspaceId: String(router.query.id)
}); });
setInitialData(dataToSort);
reorderRows(dataToSort); reorderRows(dataToSort);
setSharedToHide( setSharedToHide(
@@ -234,14 +246,6 @@ export default function Dashboard() {
]); ]);
}; };
interface overrideProps {
id: string;
keyName: string;
value: string;
pos: number;
comment: string;
}
/** /**
* This function add an ovverrided version of a certain secret to the current user * This function add an ovverrided version of a certain secret to the current user
* @param {object} obj * @param {object} obj
@@ -273,12 +277,12 @@ export default function Dashboard() {
text: `${secretName} has been deleted. Remember to save changes.`, text: `${secretName} has been deleted. Remember to save changes.`,
type: 'error' type: 'error'
}); });
setData(data!.filter((row: SecretDataProps) => !ids.includes(row.id))); sortValuesHandler(data!.filter((row: SecretDataProps) => !ids.includes(row.id)), sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical");
}; };
/** /**
* This function deleted the override of a certain secrer * This function deleted the override of a certain secrer
* @param {string} id - id of a secret to be deleted * @param {string} id - id of a shared secret; the override with the same key should be deleted
*/ */
const deleteOverride = (id: string) => { const deleteOverride = (id: string) => {
setButtonReady(true); setButtonReady(true);
@@ -291,7 +295,7 @@ export default function Dashboard() {
setSharedToHide(sharedToHide!.filter(tempId => tempId != sharedVersionOfOverride)) setSharedToHide(sharedToHide!.filter(tempId => tempId != sharedVersionOfOverride))
// resort secrets // resort secrets
const tempData = data!.filter((row: SecretDataProps) => !(row.id == id && row.type == 'personal')) const tempData = data!.filter((row: SecretDataProps) => !(row.key == data!.filter(row => row.id == id)[0]?.key && row.type == 'personal'))
sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical") sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical")
}; };
@@ -311,14 +315,6 @@ export default function Dashboard() {
setButtonReady(true); setButtonReady(true);
}; };
const modifyVisibility = (value: "shared" | "personal", pos: number) => {
setData((oldData) => {
oldData![pos].type = value;
return [...oldData!];
});
setButtonReady(true);
};
const modifyComment = (value: string, pos: number) => { const modifyComment = (value: string, pos: number) => {
setData((oldData) => { setData((oldData) => {
oldData![pos].comment = value; oldData![pos].comment = value;
@@ -336,10 +332,6 @@ export default function Dashboard() {
modifyKey(value, pos); modifyKey(value, pos);
}, []); }, []);
const listenChangeVisibility = useCallback((value: "shared" | "personal", pos: number) => {
modifyVisibility(value, pos);
}, []);
const listenChangeComment = useCallback((value: string, pos: number) => { const listenChangeComment = useCallback((value: string, pos: number) => {
modifyComment(value, pos); modifyComment(value, pos);
}, []); }, []);
@@ -347,22 +339,20 @@ export default function Dashboard() {
/** /**
* Save the changes of environment variables and push them to the database * Save the changes of environment variables and push them to the database
*/ */
const savePush = async (dataToPush?: any[], envToPush?: string) => { const savePush = async (dataToPush?: SecretDataProps[]) => {
let obj; let newData: SecretDataProps[] | null | undefined;
// dataToPush is mostly used for rollbacks, otherwise we always take the current state data // dataToPush is mostly used for rollbacks, otherwise we always take the current state data
if ((dataToPush ?? [])?.length > 0) { if ((dataToPush ?? [])?.length > 0) {
obj = Object.assign( newData = dataToPush;
{},
...dataToPush!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] }))
);
} else { } else {
// Format the new object with environment variables newData = data;
obj = Object.assign(
{},
...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] }))
);
} }
const obj = Object.assign(
{},
...newData!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] }))
);
// Checking if any of the secret keys start with a number - if so, don't do anything // Checking if any of the secret keys start with a number - if so, don't do anything
const nameErrors = !Object.keys(obj) const nameErrors = !Object.keys(obj)
.map((key) => !isNaN(Number(key[0].charAt(0)))) .map((key) => !isNaN(Number(key[0].charAt(0))))
@@ -383,10 +373,37 @@ export default function Dashboard() {
}); });
} }
// Once "Save changed is clicked", disable that button // Once "Save changes" is clicked, disable that button
setButtonReady(false); setButtonReady(false);
console.log(envToPush ? envToPush : env, env, envToPush)
pushKeys({ obj, workspaceId: String(router.query.id), env: envToPush ? envToPush : env }); const secretsToBeDeleted
= initialData
.filter(initDataPoint => !newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id))
.map(secret => secret.id);
const secretsToBeAdded
= newData!
.filter(newDataPoint => !initialData.map(initDataPoint => initDataPoint.id).includes(newDataPoint.id));
const secretsToBeUpdated
= newData!.filter(newDataPoint => initialData
.filter(initDataPoint => newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id)
&& (newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].value != initDataPoint.value
|| newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].key != initDataPoint.key
|| newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].comment != initDataPoint.comment))
.map(secret => secret.id).includes(newDataPoint.id));
if (secretsToBeDeleted.length > 0) {
await deleteSecrets({ secretIds: secretsToBeDeleted });
}
if (secretsToBeAdded.length > 0) {
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId, env: envMapping[env] })
await addSecrets({ secrets, env: envMapping[env], workspaceId });
}
if (secretsToBeUpdated.length > 0) {
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated, workspaceId, env: envMapping[env] })
await updateSecrets({ secrets });
}
// If this user has never saved environment variables before, show them a prompt to read docs // If this user has never saved environment variables before, show them a prompt to read docs
if (!hasUserEverPushed) { if (!hasUserEverPushed) {
@@ -425,19 +442,6 @@ export default function Dashboard() {
setData(sortedData); setData(sortedData);
}; };
// This function downloads the secrets as a .env file
const download = () => {
const file = data!
.map((item: SecretDataProps) => [item.key, item.value].join('='))
.join('\n');
const blob = new Blob([file]);
const fileDownloadUrl = URL.createObjectURL(blob);
const alink = document.createElement('a');
alink.href = fileDownloadUrl;
alink.download = envMapping[env] + '.env';
alink.click();
};
const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => { const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
deleteRow({ids, secretName}); deleteRow({ids, secretName});
}; };
@@ -535,8 +539,6 @@ export default function Dashboard() {
<Button <Button
text={String(t("Rollback to this snapshot"))} text={String(t("Rollback to this snapshot"))}
onButtonPressed={async () => { onButtonPressed={async () => {
const envsToRollback = snapshotData.secretVersions.map(sv => sv.environment).filter((v, i, a) => a.indexOf(v) === i);
// Update secrets in the state only for the current environment // Update secrets in the state only for the current environment
setData( setData(
snapshotData.secretVersions snapshotData.secretVersions
@@ -548,18 +550,9 @@ export default function Dashboard() {
}) })
); );
// Rollback each of the environments in the snapshot // Perform the rollback globally
// #TODO: clean up other environments performSecretRollback({ workspaceId, version: snapshotData.version })
envsToRollback.map(async (envToRollback) => {
await savePush(
snapshotData.secretVersions
.filter(row => row.environment == envToRollback)
.map((sv, position) => {
return {id: sv.id, pos: position, type: sv.type, key: sv.key, value: sv.value, comment: ''}
}),
reverseEnvMapping[envToRollback]
);
});
setSnapshotData(undefined); setSnapshotData(undefined);
createNotification({ createNotification({
text: `Rollback has been performed successfully.`, text: `Rollback has been performed successfully.`,
@@ -614,12 +607,7 @@ export default function Dashboard() {
/> />
</div>} </div>}
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start"> {!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
<Button <DownloadSecretMenu data={data} env={env} />
onButtonPressed={download}
color="mineshaft"
size="icon-md"
icon={faDownload}
/>
</div>} </div>}
<div className="ml-2 min-w-max flex flex-row items-start justify-start"> <div className="ml-2 min-w-max flex flex-row items-start justify-start">
<Button <Button
@@ -672,7 +660,9 @@ export default function Dashboard() {
modifyValue={listenChangeValue} modifyValue={listenChangeValue}
modifyKey={listenChangeKey} modifyKey={listenChangeKey}
isBlurred={blurred} isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)} isDuplicate={findDuplicates(
data?.map((item) => item.key + item.type)
)?.includes(keyPair.key + keyPair.type)}
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId} sidebarSecretId={sidebarSecretId}
isSnapshot={false} isSnapshot={false}
@@ -694,7 +684,9 @@ export default function Dashboard() {
modifyValue={listenChangeValue} modifyValue={listenChangeValue}
modifyKey={listenChangeKey} modifyKey={listenChangeKey}
isBlurred={blurred} isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)} isDuplicate={findDuplicates(
data?.map((item) => item.key + item.type)
)?.includes(keyPair.key + keyPair.type)}
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId} sidebarSecretId={sidebarSecretId}
isSnapshot={true} isSnapshot={true}
@@ -728,7 +720,6 @@ export default function Dashboard() {
/> />
)} )}
{ {
// fileState.message == 'Access needed to pull the latest file' ||
(!isKeyAvailable && ( (!isKeyAvailable && (
<> <>
<FontAwesomeIcon <FontAwesomeIcon