Merge branch 'Infisical:main' into feat/translation-fr

This commit is contained in:
Grraahaam
2023-01-10 14:53:01 +01:00
committed by GitHub
73 changed files with 2416 additions and 895 deletions

View File

@@ -19,7 +19,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import getOrganizations from "~/pages/api/organization/getOrgs";
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers";
import checkUserAction from "~/pages/api/userActions/checkUserAction";
import getUser from "~/pages/api/user/getUser";
import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace";
import createWorkspace from "~/pages/api/workspace/createWorkspace";
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
@@ -39,6 +39,7 @@ import Listbox from "./Listbox";
interface LayoutProps {
children: React.ReactNode;
}
const crypto = require("crypto");
export default function Layout({ children }: LayoutProps) {
const router = useRouter();
@@ -83,12 +84,31 @@ export default function Layout({ children }: LayoutProps) {
});
const newWorkspaceId = newWorkspace._id;
const randomBytes = crypto.randomBytes(16).toString("hex");
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
const myUser = await getUser();
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: myUser.publicKey,
privateKey: PRIVATE_KEY,
}) as { ciphertext: string; nonce: string };
await uploadKeys(
newWorkspaceId,
myUser._id,
ciphertext,
nonce
);
if (addAllUsers) {
console.log('adding other users')
const orgUsers = await getOrganizationUsers({
orgId: tempLocalStorage("orgData.id"),
});
orgUsers.map(async (user: any) => {
if (user.status == "accepted") {
if (user.status == "accepted" && user.email != myUser.email) {
const result = await addUserToWorkspace(
user.user.email,
newWorkspaceId

View File

@@ -34,7 +34,7 @@ interface ToggleProps {
* @param {string} obj.value - value of a certain secret
* @param {number} obj.pos - position of a certain secret
#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 {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

View File

@@ -36,7 +36,7 @@ export default function BottonRightPopup({
}: PopupProps): JSX.Element {
return (
<div
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6"
role="alert"
>
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">

View File

@@ -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 { t } = useTranslation();
return <div className={`relative mt-4 px-4 pt-4`}>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.comments")}</p>
return <div className={`relative mt-4 px-4 pt-6`}>
<p className='text-sm text-bunker-300 pl-0.5'>{t("dashboard:sidebar.comments")}</p>
<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}
onChange={(e) => modifyComment(e.target.value, position)}
placeholder="Leave any comments here..."

View File

@@ -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;

View File

@@ -163,7 +163,7 @@ const SideBar = ({
</div>
</div>
<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 className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>

View File

@@ -23,6 +23,7 @@ import {
interface Integration {
_id: string;
app?: string;
target?: string;
environment: string;
integration: string;
integrationAuth: string;
@@ -69,7 +70,11 @@ const Integration = ({
switch (integration.integration) {
case "vercel":
setIntegrationTarget("Development");
setIntegrationTarget(
integration?.target
? integration.target.charAt(0).toUpperCase() + integration.target.substring(1)
: "Development"
);
break;
case "netlify":
setIntegrationContext(integration?.context ? contextNetlifyMapping[integration.context] : "Local development");
@@ -93,11 +98,11 @@ const Integration = ({
</div>
<ListBox
data={!integration.isActive ? [
"Production",
"Development",
"Preview",
"Development"
"Production"
] : null}
selected={"Production"}
selected={integrationTarget}
onChange={setIntegrationTarget}
/>
</div>

View File

@@ -112,9 +112,9 @@ export default function Navbar() {
href="https://infisical.com/docs/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="text-gray-200 hover:text-primary duration-200">
className="text-gray-200 hover:bg-white/10 px-3 rounded-md duration-200 text-sm mr-4 py-2 flex items-center">
<FontAwesomeIcon icon={faBook} className="text-xl mr-2" />
Docs
<FontAwesomeIcon icon={faUpRightFromSquare} className="text-xs mb-[0.1rem] mr-5 ml-1.5" />
</a>
<Menu as="div" className="relative inline-block text-left">
<div className="mr-4">

View File

@@ -1,147 +0,0 @@
import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm';
import login1 from '~/pages/api/auth/Login1';
import login2 from '~/pages/api/auth/Login2';
import getOrganizations from '~/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects';
import pushKeys from './secrets/pushKeys';
import Telemetry from './telemetry/Telemetry';
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
import SecurityClient from './SecurityClient';
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
const jsrp = require('jsrp');
const client = new jsrp.client();
/**
* This function loggs in the user (whether it's right after signup, or a normal login)
* @param {*} email
* @param {*} password
* @param {*} setErrorLogin
* @param {*} router
* @param {*} isSignUp
* @returns
*/
const attemptLogin = async (
email,
password,
setErrorLogin,
router,
isSignUp,
isLogin
) => {
try {
const telemetry = new Telemetry().getInstance();
client.init(
{
username: email,
password: password
},
async () => {
const clientPublicKey = client.getPublicKey();
try {
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
client.setSalt(salt);
client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1
// if everything works, go the main dashboard page.
const { token, publicKey, encryptedPrivateKey, iv, tag } =
await login2(email, clientProof);
SecurityClient.setToken(token);
const privateKey = Aes256Gcm.decrypt({
ciphertext: encryptedPrivateKey,
iv,
tag,
secret: password
.slice(0, 32)
.padStart(
32 + (password.slice(0, 32).length - new Blob([password]).size),
'0'
)
});
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
const userOrgs = await getOrganizations();
const userOrgsData = userOrgs.map((org) => org._id);
let orgToLogin;
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
orgToLogin = localStorage.getItem('orgData.id');
} else {
orgToLogin = userOrgsData[0];
localStorage.setItem('orgData.id', orgToLogin);
}
let orgUserProjects = await getOrganizationUserProjects({
orgId: orgToLogin
});
orgUserProjects = orgUserProjects?.map((project) => project._id);
let projectToLogin;
if (
orgUserProjects.includes(localStorage.getItem('projectData.id'))
) {
projectToLogin = localStorage.getItem('projectData.id');
} else {
try {
projectToLogin = orgUserProjects[0];
localStorage.setItem('projectData.id', projectToLogin);
} catch (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) {
telemetry.identify(email);
telemetry.capture('User Logged In');
}
if (isLogin) {
router.push('/dashboard/');
}
} catch (error) {
setErrorLogin(true);
console.log('Login response not available');
}
}
);
} catch (error) {
console.log('Something went wrong during authentication');
}
return true;
};
export default attemptLogin;

View File

@@ -0,0 +1,217 @@
import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm';
import login1 from '~/pages/api/auth/Login1';
import login2 from '~/pages/api/auth/Login2';
import addSecrets from '~/pages/api/files/AddSecrets';
import getOrganizations from '~/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects';
import getUser from '~/pages/api/user/getUser';
import uploadKeys from '~/pages/api/workspace/uploadKeys';
import { encryptAssymmetric } from './cryptography/crypto';
import encryptSecrets from './secrets/encryptSecrets';
import Telemetry from './telemetry/Telemetry';
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
import SecurityClient from './SecurityClient';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
const crypto = require("crypto");
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
const jsrp = require('jsrp');
const client = new jsrp.client();
/**
* This function logs in the user (whether it's right after signup, or a normal login)
* @param {string} email - email of the user logging in
* @param {string} password - password of the user logging in
* @param {function} setErrorLogin - function that visually dispay an error is something is wrong
* @param {*} router
* @param {boolean} isSignUp - whether this log in is a part of signup
* @param {boolean} isLogin - ?
* @returns
*/
const attemptLogin = async (
email: string,
password: string,
setErrorLogin: (value: boolean) => void,
router: any,
isSignUp: boolean,
isLogin: boolean
) => {
try {
const telemetry = new Telemetry().getInstance();
client.init(
{
username: email,
password: password
},
async () => {
const clientPublicKey = client.getPublicKey();
try {
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
client.setSalt(salt);
client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1
// if everything works, go the main dashboard page.
const { token, publicKey, encryptedPrivateKey, iv, tag } =
await login2(email, clientProof);
SecurityClient.setToken(token);
const privateKey = Aes256Gcm.decrypt({
ciphertext: encryptedPrivateKey,
iv,
tag,
secret: password
.slice(0, 32)
.padStart(
32 + (password.slice(0, 32).length - new Blob([password]).size),
'0'
)
});
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
const userOrgs = await getOrganizations();
const userOrgsData = userOrgs.map((org: { _id: string; }) => org._id);
let orgToLogin;
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
orgToLogin = localStorage.getItem('orgData.id');
} else {
orgToLogin = userOrgsData[0];
localStorage.setItem('orgData.id', orgToLogin);
}
let orgUserProjects = await getOrganizationUserProjects({
orgId: orgToLogin
});
orgUserProjects = orgUserProjects?.map((project: { _id: string; }) => project._id);
let projectToLogin;
if (
orgUserProjects.includes(localStorage.getItem('projectData.id'))
) {
projectToLogin = localStorage.getItem('projectData.id');
} else {
try {
projectToLogin = orgUserProjects[0];
localStorage.setItem('projectData.id', projectToLogin);
} catch (error) {
console.log('ERROR: User likely has no projects. ', error);
}
}
if (email) {
telemetry.identify(email);
telemetry.capture('User Logged In');
}
if (isSignUp) {
const randomBytes = crypto.randomBytes(16).toString("hex");
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
const myUser = await getUser();
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: myUser.publicKey,
privateKey: PRIVATE_KEY,
}) as { ciphertext: string; nonce: string };
await uploadKeys(
projectToLogin,
myUser._id,
ciphertext,
nonce
);
const secretsToBeAdded: SecretDataProps[] = [{
type: "shared",
pos: 0,
key: "DATABASE_URL",
value: "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net",
comment: "This is an example of secret referencing.",
id: ''
}, {
type: "shared",
pos: 1,
key: "DB_USERNAME",
value: "OVERRIDE_THIS",
comment: "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",
id: ''
}, {
type: "personal",
pos: 2,
key: "DB_USERNAME",
value: "user1234",
comment: "",
id: ''
}, {
type: "shared",
pos: 3,
key: "DB_PASSWORD",
value: "OVERRIDE_THIS",
comment: "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",
id: ''
}, {
type: "personal",
pos: 4,
key: "DB_PASSWORD",
value: "example_password",
comment: "",
id: ''
}, {
type: "shared",
pos: 5,
key: "TWILIO_AUTH_TOKEN",
value: "example_twillio_token",
comment: "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",
id: ''
}, {
type: "shared",
pos: 6,
key: "WEBSITE_URL",
value: "http://localhost:3000",
comment: "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",
id: ''
}]
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId: String(localStorage.getItem('projectData.id')), env: 'dev' })
await addSecrets({ secrets: secrets ?? [], env: "dev", workspaceId: String(localStorage.getItem('projectData.id')) });
}
if (isLogin) {
router.push('/dashboard/' + localStorage.getItem('projectData.id'));
}
} catch (error) {
console.log(error)
setErrorLogin(true);
console.log('Login response not available');
}
}
);
} catch (error) {
console.log('Something went wrong during authentication');
}
return true;
};
export default attemptLogin;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,53 @@
// 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();
return;
}
export default downloadYaml;

View File

@@ -0,0 +1,122 @@
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; }) => {
let secrets;
try {
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");
}
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;
});
} catch (error) {
console.log("Error while encrypting secrets");
}
return secrets;
}
export default encryptSecrets;

View File

@@ -1,7 +1,7 @@
import getSecrets from '~/pages/api/files/GetSecrets';
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
import { envMapping } from '../../../public/data/frequentConstants';
import guidGenerator from '../randomId';
const {
decryptAssymmetric,
@@ -10,6 +10,22 @@ const {
const nacl = require('tweetnacl');
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 {
key: string;
value: string;
@@ -18,107 +34,102 @@ interface SecretProps {
id: string;
}
interface Props {
interface FunctionProps {
env: keyof typeof envMapping;
setFileState: any;
setIsKeyAvailable: any;
setData: any;
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 ({
env,
setFileState,
setIsKeyAvailable,
setData,
workspaceId
}: Props) => {
}: FunctionProps) => {
try {
let file;
let encryptedSecrets;
try {
file = await getSecrets(workspaceId, envMapping[env]);
setFileState(file);
encryptedSecrets = await getSecrets(workspaceId, envMapping[env]);
} 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
setIsKeyAvailable(!file.key ? file.secrets.length == 0 : true);
setIsKeyAvailable(!latestKey ? encryptedSecrets.length == 0 : true);
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
const tempFileState: SecretProps[] = [];
if (file.key) {
const tempDecryptedSecrets: SecretProps[] = [];
if (latestKey) {
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: file.key.encryptedKey,
nonce: file.key.nonce,
publicKey: file.key.sender.publicKey,
ciphertext: latestKey.latestKey.encryptedKey,
nonce: latestKey.latestKey.nonce,
publicKey: latestKey.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
file.secrets.map((secretPair: any) => {
// decrypt .env file with symmetric key
// decrypt secret keys, values, and comments
encryptedSecrets.map((secret: EncryptedSecretProps) => {
const plainTextKey = decryptSymmetric({
ciphertext: secretPair.secretKey.ciphertext,
iv: secretPair.secretKey.iv,
tag: secretPair.secretKey.tag,
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
const plainTextValue = decryptSymmetric({
ciphertext: secretPair.secretValue.ciphertext,
iv: secretPair.secretValue.iv,
tag: secretPair.secretValue.tag,
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
let plainTextComment;
if (secretPair.secretComment.ciphertext) {
if (secret.secretCommentCiphertext) {
plainTextComment = decryptSymmetric({
ciphertext: secretPair.secretComment.ciphertext,
iv: secretPair.secretComment.iv,
tag: secretPair.secretComment.tag,
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key
});
} else {
plainTextComment = "";
}
tempFileState.push({
id: secretPair._id,
tempDecryptedSecrets.push({
id: secret._id,
key: plainTextKey,
value: plainTextValue,
type: secretPair.type,
type: secret.type,
comment: plainTextComment
});
});
}
setFileState(tempFileState);
setData(
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) => {
const result = tempDecryptedSecrets.map((secret, index) => {
return {
id: line['id'],
id: secret['id'],
pos: index,
key: line['key'],
value: line['value'],
type: line['type'],
comment: line['comment']
key: secret['key'],
value: secret['value'],
type: secret['type'],
comment: secret['comment']
};
});
setData(result);
return result;
} catch (error) {
console.log('Something went wrong during accessing or decripting secrets.');
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -20,7 +20,6 @@ const getActionData = async ({ actionId }: workspaceProps) => {
}
}
).then(async (res) => {
console.log(188, res)
if (res && res.status == 200) {
return (await res.json()).action;
} else {

View File

@@ -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;

View File

@@ -72,10 +72,8 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
<td>{String(t("common:timestamp"))}</td>
<td>{row.createdAt}</td>
</tr>}
{payloadOpened &&
row.payload?.map((action, index) => {
action.secretVersions.length > 0 &&
<tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
{payloadOpened && row.payload?.map((action, index) => {
return action.secretVersions.length > 0 && <tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
<td></td>
<td className="">{t("activity:event." + action.name)}</td>
<td className="text-primary-300 cursor-pointer hover:text-primary duration-200" onClick={() => toggleSidebar(action._id)}>
@@ -87,7 +85,7 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
{payloadOpened &&
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
<td></td>
<td>{String(t("common:ip-address"))}</td>
<td>{String(t("activity:ip-address"))}</td>
<td>{row.ipAddress}</td>
</tr>}
</>

View File

@@ -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`}>

View File

@@ -5,7 +5,7 @@
*/
module.exports = {
// https://www.i18next.com/overview/configuration-options#logging
debug: process.env.NODE_ENV === "development",
debug: false,
i18n: {
defaultLocale: "en",
locales: ["en", "ko", "fr"],

View File

@@ -4,6 +4,7 @@
"requires": true,
"packages": {
"": {
"name": "frontend",
"dependencies": {
"@emotion/css": "^11.10.0",
"@emotion/server": "^11.10.0",
@@ -4639,9 +4640,9 @@
"dev": true
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"peer": true,
"bin": {
"json5": "lib/cli.js"
@@ -7454,9 +7455,9 @@
}
},
"node_modules/tsconfig-paths/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
@@ -11211,9 +11212,9 @@
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"peer": true
},
"jsonp": {
@@ -13118,9 +13119,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"requires": {
"minimist": "^1.2.0"

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;

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;

View File

@@ -1,19 +1,17 @@
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} env - environment of a project for which a user is trying ot get secrets
* @returns
*/
const getSecrets = async (workspaceId: string, env: string) => {
return SecurityClient.fetchCall(
'/api/v1/secret/' +
workspaceId +
'?' +
'/api/v2/secrets?' +
new URLSearchParams({
environment: env,
channel: 'web'
workspaceId
}),
{
method: 'GET',
@@ -23,7 +21,7 @@ const getSecrets = async (workspaceId: string, env: string) => {
}
).then(async (res) => {
if (res && res.status == 200) {
return await res.json();
return (await res.json()).secrets;
} else {
console.log('Failed to get project secrets');
}

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;

View File

@@ -9,7 +9,6 @@ import {
faArrowLeft,
faCheck,
faClockRotateLeft,
faDownload,
faEye,
faEyeSlash,
faFolderOpen,
@@ -17,31 +16,33 @@ import {
faPlus,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react';
import getProjectSercetSnapshotsCount from 'ee/api/secrets/GetProjectSercetSnapshotsCount';
import performSecretRollback from 'ee/api/secrets/PerformSecretRollback';
import PITRecoverySidebar from 'ee/components/PITRecoverySidebar';
import { Document, YAMLSeq } from 'yaml';
import Button from '~/components/basic/buttons/Button';
import ListBox from '~/components/basic/Listbox';
import BottonRightPopup from '~/components/basic/popups/BottomRightPopup';
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
import DownloadSecretMenu from '~/components/dashboard/DownloadSecretsMenu';
import DropZone from '~/components/dashboard/DropZone';
import KeyPair from '~/components/dashboard/KeyPair';
import SideBar from '~/components/dashboard/SideBar';
import NavHeader from '~/components/navigation/NavHeader';
import encryptSecrets from '~/components/utilities/secrets/encryptSecrets';
import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject';
import pushKeys from '~/components/utilities/secrets/pushKeys';
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
import guidGenerator from '~/utilities/randomId';
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 checkUserAction from '../api/userActions/checkUserAction';
import registerUserAction from '../api/userActions/registerUserAction';
import getWorkspaces from '../api/workspace/getWorkspaces';
const queryString = require("query-string");
interface SecretDataProps {
type: 'personal' | 'shared';
@@ -52,9 +53,18 @@ interface SecretDataProps {
comment: string;
}
interface overrideProps {
id: string;
keyName: string;
value: string;
pos: number;
comment: string;
}
interface SnapshotProps {
id: string;
createdAt: string;
version: number;
secretVersions: {
id: string;
pos: number;
@@ -89,7 +99,7 @@ function findDuplicates(arr: any[]) {
*/
export default function Dashboard() {
const [data, setData] = useState<SecretDataProps[] | null>();
const [fileState, setFileState] = useState<SecretDataProps[]>([]);
const [initialData, setInitialData] = useState<SecretDataProps[]>([]);
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter();
const [workspaceId, setWorkspaceId] = useState('');
@@ -196,11 +206,11 @@ export default function Dashboard() {
const dataToSort = await getSecretsForProject({
env,
setFileState,
setIsKeyAvailable,
setData,
workspaceId: String(router.query.id)
});
setInitialData(dataToSort);
reorderRows(dataToSort);
setSharedToHide(
@@ -236,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
* @param {object} obj
@@ -275,12 +277,12 @@ export default function Dashboard() {
text: `${secretName} has been deleted. Remember to save changes.`,
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
* @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) => {
setButtonReady(true);
@@ -293,7 +295,7 @@ export default function Dashboard() {
setSharedToHide(sharedToHide!.filter(tempId => tempId != sharedVersionOfOverride))
// 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")
};
@@ -313,14 +315,6 @@ export default function Dashboard() {
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) => {
setData((oldData) => {
oldData![pos].comment = value;
@@ -338,10 +332,6 @@ export default function Dashboard() {
modifyKey(value, pos);
}, []);
const listenChangeVisibility = useCallback((value: "shared" | "personal", pos: number) => {
modifyVisibility(value, pos);
}, []);
const listenChangeComment = useCallback((value: string, pos: number) => {
modifyComment(value, pos);
}, []);
@@ -349,22 +339,20 @@ export default function Dashboard() {
/**
* Save the changes of environment variables and push them to the database
*/
const savePush = async (dataToPush?: any[], envToPush?: string) => {
let obj;
const savePush = async (dataToPush?: SecretDataProps[]) => {
let newData: SecretDataProps[] | null | undefined;
// dataToPush is mostly used for rollbacks, otherwise we always take the current state data
if ((dataToPush ?? [])?.length > 0) {
obj = Object.assign(
{},
...dataToPush!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] }))
);
newData = dataToPush;
} else {
// Format the new object with environment variables
obj = Object.assign(
{},
...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] }))
);
newData = data;
}
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
const nameErrors = !Object.keys(obj)
.map((key) => !isNaN(Number(key[0].charAt(0))))
@@ -385,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);
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] })
secrets && await addSecrets({ secrets, env: envMapping[env], workspaceId });
}
if (secretsToBeUpdated.length > 0) {
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated, workspaceId, env: envMapping[env] })
secrets && await updateSecrets({ secrets });
}
// If this user has never saved environment variables before, show them a prompt to read docs
if (!hasUserEverPushed) {
@@ -426,79 +441,7 @@ export default function Dashboard() {
setData(sortedData);
};
// check if there are secrets with an override
const checkOverrides = (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;
};
// This function downloads the secrets as a .env file
const downloadDotEnv = () => {
if (!data) return;
const secrets = 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();
};
// This function downloads the secrets as a .yml file
const downloadYaml = () => {
if (!data) return;
const doc = new Document(new YAMLSeq());
const secrets = checkOverrides(data);
secrets.forEach((secret) => {
const pair = doc.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();
};
const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
deleteRow({ids, secretName});
};
@@ -596,31 +539,29 @@ export default function Dashboard() {
<Button
text={String(t("Rollback to this snapshot"))}
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
setData(
snapshotData.secretVersions
.filter(row => reverseEnvMapping[row.environment] == env)
.map((sv, position) => {
return {
id: sv.id, pos: position, type: sv.type, key: sv.key, value: sv.value, comment: ''
}
})
);
// Rollback each of the environments in the snapshot
// #TODO: clean up other environments
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]
);
const rolledBackSecrets = snapshotData.secretVersions
.filter(row => reverseEnvMapping[row.environment] == env)
.map((sv, position) => {
return {
id: sv.id, pos: position, type: sv.type, key: sv.key, value: sv.value, comment: ''
}
});
setData(rolledBackSecrets);
setSharedToHide(
rolledBackSecrets?.filter(row => (rolledBackSecrets
?.map((item) => item.key)
.filter(
(item, index) =>
index !==
rolledBackSecrets?.map((item) => item.key).indexOf(item)
).includes(row.key) && row.type == 'shared'))?.map((item) => item.id)
)
// Perform the rollback globally
performSecretRollback({ workspaceId, version: snapshotData.version })
setSnapshotData(undefined);
createNotification({
text: `Rollback has been performed successfully.`,
@@ -675,50 +616,7 @@ export default function Dashboard() {
/>
</div>}
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
<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-[20rem] 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}
size="md"
text="Download as .env"
/>
</Menu.Item>
<Menu.Item>
<Button
color="mineshaft"
onButtonPressed={downloadYaml}
size="md"
text="Download as .yml"
/>
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
<DownloadSecretMenu data={data} env={env} />
</div>}
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
<Button
@@ -763,7 +661,7 @@ export default function Dashboard() {
className={`max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
>
<div className="px-1 pt-2 bg-mineshaft-800 rounded-md p-2">
{!snapshotData && data?.filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
{!snapshotData && data?.filter(row => row.key?.toUpperCase().includes(searchKeys.toUpperCase()))
.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => (
<KeyPair
key={keyPair.id}
@@ -831,7 +729,6 @@ export default function Dashboard() {
/>
)}
{
// fileState.message == 'Access needed to pull the latest file' ||
(!isKeyAvailable && (
<>
<FontAwesomeIcon