From 092436b6a3e0eab86a9108b5c9b83f092395b8f7 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Thu, 22 Dec 2022 14:15:12 -0500 Subject: [PATCH 01/42] Finished the first ddraft of the dashboard sidebar --- frontend/components/basic/Toggle.tsx | 28 +++++ frontend/components/basic/buttons/Button.tsx | 2 +- .../dashboard/GenerateSecretMenu.tsx | 93 ++++++++++++++ frontend/components/dashboard/SideBar.tsx | 84 +++++++++++++ frontend/pages/dashboard/[id].js | 115 ++---------------- 5 files changed, 216 insertions(+), 106 deletions(-) create mode 100644 frontend/components/basic/Toggle.tsx create mode 100644 frontend/components/dashboard/GenerateSecretMenu.tsx create mode 100644 frontend/components/dashboard/SideBar.tsx diff --git a/frontend/components/basic/Toggle.tsx b/frontend/components/basic/Toggle.tsx new file mode 100644 index 000000000..c2cf3cb07 --- /dev/null +++ b/frontend/components/basic/Toggle.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { Switch } from "@headlessui/react"; + +/** + * This is a typical 'iPhone' toggle (e.g., user for overriding secrets with personal values) + * @param obj + * @param {boolean} obj.enabled - whether the toggle is turned on or off + * @param {function} obj.setEnabled - change the state of the toggle + * @returns + */ +export default function Toggle ({ enabled, setEnabled }: { enabled: boolean; setEnabled: (value: boolean) => void; }): JSX.Element { + return ( + + Enable notifications + + + ) +} diff --git a/frontend/components/basic/buttons/Button.tsx b/frontend/components/basic/buttons/Button.tsx index 562a82a36..f8bd1941d 100644 --- a/frontend/components/basic/buttons/Button.tsx +++ b/frontend/components/basic/buttons/Button.tsx @@ -80,7 +80,7 @@ export default function Button(props: ButtonProps): JSX.Element { ); const textStyle = classNames( - "relative duration-200", + "relative duration-200 text-center w-full", // Show the loading sign if the loading indicator is on props.loading ? "opacity-0" : "opacity-100", diff --git a/frontend/components/dashboard/GenerateSecretMenu.tsx b/frontend/components/dashboard/GenerateSecretMenu.tsx new file mode 100644 index 000000000..1d71749e5 --- /dev/null +++ b/frontend/components/dashboard/GenerateSecretMenu.tsx @@ -0,0 +1,93 @@ +import { Fragment,useState } from 'react'; +import { faShuffle } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Menu, Transition } from '@headlessui/react'; + + +/** + * This is the menu that is used to (re)generate secrets (currently we only have ranom hex, in future we will have more options) + * @returns the popup-menu for randomly generating secrets + */ +const GenerateSecretMenu = () => { + const [randomStringLength, setRandomStringLength] = useState(32); + + return +
+ +
+ +
+
+
+ + +
{ + if (randomStringLength > 32) { + setRandomStringLength(32); + } else if (randomStringLength < 2) { + setRandomStringLength(2); + } else { + // modifyValue( + // [...Array(randomStringLength)] + // .map(() => Math.floor(Math.random() * 16).toString(16)) + // .join(''), + // keyPair.pos + // ); + } + }} + className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full" + > + +
+

Generate Random Hex

+

digits

+
+
+
+
{ + if (randomStringLength > 1) { + setRandomStringLength(randomStringLength - 1); + } + }} + > + - +
+ + setRandomStringLength(parseInt(e.target.value)) + } + value={randomStringLength} + className="text-center z-20 peer text-sm bg-transparent w-full outline-none" + spellCheck="false" + /> +
{ + if (randomStringLength < 32) { + setRandomStringLength(randomStringLength + 1); + } + }} + > + + +
+
+
+
+
+} + +export default GenerateSecretMenu; diff --git a/frontend/components/dashboard/SideBar.tsx b/frontend/components/dashboard/SideBar.tsx new file mode 100644 index 000000000..11f756ccb --- /dev/null +++ b/frontend/components/dashboard/SideBar.tsx @@ -0,0 +1,84 @@ +import { useState } from 'react'; +import { faX } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import Button from '../basic/buttons/Button'; +import Toggle from '../basic/Toggle'; +import DashboardInputField from './DashboardInputField'; +import GenerateSecretMenu from './GenerateSecretMenu'; + +/** + * @returns the sidebar with 'secret's settings' + */ +const SideBar = () => { + const [overrideEnabled, setOverrideEnabled] = useState(false) + + return
+
+
+

Secret

+ +
+
+

Key

+ {}} + type="varName" + position={1} + value={"KeyKeyKey"} + duplicates={[]} + blurred={false} + /> +
+
+

Value

+ {}} + type="value" + position={1} + value={"ValueValueValue"} + duplicates={[]} + blurred={true} + /> +
+ +
+
+
+
+

Override value with a personal value

+ +
+
+ {}} + type="value" + position={1} + value={"ValueValueValue"} + duplicates={[]} + blurred={true} + /> +
+ +
+
+
+

Comments & notes

+
+ Leave your comment here... +
+
+
+
+
+
+
+}; + +export default SideBar; diff --git a/frontend/pages/dashboard/[id].js b/frontend/pages/dashboard/[id].js index 36f5eb3f2..db1f4a746 100644 --- a/frontend/pages/dashboard/[id].js +++ b/frontend/pages/dashboard/[id].js @@ -1,4 +1,4 @@ -import React, { Fragment, useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import Head from 'next/head'; import Image from 'next/image'; import { useRouter } from 'next/router'; @@ -14,14 +14,10 @@ import { faEyeSlash, faFolderOpen, faMagnifyingGlass, - faPeopleGroup, - faPerson, faPlus, - faShuffle, faX } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Menu, Transition } from '@headlessui/react'; import Button from '~/components/basic/buttons/Button'; import ListBox from '~/components/basic/Listbox'; @@ -29,14 +25,13 @@ import BottonRightPopup from '~/components/basic/popups/BottomRightPopup'; import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; import DashboardInputField from '~/components/dashboard/DashboardInputField'; import DropZone from '~/components/dashboard/DropZone'; +import SideBar from '~/components/dashboard/Sidebar'; import NavHeader from '~/components/navigation/NavHeader'; import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject'; import pushKeys from '~/components/utilities/secrets/pushKeys'; -import pushKeysIntegration from '~/components/utilities/secrets/pushKeysIntegration'; import guidGenerator from '~/utilities/randomId'; import { envMapping } from '../../public/data/frequentConstants'; -import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations'; import getUser from '../api/user/getUser'; import checkUserAction from '../api/userActions/checkUserAction'; import registerUserAction from '../api/userActions/registerUserAction'; @@ -63,7 +58,6 @@ const KeyPair = ({ isBlurred, duplicates }) => { - const [randomStringLength, setRandomStringLength] = useState(32); return (
@@ -90,103 +84,12 @@ const KeyPair = ({ />
- -
- -
- -
-
-
- - -
- modifyVisibility( - keyPair.type == 'personal' ? 'shared' : 'personal', - keyPair.pos - ) - } - className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full" - > - -
- {keyPair.type == 'personal' ? 'Make Shared' : 'Make Personal'} -
-
-
{ - if (randomStringLength > 32) { - setRandomStringLength(32); - } else if (randomStringLength < 2) { - setRandomStringLength(2); - } else { - modifyValue( - [...Array(randomStringLength)] - .map(() => Math.floor(Math.random() * 16).toString(16)) - .join(''), - keyPair.pos - ); - } - }} - className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full" - > - -
-

Generate Random Hex

-

digits

-
-
-
-
{ - if (randomStringLength > 1) { - setRandomStringLength(randomStringLength - 1); - } - }} - > - - -
- - setRandomStringLength(parseInt(e.target.value)) - } - value={randomStringLength} - className="text-center z-20 peer text-sm bg-transparent w-full outline-none" - spellCheck="false" - /> -
{ - if (randomStringLength < 32) { - setRandomStringLength(randomStringLength + 1); - } - }} - > - + -
-
-
-
-
+
+ +
-
+
{data .filter( (keyPair) => @@ -582,6 +590,8 @@ export default function Dashboard() { index !== data?.map((item) => item.key).indexOf(item) )} + toggleSidebar={toggleSidebar} + sidebarSecretNumber={sidebarSecretNumber} /> ))}
@@ -631,6 +641,8 @@ export default function Dashboard() { index !== data?.map((item) => item.key).indexOf(item) )} + toggleSidebar={toggleSidebar} + sidebarSecretNumber={sidebarSecretNumber} /> ))}
From e4e0370dad184a0c92414078b9b11be141cd8169 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 23 Dec 2022 10:06:37 -0500 Subject: [PATCH 03/42] Complete v1 secret versioning and project secret snapshots --- backend/src/helpers/secret.ts | 151 ++++++++++++++++++++++++--- backend/src/models/index.ts | 6 ++ backend/src/models/secret.ts | 6 ++ backend/src/models/secretSnapshot.ts | 109 +++++++++++++++++++ backend/src/models/secretVersion.ts | 75 +++++++++++++ 5 files changed, 332 insertions(+), 15 deletions(-) create mode 100644 backend/src/models/secretSnapshot.ts create mode 100644 backend/src/models/secretVersion.ts diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 042aba4fa..b82b64bfc 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -1,7 +1,11 @@ import * as Sentry from '@sentry/node'; import { Secret, - ISecret + ISecret, + SecretVersion, + ISecretVersion, + SecretSnapshot, + ISecretSnapshot } from '../models'; import { decryptSymmetric } from '../utils/crypto'; import { SECRET_SHARED, SECRET_PERSONAL } from '../variables'; @@ -19,7 +23,7 @@ interface PushSecret { } interface Update { - [index: string]: string; + [index: string]: any; } type DecryptSecretType = 'text' | 'object' | 'expanded'; @@ -61,17 +65,27 @@ const pushSecrets = async ({ }, {}); // handle deleting secrets - const toDelete = oldSecrets.filter( - (s: ISecret) => !(s.secretKeyHash in newSecretsObj) - ); + const toDelete = oldSecrets + .filter( + (s: ISecret) => !(s.secretKeyHash in newSecretsObj) + ) + .map((s) => s._id); if (toDelete.length > 0) { await Secret.deleteMany({ - _id: { $in: toDelete.map((s) => s._id) } + _id: { $in: toDelete } + }, { + rawResult: true + }); + + await SecretVersion.updateMany({ + secret: { $in: toDelete } + }, { + isDeleted: true }); } // handle modifying secrets where type or value changed - const operations = secrets + const toUpdate = secrets .filter((s) => { if (s.hashKey in oldSecretsObj) { if (s.hashValue !== oldSecretsObj[s.hashKey].secretValueHash) { @@ -86,18 +100,22 @@ const pushSecrets = async ({ } return false; - }) + }); + + const operations = toUpdate .map((s) => { const update: Update = { - type: s.type, secretValueCiphertext: s.ciphertextValue, secretValueIV: s.ivValue, secretValueTag: s.tagValue, - secretValueHash: s.hashValue + secretValueHash: s.hashValue, + $inc: { + version: 1 + } }; if (s.type === SECRET_PERSONAL) { - // attach user assocaited with the personal secret + // attach user associated with the personal secret update['user'] = userId; } @@ -111,16 +129,40 @@ const pushSecrets = async ({ } }; }); - const a = await Secret.bulkWrite(operations as any); + await Secret.bulkWrite(operations as any); + await SecretVersion.insertMany( + toUpdate.map(({ + ciphertextKey, + ivKey, + tagKey, + hashKey, + ciphertextValue, + ivValue, + tagValue, + hashValue + }) => ({ + secret: oldSecretsObj[hashKey]._id, + version: oldSecretsObj[hashKey].version + 1, + isDeleted: false, + secretKeyCiphertext: ciphertextKey, + secretKeyIV: ivKey, + secretKeyTag: tagKey, + secretKeyHash: hashKey, + secretValueCiphertext: ciphertextValue, + secretValueIV: ivValue, + secretValueTag: tagValue, + secretValueHash: hashValue + })) + ); // handle adding new secrets const toAdd = secrets.filter((s) => !(s.hashKey in oldSecretsObj)); if (toAdd.length > 0) { // add secrets - await Secret.insertMany( + const newSecrets = await Secret.insertMany( toAdd.map((s, idx) => { - let obj: any = { + const obj: any = { workspace: workspaceId, type: toAdd[idx].type, environment, @@ -141,7 +183,39 @@ const pushSecrets = async ({ return obj; }) ); + + await SecretVersion.insertMany( + newSecrets.map(({ + _id, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + }) => ({ + secret: _id, + version: 1, + isDeleted: false, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + })) + ); } + + await takeSecretSnapshotHelper({ + workspaceId + }); + // TODO: in the future add secret snapshot to capture entire + // state of project at this point in time } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -295,9 +369,56 @@ const decryptSecrets = ({ return content; }; +/** + * Saves a copy of the current state of secrets in workspace with id + * [workspaceId] under a new snapshot with incremented version under the + * secretsnapshots collection. + * @param {Object} obj + * @param {String} obj.workspaceId + */ +const takeSecretSnapshotHelper = async ({ + workspaceId +}: { + workspaceId: string; +}) => { + try { + const secrets = await Secret.find({ + workspace: workspaceId + }); + + const latestSecretSnapshot = await SecretSnapshot.findOne({ + workspace: workspaceId + }).sort({ version: -1 }); + + if (!latestSecretSnapshot) { + // case: no snapshots exist for workspace -> create first snapshot + await new SecretSnapshot({ + workspace: workspaceId, + version: 1, + secrets + }).save(); + + return; + } + + // case: snapshots exist for workspace + await new SecretSnapshot({ + workspace: workspaceId, + version: latestSecretSnapshot.version + 1, + secrets + }).save(); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to take a secret snapshot'); + } +} + export { pushSecrets, pullSecrets, reformatPullSecrets, - decryptSecrets + decryptSecrets, + takeSecretSnapshotHelper }; diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 78c38060b..daab77b2a 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -9,6 +9,8 @@ import Membership, { IMembership } from './membership'; import MembershipOrg, { IMembershipOrg } from './membershipOrg'; import Organization, { IOrganization } from './organization'; import Secret, { ISecret } from './secret'; +import SecretVersion, { ISecretVersion } from './secretVersion'; +import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot'; import ServiceToken, { IServiceToken } from './serviceToken'; import Token, { IToken } from './token'; import User, { IUser } from './user'; @@ -38,6 +40,10 @@ export { IOrganization, Secret, ISecret, + SecretVersion, + ISecretVersion, + SecretSnapshot, + ISecretSnapshot, ServiceToken, IServiceToken, Token, diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index b83ef728d..ee879de30 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -10,6 +10,7 @@ import { export interface ISecret { _id: Types.ObjectId; + version: number; workspace: Types.ObjectId; type: string; user: Types.ObjectId; @@ -26,6 +27,11 @@ export interface ISecret { const secretSchema = new Schema( { + version: { + type: Number, + default: 1, + required: true + }, workspace: { type: Schema.Types.ObjectId, ref: 'Workspace', diff --git a/backend/src/models/secretSnapshot.ts b/backend/src/models/secretSnapshot.ts new file mode 100644 index 000000000..376115308 --- /dev/null +++ b/backend/src/models/secretSnapshot.ts @@ -0,0 +1,109 @@ +import { Schema, model, Types } from 'mongoose'; +import { + SECRET_SHARED, + SECRET_PERSONAL, + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD +} from '../variables'; + +export interface ISecretSnapshot { + workspace: Types.ObjectId; + version: number; + secrets: { + version: number; + workspace: Types.ObjectId; + type: string; + user: Types.ObjectId; + environment: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; + }[] +} + +const secretSnapshotSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + version: { + type: Number, + required: true + }, + secrets: [{ + version: { + type: Number, + default: 1, + required: true + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + type: { + type: String, + enum: [SECRET_SHARED, SECRET_PERSONAL], + required: true + }, + user: { + // user associated with the personal secret + type: Schema.Types.ObjectId, + ref: 'User' + }, + environment: { + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], + required: true + }, + secretKeyCiphertext: { + type: String, + required: true + }, + secretKeyIV: { + type: String, // symmetric + required: true + }, + secretKeyTag: { + type: String, // symmetric + required: true + }, + secretKeyHash: { + type: String, + required: true + }, + secretValueCiphertext: { + type: String, + required: true + }, + secretValueIV: { + type: String, // symmetric + required: true + }, + secretValueTag: { + type: String, // symmetric + required: true + }, + secretValueHash: { + type: String, + required: true + } + }] + }, + { + timestamps: true + } +); + +const SecretSnapshot = model('SecretSnapshot', secretSnapshotSchema); + +export default SecretSnapshot; \ No newline at end of file diff --git a/backend/src/models/secretVersion.ts b/backend/src/models/secretVersion.ts new file mode 100644 index 000000000..97c8ba585 --- /dev/null +++ b/backend/src/models/secretVersion.ts @@ -0,0 +1,75 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface ISecretVersion { + _id: Types.ObjectId; + secret: Types.ObjectId; + version: number; + isDeleted: boolean; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; +} + +const secretVersionSchema = new Schema( + { + secret: { // could be deleted + type: Schema.Types.ObjectId, + ref: 'Secret', + required: true + }, + version: { + type: Number, + default: 1, + required: true + }, + isDeleted: { + type: Boolean, + default: false, + required: true + }, + secretKeyCiphertext: { + type: String, + required: true + }, + secretKeyIV: { + type: String, // symmetric + required: true + }, + secretKeyTag: { + type: String, // symmetric + required: true + }, + secretKeyHash: { + type: String, + required: true + }, + secretValueCiphertext: { + type: String, + required: true + }, + secretValueIV: { + type: String, // symmetric + required: true + }, + secretValueTag: { + type: String, // symmetric + required: true + }, + secretValueHash: { + type: String, + required: true + } + }, + { + timestamps: true + } +); + +const SecretVersion = model('SecretVersion', secretVersionSchema); + +export default SecretVersion; \ No newline at end of file From 71a7497ea7c358c354992605344e6d68a828d9de Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 24 Dec 2022 03:28:58 +0200 Subject: [PATCH 04/42] initial commit --- frontend/components/basic/InputField.tsx | 1 + frontend/components/basic/buttons/Button.tsx | 4 +- frontend/pages/login.tsx | 121 ++--- frontend/pages/signup.tsx | 491 ++++++++++--------- 4 files changed, 317 insertions(+), 300 deletions(-) diff --git a/frontend/components/basic/InputField.tsx b/frontend/components/basic/InputField.tsx index 46b42c15c..08afa975d 100644 --- a/frontend/components/basic/InputField.tsx +++ b/frontend/components/basic/InputField.tsx @@ -96,6 +96,7 @@ const InputField = ( /> {props.label?.includes('Password') && ( - - + {false && ( +
+ + We are experiencing minor technical difficulties. We are working on + solving it right now. Please come back in a few minutes. +
+ )} +
+

+ Need an Infisical account? +

+ + + +
+ ); } diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 8991dc867..d1eacaff2 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -40,8 +40,8 @@ const props = { backgroundColor: '#0d1117', color: 'white', border: '1px solid gray', - textAlign: 'center' - } + textAlign: 'center', + }, } as const; const propsPhone = { inputStyle: { @@ -56,8 +56,8 @@ const propsPhone = { backgroundColor: '#0d1117', color: 'white', border: '1px solid gray', - textAlign: 'center' - } + textAlign: 'center', + }, } as const; export default function SignUp() { @@ -148,7 +148,8 @@ export default function SignUp() { } }; - // Verifies if the imformation that the users entered (name, workspace) is there, and if the password matched the criteria. + // Verifies if the imformation that the users entered (name, workspace) is there, and if the password matched the + // criteria. const signupErrorCheck = async () => { setIsLoading(true); let errorCheck = false; @@ -169,7 +170,7 @@ export default function SignUp() { setPasswordErrorLength, setPasswordErrorNumber, setPasswordErrorLowerCase, - currentErrorCheck: errorCheck + currentErrorCheck: errorCheck, }); if (!errorCheck) { @@ -186,8 +187,8 @@ export default function SignUp() { .slice(0, 32) .padStart( 32 + (password.slice(0, 32).length - new Blob([password]).size), - '0' - ) + '0', + ), }) as { ciphertext: string; iv: string; tag: string }; localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY); @@ -195,7 +196,7 @@ export default function SignUp() { client.init( { username: email, - password: password + password: password, }, async () => { client.createVerifier( @@ -204,14 +205,14 @@ export default function SignUp() { email, firstName, lastName, - organizationName: firstName + "'s organization", + organizationName: firstName + '\'s organization', publicKey: PUBLIC_KEY, ciphertext, iv, tag, salt: result.salt, verifier: result.verifier, - token: verificationToken + token: verificationToken, }); // if everything works, go the main dashboard page. @@ -230,16 +231,16 @@ export default function SignUp() { setErrorLogin, router, true, - false + false, ); incrementStep(); } catch (error) { setIsLoading(false); } } - } + }, ); - } + }, ); } else { setIsLoading(false); @@ -250,7 +251,7 @@ export default function SignUp() { const step1 = (

- {"Let'"}s get started + {'Let\''}s get started

@@ -261,260 +262,267 @@ export default function SignUp() {
-
- -
- {/*
+
{e.preventDefault();}}> +
+ +
+ {/*

I do not want to receive emails about Infisical and its products.

*/} -
-

- By creating an account, you agree to our Terms and have read and - acknowledged the Privacy Policy. -

-
-
-
+
); // Step 2 of the signup process (enter the email verification code) const step2 = ( -
-

- {"We've"} sent a verification email to{' '} -

-

- {email}{' '} -

-
- -
-
- -
- {codeError && ( - - )} -
-
-
- {/* +
{e.preventDefault();}}> +
+

+ {'We\'ve'} sent a verification email to{' '} +

+

+ {email}{' '} +

+
+ +
+
+ +
+ {codeError && ( + + )} +
+
+
+ {/* */} -

- Make sure to check your spam inbox. -

+

+ Make sure to check your spam inbox. +

+
-
+ ); // Step 3 of the signup process (enter the rest of the impformation) const step3 = ( -
-

- Almost there! -

-
- -
-
- -
-
- { - setPassword(password); - passwordCheck({ - password, - setPasswordErrorLength, - setPasswordErrorNumber, - setPasswordErrorLowerCase, - currentErrorCheck: false - }); - }} - type="password" - value={password} - isRequired - error={ - passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase - } - autoComplete="new-password" - id="new-password" - /> - {passwordErrorLength || - passwordErrorLowerCase || - passwordErrorNumber ? ( -
-
- Password should contain at least: -
-
- {passwordErrorLength ? ( - - ) : ( - - )} -
- 14 characters +
{e.preventDefault();}}> +
+

+ Almost there! +

+
+ +
+
+ +
+
+ { + setPassword(password); + passwordCheck({ + password, + setPasswordErrorLength, + setPasswordErrorNumber, + setPasswordErrorLowerCase, + currentErrorCheck: false, + }); + }} + type="password" + value={password} + isRequired + error={ + passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase + } + autoComplete="new-password" + id="new-password" + /> + {passwordErrorLength || + passwordErrorLowerCase || + passwordErrorNumber ? ( +
+
+ Password should contain at least: +
+
+ {passwordErrorLength ? ( + + ) : ( + + )} +
+ 14 characters +
+
+
+ {passwordErrorLowerCase ? ( + + ) : ( + + )} +
+ 1 lowercase character +
+
+
+ {passwordErrorNumber ? ( + + ) : ( + + )} +
+ 1 number +
-
- {passwordErrorLowerCase ? ( - - ) : ( - - )} -
- 1 lowercase character -
-
-
- {passwordErrorNumber ? ( - - ) : ( - - )} -
- 1 number -
-
-
- ) : ( -
- )} + ) : ( +
+ )} +
+
+
-
-
-
+ ); // Step 4 of the sign up process (download the emergency kit pdf) const step4 = ( -
-

- Save your Emergency Kit -

-
-
- If you get locked out of your account, your Emergency Kit is the only - way to sign in. +
{e.preventDefault();}}> +
+

+ Save your Emergency Kit +

+
+
+ If you get locked out of your account, your Emergency Kit is the only + way to sign in. +
+
+ We recommend you download it and keep it somewhere safe. +
-
- We recommend you download it and keep it somewhere safe. +
+ + It contains your Secret Key which we cannot access or recover for you if + you lose it.
-
-
- - It contains your Secret Key which we cannot access or recover for you if - you lose it. -
-
-
-
+ ); return ( From d25f4ccc89695ca331c3ab64dc0179d32791f861 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 24 Dec 2022 03:33:43 +0200 Subject: [PATCH 05/42] wip --- frontend/pages/login.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index ef9b7a7d6..25fc85a9f 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -38,6 +38,10 @@ export default function Login() { * This function check if the user entered the correct credentials and should be allowed to log in. */ const loginCheck = async () => { + if (!email || !password) { + return; + } + setIsLoading(true); await attemptLogin( email, @@ -75,9 +79,11 @@ export default function Login() { />
-
{ - e.preventDefault() - }}> + setErrorLogin(false)} onSubmit={(e) => { + e.preventDefault(); + }} + >

Log in to your account From 13e78833731b284dd343a65875f544f541dc408d Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 24 Dec 2022 03:34:27 +0200 Subject: [PATCH 06/42] wip --- frontend/pages/login.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index 25fc85a9f..5346b77fa 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -114,7 +114,7 @@ export default function Login() { Forgot password?

- {errorLogin && } + {!isLoading && errorLogin && }
- {e.preventDefault();}}>
-
); // Step 2 of the signup process (enter the email verification code) const step2 = ( -
{e.preventDefault();}}>

{'We\'ve'} sent a verification email to{' '} @@ -344,12 +341,10 @@ export default function SignUp() {

- ); // Step 3 of the signup process (enter the rest of the impformation) const step3 = ( -
{e.preventDefault();}}>

Almost there! @@ -481,12 +476,10 @@ export default function SignUp() { />

- ); // Step 4 of the sign up process (download the emergency kit pdf) const step4 = ( -
{e.preventDefault();}}>

Save your Emergency Kit @@ -536,7 +529,6 @@ export default function SignUp() {

*/}
- ); return ( @@ -565,7 +557,9 @@ export default function SignUp() { /> - {step == 1 ? step1 : step == 2 ? step2 : step == 3 ? step3 : step4} +
{e.preventDefault();}}> + {step == 1 ? step1 : step == 2 ? step2 : step == 3 ? step3 : step4} +
); From 417eddaeff8b3efbb5875ee48447dd54c3797b83 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 24 Dec 2022 03:43:03 +0200 Subject: [PATCH 08/42] cleanup --- frontend/pages/login.tsx | 4 +--- frontend/pages/signup.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index 5346b77fa..036902de0 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -80,9 +80,7 @@ export default function Login() {
setErrorLogin(false)} onSubmit={(e) => { - e.preventDefault(); - }} + onChange={() => setErrorLogin(false)} onSubmit={(e) => e.preventDefault()} >

diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 8ca92aea7..a6743f457 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -557,7 +557,7 @@ export default function SignUp() { />

- {e.preventDefault();}}> + e.preventDefault()}> {step == 1 ? step1 : step == 2 ? step2 : step == 3 ? step3 : step4}
From 205bf70861e7f4311ef761caba076ca0f6108a14 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Fri, 23 Dec 2022 23:00:26 -0500 Subject: [PATCH 09/42] Added overrides for secrets --- frontend/components/basic/Toggle.tsx | 31 +++- .../dashboard/DashboardInputField.tsx | 12 +- frontend/components/dashboard/SideBar.tsx | 90 ++++++++-- frontend/pages/dashboard/[id].js | 167 +++++++----------- 4 files changed, 183 insertions(+), 117 deletions(-) diff --git a/frontend/components/basic/Toggle.tsx b/frontend/components/basic/Toggle.tsx index c2cf3cb07..6a32d588a 100644 --- a/frontend/components/basic/Toggle.tsx +++ b/frontend/components/basic/Toggle.tsx @@ -1,6 +1,25 @@ import React from "react"; import { Switch } from "@headlessui/react"; + +interface OverrideProps { + id: string; + keyName: string; + value: string; + pos: number; +} + +interface ToggleProps { + enabled: boolean; + setEnabled: (value: boolean) => void; + addOverride: (value: OverrideProps) => void; + keyName: string; + value: string; + pos: number; + id: string; + deleteOverride: (id: string) => void; +} + /** * This is a typical 'iPhone' toggle (e.g., user for overriding secrets with personal values) * @param obj @@ -8,11 +27,19 @@ import { Switch } from "@headlessui/react"; * @param {function} obj.setEnabled - change the state of the toggle * @returns */ -export default function Toggle ({ enabled, setEnabled }: { enabled: boolean; setEnabled: (value: boolean) => void; }): JSX.Element { +export default function Toggle ({ enabled, setEnabled, addOverride, keyName, value, pos, id, deleteOverride }: ToggleProps): JSX.Element { + console.log(755, pos, enabled) return ( { + if (enabled == false) { + addOverride({ id, keyName, value, pos }); + } else { + deleteOverride(id); + } + setEnabled(!enabled); + }} className={`${ enabled ? 'bg-primary' : 'bg-bunker-400' } relative inline-flex h-5 w-9 items-center rounded-full`} diff --git a/frontend/components/dashboard/DashboardInputField.tsx b/frontend/components/dashboard/DashboardInputField.tsx index cb75dcf8c..3a5cd80ee 100644 --- a/frontend/components/dashboard/DashboardInputField.tsx +++ b/frontend/components/dashboard/DashboardInputField.tsx @@ -13,6 +13,7 @@ interface DashboardInputFieldProps { type: 'varName' | 'value'; blurred: boolean; duplicates: string[]; + override?: boolean; } /** @@ -33,7 +34,8 @@ const DashboardInputField = ({ type, value, blurred, - duplicates + duplicates, + override }: DashboardInputFieldProps) => { const ref = useRef(null); const syncScroll = (e: SyntheticEvent) => { @@ -85,6 +87,7 @@ const DashboardInputField = ({
+ {override == true &&
Override enabled
} onChangeHandler(e.target.value, position)} @@ -99,10 +102,13 @@ const DashboardInputField = ({
{value.split(REGEX).map((word, id) => { if (word.match(REGEX) !== null) { diff --git a/frontend/components/dashboard/SideBar.tsx b/frontend/components/dashboard/SideBar.tsx index 5e77e7d75..38f575c6b 100644 --- a/frontend/components/dashboard/SideBar.tsx +++ b/frontend/components/dashboard/SideBar.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { faX } from '@fortawesome/free-solid-svg-icons'; +import { faBackward, faDotCircle, faRotateLeft, faX } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Button from '../basic/buttons/Button'; @@ -13,7 +13,14 @@ interface SecretProps { key: string; value: string; pos: number; - visibility: string; + id: string; +} + +interface OverrideProps { + id: string; + keyName: string; + value: string; + pos: number; } interface SideBarProps { @@ -22,6 +29,8 @@ interface SideBarProps { modifyKey: (value: string) => void; modifyValue: (value: string) => void; modifyVisibility: (value: string) => void; + addOverride: (value: OverrideProps) => void; + deleteOverride: (id: string) => void; } /** @@ -33,7 +42,7 @@ interface SideBarProps { * @param {function} obj.modifyVisibility - function that modifies the secret visibility * @returns the sidebar with 'secret's settings' */ -const SideBar = ({ toggleSidebar, data, modifyKey, modifyValue, modifyVisibility }: SideBarProps) => { +const SideBar = ({ toggleSidebar, data, modifyKey, modifyValue, modifyVisibility, addOverride, deleteOverride }: SideBarProps) => { const [overrideEnabled, setOverrideEnabled] = useState(false); return
@@ -44,7 +53,7 @@ const SideBar = ({ toggleSidebar, data, modifyKey, modifyValue, modifyVisibility
-
+

Key

-
+

Override value with a personal value

- +
@@ -97,9 +115,57 @@ const SideBar = ({ toggleSidebar, data, modifyKey, modifyValue, modifyVisibility isFull={true} />
-
-

Comments & notes

-
+
+

Version History

+
+
+
+
+
+
+
+
+
Current
+

Key:{data[0].key}

+

Value:{data[0].value}

+

Visibility:{'shared'}

+
+
+
+
+
+
+
+
+
12/22/2022 12:36 EST
+
Key: KeyKeyKey
+
Value: ValueValueValue
+

Visibility:{'shared'}

+
+
+
+
+
+
+
+
+
12/21/2022 09:11 EST
+
Key: KeyKey
+
Value: ValueValue
+

Visibility:{'shared'}

+
+
+
+
+
+
+
+

Comments & notes

+
+

Coming soon!

+
+
+
Leave your comment here...
diff --git a/frontend/pages/dashboard/[id].js b/frontend/pages/dashboard/[id].js index ef4db42bd..11519578e 100644 --- a/frontend/pages/dashboard/[id].js +++ b/frontend/pages/dashboard/[id].js @@ -6,7 +6,6 @@ import { faArrowDownAZ, faArrowDownZA, faCheck, - faCircleInfo, faCopy, faDownload, faEllipsis, @@ -83,6 +82,7 @@ const KeyPair = ({ position={keyPair.pos} value={keyPair.value} blurred={isBlurred} + override={keyPair.value == "user1234" && true} />
@@ -176,7 +176,7 @@ export default function Dashboard() { prevSort == 'alphabetical' ? '-alphabetical' : 'alphabetical' ); - sortValuesHandler(dataToReorder); + sortValuesHandler(dataToReorder, ""); }; useEffect(() => { @@ -238,11 +238,43 @@ export default function Dashboard() { ]); }; + /** + * This function add an ovverrided version of a certain secret to the current user + * @param {object} obj + * @param {string} obj.id - if of this secret that is about to be overriden + * @param {string} obj.keyName - key name of this secret + * @param {string} obj.value - value of this secret + * @param {string} obj.pos - position of this secret on the dashboard + */ + const addOverride = ({ id, keyName, value, pos }) => { + setIsNew(false); + const tempdata = [ + ...data, + { + id: id, + pos: pos, + key: keyName, + value: value, + type: 'personal' + } + ]; + sortValuesHandler(tempdata, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical"); + }; + const deleteRow = (id) => { setButtonReady(true); setData(data.filter((row) => row.id !== id)); }; + /** + * This function deleted the override of a certain secrer + * @param {string} id - id of a secret to be deleted + */ + const deleteOverride = (id) => { + setButtonReady(true); + setData(data.filter((row) => !(row.id == id && row.type == 'personal'))); + }; + const modifyValue = (value, pos) => { setData((oldData) => { oldData[pos].value = value; @@ -335,10 +367,11 @@ export default function Dashboard() { setBlurred(!blurred); }; - const sortValuesHandler = (dataToSort) => { + const sortValuesHandler = (dataToSort, specificSortMethod) => { + const howToSort = specificSortMethod != "" ? specificSortMethod : sortMethod const sortedData = (dataToSort != 1 ? dataToSort : data) .sort((a, b) => - sortMethod == 'alphabetical' + howToSort == 'alphabetical' ? a.key.localeCompare(b.key) : b.key.localeCompare(a.key) ) @@ -397,12 +430,14 @@ export default function Dashboard() { />
- {sidebarSecretNumber != -1 && row.pos == sidebarSecretNumber)} modifyKey={listenChangeKey} modifyValue={listenChangeValue} modifyVisibility={listenChangeVisibility} + addOverride={addOverride} + deleteOverride={deleteOverride} />}
@@ -550,101 +585,33 @@ export default function Dashboard() {
-
- {/* */} -
-

Personal

-
- - - Personal keys are only visible to you - -
-
-
-
- {data - .filter( - (keyPair) => - keyPair.key - .toLowerCase() - .includes(searchKeys.toLowerCase()) && - keyPair.type == 'personal' - ) - ?.map((keyPair) => ( - item.key) - .filter( - (item, index) => - index !== - data?.map((item) => item.key).indexOf(item) - )} - toggleSidebar={toggleSidebar} - sidebarSecretNumber={sidebarSecretNumber} - /> - ))} -
-
-
8 ? 'h-3/4' : 'h-min' - }`} - > -
- {/* */} -
-

Shared

-
- - - Shared keys are visible to your whole team - -
-
-
-
- {data - .filter( - (keyPair) => - keyPair.key - .toLowerCase() - .includes(searchKeys.toLowerCase()) && - keyPair.type == 'shared' - ) - ?.map((keyPair) => ( - item.key) - .filter( - (item, index) => - index !== - data?.map((item) => item.key).indexOf(item) - )} - toggleSidebar={toggleSidebar} - sidebarSecretNumber={sidebarSecretNumber} - /> - ))} +
+ {data?.filter(row => !(data + ?.map((item) => item.key) + .filter( + (item, index) => + index !== + data?.map((item) => item.key).indexOf(item) + ).includes(row.key) && row.type == 'shared')).map((keyPair) => ( + item.key) + .filter( + (item, index) => + index !== + data?.map((item) => item.key).indexOf(item) + )} + toggleSidebar={toggleSidebar} + sidebarSecretNumber={sidebarSecretNumber} + /> + ))}
From dca3bd4fbb0fbd089891ce3869838e664611fd92 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 23 Dec 2022 10:06:37 -0500 Subject: [PATCH 10/42] Complete v1 secret versioning and project secret snapshots --- backend/src/helpers/secret.ts | 151 ++++++++++++++++++++++++--- backend/src/models/index.ts | 6 ++ backend/src/models/secret.ts | 6 ++ backend/src/models/secretSnapshot.ts | 109 +++++++++++++++++++ backend/src/models/secretVersion.ts | 75 +++++++++++++ 5 files changed, 332 insertions(+), 15 deletions(-) create mode 100644 backend/src/models/secretSnapshot.ts create mode 100644 backend/src/models/secretVersion.ts diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 042aba4fa..b82b64bfc 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -1,7 +1,11 @@ import * as Sentry from '@sentry/node'; import { Secret, - ISecret + ISecret, + SecretVersion, + ISecretVersion, + SecretSnapshot, + ISecretSnapshot } from '../models'; import { decryptSymmetric } from '../utils/crypto'; import { SECRET_SHARED, SECRET_PERSONAL } from '../variables'; @@ -19,7 +23,7 @@ interface PushSecret { } interface Update { - [index: string]: string; + [index: string]: any; } type DecryptSecretType = 'text' | 'object' | 'expanded'; @@ -61,17 +65,27 @@ const pushSecrets = async ({ }, {}); // handle deleting secrets - const toDelete = oldSecrets.filter( - (s: ISecret) => !(s.secretKeyHash in newSecretsObj) - ); + const toDelete = oldSecrets + .filter( + (s: ISecret) => !(s.secretKeyHash in newSecretsObj) + ) + .map((s) => s._id); if (toDelete.length > 0) { await Secret.deleteMany({ - _id: { $in: toDelete.map((s) => s._id) } + _id: { $in: toDelete } + }, { + rawResult: true + }); + + await SecretVersion.updateMany({ + secret: { $in: toDelete } + }, { + isDeleted: true }); } // handle modifying secrets where type or value changed - const operations = secrets + const toUpdate = secrets .filter((s) => { if (s.hashKey in oldSecretsObj) { if (s.hashValue !== oldSecretsObj[s.hashKey].secretValueHash) { @@ -86,18 +100,22 @@ const pushSecrets = async ({ } return false; - }) + }); + + const operations = toUpdate .map((s) => { const update: Update = { - type: s.type, secretValueCiphertext: s.ciphertextValue, secretValueIV: s.ivValue, secretValueTag: s.tagValue, - secretValueHash: s.hashValue + secretValueHash: s.hashValue, + $inc: { + version: 1 + } }; if (s.type === SECRET_PERSONAL) { - // attach user assocaited with the personal secret + // attach user associated with the personal secret update['user'] = userId; } @@ -111,16 +129,40 @@ const pushSecrets = async ({ } }; }); - const a = await Secret.bulkWrite(operations as any); + await Secret.bulkWrite(operations as any); + await SecretVersion.insertMany( + toUpdate.map(({ + ciphertextKey, + ivKey, + tagKey, + hashKey, + ciphertextValue, + ivValue, + tagValue, + hashValue + }) => ({ + secret: oldSecretsObj[hashKey]._id, + version: oldSecretsObj[hashKey].version + 1, + isDeleted: false, + secretKeyCiphertext: ciphertextKey, + secretKeyIV: ivKey, + secretKeyTag: tagKey, + secretKeyHash: hashKey, + secretValueCiphertext: ciphertextValue, + secretValueIV: ivValue, + secretValueTag: tagValue, + secretValueHash: hashValue + })) + ); // handle adding new secrets const toAdd = secrets.filter((s) => !(s.hashKey in oldSecretsObj)); if (toAdd.length > 0) { // add secrets - await Secret.insertMany( + const newSecrets = await Secret.insertMany( toAdd.map((s, idx) => { - let obj: any = { + const obj: any = { workspace: workspaceId, type: toAdd[idx].type, environment, @@ -141,7 +183,39 @@ const pushSecrets = async ({ return obj; }) ); + + await SecretVersion.insertMany( + newSecrets.map(({ + _id, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + }) => ({ + secret: _id, + version: 1, + isDeleted: false, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + })) + ); } + + await takeSecretSnapshotHelper({ + workspaceId + }); + // TODO: in the future add secret snapshot to capture entire + // state of project at this point in time } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -295,9 +369,56 @@ const decryptSecrets = ({ return content; }; +/** + * Saves a copy of the current state of secrets in workspace with id + * [workspaceId] under a new snapshot with incremented version under the + * secretsnapshots collection. + * @param {Object} obj + * @param {String} obj.workspaceId + */ +const takeSecretSnapshotHelper = async ({ + workspaceId +}: { + workspaceId: string; +}) => { + try { + const secrets = await Secret.find({ + workspace: workspaceId + }); + + const latestSecretSnapshot = await SecretSnapshot.findOne({ + workspace: workspaceId + }).sort({ version: -1 }); + + if (!latestSecretSnapshot) { + // case: no snapshots exist for workspace -> create first snapshot + await new SecretSnapshot({ + workspace: workspaceId, + version: 1, + secrets + }).save(); + + return; + } + + // case: snapshots exist for workspace + await new SecretSnapshot({ + workspace: workspaceId, + version: latestSecretSnapshot.version + 1, + secrets + }).save(); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to take a secret snapshot'); + } +} + export { pushSecrets, pullSecrets, reformatPullSecrets, - decryptSecrets + decryptSecrets, + takeSecretSnapshotHelper }; diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 9b07f6766..f43e4309f 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -7,6 +7,8 @@ import Membership, { IMembership } from './membership'; import MembershipOrg, { IMembershipOrg } from './membershipOrg'; import Organization, { IOrganization } from './organization'; import Secret, { ISecret } from './secret'; +import SecretVersion, { ISecretVersion } from './secretVersion'; +import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot'; import ServiceToken, { IServiceToken } from './serviceToken'; import Token, { IToken } from './token'; import User, { IUser } from './user'; @@ -32,6 +34,10 @@ export { IOrganization, Secret, ISecret, + SecretVersion, + ISecretVersion, + SecretSnapshot, + ISecretSnapshot, ServiceToken, IServiceToken, Token, diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index b83ef728d..ee879de30 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -10,6 +10,7 @@ import { export interface ISecret { _id: Types.ObjectId; + version: number; workspace: Types.ObjectId; type: string; user: Types.ObjectId; @@ -26,6 +27,11 @@ export interface ISecret { const secretSchema = new Schema( { + version: { + type: Number, + default: 1, + required: true + }, workspace: { type: Schema.Types.ObjectId, ref: 'Workspace', diff --git a/backend/src/models/secretSnapshot.ts b/backend/src/models/secretSnapshot.ts new file mode 100644 index 000000000..376115308 --- /dev/null +++ b/backend/src/models/secretSnapshot.ts @@ -0,0 +1,109 @@ +import { Schema, model, Types } from 'mongoose'; +import { + SECRET_SHARED, + SECRET_PERSONAL, + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD +} from '../variables'; + +export interface ISecretSnapshot { + workspace: Types.ObjectId; + version: number; + secrets: { + version: number; + workspace: Types.ObjectId; + type: string; + user: Types.ObjectId; + environment: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; + }[] +} + +const secretSnapshotSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + version: { + type: Number, + required: true + }, + secrets: [{ + version: { + type: Number, + default: 1, + required: true + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + type: { + type: String, + enum: [SECRET_SHARED, SECRET_PERSONAL], + required: true + }, + user: { + // user associated with the personal secret + type: Schema.Types.ObjectId, + ref: 'User' + }, + environment: { + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], + required: true + }, + secretKeyCiphertext: { + type: String, + required: true + }, + secretKeyIV: { + type: String, // symmetric + required: true + }, + secretKeyTag: { + type: String, // symmetric + required: true + }, + secretKeyHash: { + type: String, + required: true + }, + secretValueCiphertext: { + type: String, + required: true + }, + secretValueIV: { + type: String, // symmetric + required: true + }, + secretValueTag: { + type: String, // symmetric + required: true + }, + secretValueHash: { + type: String, + required: true + } + }] + }, + { + timestamps: true + } +); + +const SecretSnapshot = model('SecretSnapshot', secretSnapshotSchema); + +export default SecretSnapshot; \ No newline at end of file diff --git a/backend/src/models/secretVersion.ts b/backend/src/models/secretVersion.ts new file mode 100644 index 000000000..97c8ba585 --- /dev/null +++ b/backend/src/models/secretVersion.ts @@ -0,0 +1,75 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface ISecretVersion { + _id: Types.ObjectId; + secret: Types.ObjectId; + version: number; + isDeleted: boolean; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; +} + +const secretVersionSchema = new Schema( + { + secret: { // could be deleted + type: Schema.Types.ObjectId, + ref: 'Secret', + required: true + }, + version: { + type: Number, + default: 1, + required: true + }, + isDeleted: { + type: Boolean, + default: false, + required: true + }, + secretKeyCiphertext: { + type: String, + required: true + }, + secretKeyIV: { + type: String, // symmetric + required: true + }, + secretKeyTag: { + type: String, // symmetric + required: true + }, + secretKeyHash: { + type: String, + required: true + }, + secretValueCiphertext: { + type: String, + required: true + }, + secretValueIV: { + type: String, // symmetric + required: true + }, + secretValueTag: { + type: String, // symmetric + required: true + }, + secretValueHash: { + type: String, + required: true + } + }, + { + timestamps: true + } +); + +const SecretVersion = model('SecretVersion', secretVersionSchema); + +export default SecretVersion; \ No newline at end of file From c4ebea74224b9388cfe5f536a2c8e840b1e479b7 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 23 Dec 2022 17:47:42 -0500 Subject: [PATCH 11/42] Finish get secret versions route --- backend/src/controllers/secretController.ts | 37 ++++++++++++++++++--- backend/src/routes/secret.ts | 14 ++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/backend/src/controllers/secretController.ts b/backend/src/controllers/secretController.ts index d1cf5f65d..350375e28 100644 --- a/backend/src/controllers/secretController.ts +++ b/backend/src/controllers/secretController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { Key } from '../models'; +import { Key, Secret, SecretVersion } from '../models'; import { pushSecrets as push, pullSecrets as pull, @@ -160,9 +160,6 @@ export const pullSecrets = async (req: Request, res: Response) => { * @returns */ export const pullSecretsServiceToken = async (req: Request, res: Response) => { - // get (encrypted) secrets from workspace with id [workspaceId] - // service token route - let secrets; let key; try { @@ -217,3 +214,35 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => { key }); }; + +/** + * Return secret versions for secret with id [secretId] + * @param req + * @param res + */ +export const getSecretVersions = async (req: Request, res: Response) => { + let secretVersions; + try { + const { secretId } = req.params; + + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); + + secretVersions = await SecretVersion.find({ + secret: secretId + }) + .skip(offset) + .limit(limit); + + } catch (err) { + Sentry.setUser({ email: req.serviceToken.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get secret versions' + }); + } + + return res.status(200).send({ + secretVersions + }); +} \ No newline at end of file diff --git a/backend/src/routes/secret.ts b/backend/src/routes/secret.ts index 98b3009de..073384129 100644 --- a/backend/src/routes/secret.ts +++ b/backend/src/routes/secret.ts @@ -50,4 +50,18 @@ router.get( secretController.pullSecretsServiceToken ); +router.get( + '/:secretId/secret-versions', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('secretId').exists().trim(), + query('offset').exists().isInt(), + query('limit').exists().isInt(), + validateRequest, + secretController.getSecretVersions +); + export default router; From 9c769853b4a002b72b18b40e2cbfd3a9afda2114 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sat, 24 Dec 2022 20:01:33 -0500 Subject: [PATCH 12/42] Patch secret-override mechanism with versioning/snapshots --- backend/src/helpers/secret.ts | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index b82b64bfc..5c8edddf7 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -57,17 +57,17 @@ const pushSecrets = async ({ workspaceId, environment }); - const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => { - return { ...accumulator, [s.secretKeyHash]: s }; - }, {}); - const newSecretsObj = secrets.reduce((accumulator, s) => { - return { ...accumulator, [s.hashKey]: s }; - }, {}); + const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => + ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) + , {}); + const newSecretsObj = secrets.reduce((accumulator, s) => + ({ ...accumulator, [`${s.type}-${s.hashKey}`]: s }) + , {}); // handle deleting secrets const toDelete = oldSecrets .filter( - (s: ISecret) => !(s.secretKeyHash in newSecretsObj) + (s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj) ) .map((s) => s._id); if (toDelete.length > 0) { @@ -87,16 +87,11 @@ const pushSecrets = async ({ // handle modifying secrets where type or value changed const toUpdate = secrets .filter((s) => { - if (s.hashKey in oldSecretsObj) { - if (s.hashValue !== oldSecretsObj[s.hashKey].secretValueHash) { + if (`${s.type}-${s.hashKey}` in oldSecretsObj) { + if (s.hashValue !== oldSecretsObj[`${s.type}-${s.hashKey}`].secretValueHash) { // case: filter secrets where value changed return true; } - - if (s.type !== oldSecretsObj[s.hashKey].type) { - // case: filter secrets where type changed - return true; - } } return false; @@ -122,8 +117,7 @@ const pushSecrets = async ({ return { updateOne: { filter: { - workspace: workspaceId, - _id: oldSecretsObj[s.hashKey]._id + _id: oldSecretsObj[`${s.type}-${s.hashKey}`]._id }, update } @@ -132,6 +126,7 @@ const pushSecrets = async ({ await Secret.bulkWrite(operations as any); await SecretVersion.insertMany( toUpdate.map(({ + type, ciphertextKey, ivKey, tagKey, @@ -141,8 +136,8 @@ const pushSecrets = async ({ tagValue, hashValue }) => ({ - secret: oldSecretsObj[hashKey]._id, - version: oldSecretsObj[hashKey].version + 1, + secret: oldSecretsObj[`${type}-${hashKey}`]._id, + version: oldSecretsObj[`${type}-${hashKey}`].version + 1, isDeleted: false, secretKeyCiphertext: ciphertextKey, secretKeyIV: ivKey, @@ -156,7 +151,7 @@ const pushSecrets = async ({ ); // handle adding new secrets - const toAdd = secrets.filter((s) => !(s.hashKey in oldSecretsObj)); + const toAdd = secrets.filter((s) => !(`${s.type}-${s.hashKey}` in oldSecretsObj)); if (toAdd.length > 0) { // add secrets @@ -214,8 +209,6 @@ const pushSecrets = async ({ await takeSecretSnapshotHelper({ workspaceId }); - // TODO: in the future add secret snapshot to capture entire - // state of project at this point in time } catch (err) { Sentry.setUser(null); Sentry.captureException(err); From 7f51aaf451b75554eabae5f507306f4e7844c3b5 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 25 Dec 2022 00:04:44 -0500 Subject: [PATCH 13/42] Add vault docs --- docs/cli/commands/commands.mdx | 2 +- docs/cli/commands/login.mdx | 6 +-- docs/cli/commands/vault.mdx | 52 ++++++++++++++++++++++++ docs/cli/usage.mdx | 19 ++++++++- docs/getting-started/dashboard/token.mdx | 6 +++ docs/mint.json | 3 +- 6 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 docs/cli/commands/vault.mdx diff --git a/docs/cli/commands/commands.mdx b/docs/cli/commands/commands.mdx index acefdfa7e..7c1deeb1b 100644 --- a/docs/cli/commands/commands.mdx +++ b/docs/cli/commands/commands.mdx @@ -9,7 +9,7 @@ title: "Commands" | `login` | Used to authenticate and set the logged in user. | | `init` | Used to link a local project to the platform. | | `run` | Used to inject envars from the platform into an application process. | - +| `vault` | Used to manage where your login credentials are stored at rest | ## Global options | Option | Description | diff --git a/docs/cli/commands/login.mdx b/docs/cli/commands/login.mdx index 32c1bedba..de004c97f 100644 --- a/docs/cli/commands/login.mdx +++ b/docs/cli/commands/login.mdx @@ -7,7 +7,5 @@ infisical login ``` ## Description - -Verify a user and save credentials to the system keyring. - -To change the logged in user, run the command again to overwrite the previous login. +The CLI uses authentication to verify your identity. When you enter the correct email and password for your account, a token is generated and saved in your system Keyring to allow you to make future interactions with the CLI. +If you want to change where the login credentials are stored, visit the [vaults command](./vault) \ No newline at end of file diff --git a/docs/cli/commands/vault.mdx b/docs/cli/commands/vault.mdx new file mode 100644 index 000000000..7ffec4e91 --- /dev/null +++ b/docs/cli/commands/vault.mdx @@ -0,0 +1,52 @@ +--- +title: "infisical vault" +--- + + + + ```bash + infisical vault + + # Example output + The following vaults are available on your system: + - keychain + - pass + - file + + You are currently using [keychain] vault to store your login credentials + ``` + + + + ```bash + infisical vault set + + # Example + infisical vault set keychain + ``` + + + + +## Description + +To ensure secure storage of your login credentials when using the CLI, Infisical saves them to a password manager if one is detected. +If a password manager is not available, your credentials are stored in an encrypted text file. + + + + By default, the most appropriate password manager is chosen to store your login credentials. + For example, if you are on macOS, KeyChain will be automatically selected. + +- [macOS Keychain](https://support.apple.com/en-au/guide/keychain-access/welcome/mac) +- [Windows Credential Manager](https://support.microsoft.com/en-au/help/4026814/windows-accessing-credential-manager) +- Secret Service ([Gnome Keyring](https://wiki.gnome.org/Projects/GnomeKeyring), [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5)) +- [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5) +- [Pass](https://www.passwordstore.org/) +- [KeyCtl]() +- Encrypted file (JWT) + + +To avoid constantly entering your passphrase when using the `file` vault type, set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable with your password in your shell + + diff --git a/docs/cli/usage.mdx b/docs/cli/usage.mdx index 4237f8154..aac5c1b74 100644 --- a/docs/cli/usage.mdx +++ b/docs/cli/usage.mdx @@ -4,10 +4,27 @@ title: "Usage" Prerequisite: [Install the CLI](/cli/overview) +## Authenticate + + + To use the Infisical CLI in your development environment, you can run the command below. + This will allow you to access the features and functionality provided by the CLI. + + ```bash + infisical login + ``` + + + + To use Infisical CLI in environments where you cannot run the `infisical login` command, you can authenticate via a + Infisical Token instead. Learn more about [Infisical Token](../getting-started/dashboard/token). + + + ## Initialize Infisical for your project ```bash -# move to your project +# navigate to your project cd /path/to/project # initialize infisical diff --git a/docs/getting-started/dashboard/token.mdx b/docs/getting-started/dashboard/token.mdx index 9b3ddf79f..455a57929 100644 --- a/docs/getting-started/dashboard/token.mdx +++ b/docs/getting-started/dashboard/token.mdx @@ -11,6 +11,12 @@ To generate the the token, head over to your project settings as shown below. ![token add](../../images/project-token-add.png) +## Feeding Infisical Token to the CLI + +The Infisical CLI checks for the presence of an environment variable called `INFISICAL_TOKEN`. +If it detects this variable in the terminal where it is being run, it will use it to authenticate and retrieve the environment variables that the token is authorized to access. +This allows you to use the CLI in environments where you are unable to run the `infisical login` command. + The token grants read-only access to a particular environment and project for a specified amount of time. Once the token is expired, the CLI using it will no longer be able to make diff --git a/docs/mint.json b/docs/mint.json index c94b9131a..57beea626 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -94,7 +94,8 @@ "cli/commands/login", "cli/commands/init", "cli/commands/run", - "cli/commands/export" + "cli/commands/export", + "cli/commands/vault" ] } ] From d89af29070589eec8425998b6e847322b68eff3d Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Sun, 25 Dec 2022 00:33:37 -0500 Subject: [PATCH 14/42] Refactored dashboard to TS - still some bugs and inefficiencies --- frontend/components/basic/Toggle.tsx | 1 - frontend/components/basic/buttons/Button.tsx | 2 +- ...ttomRightPopup.js => BottomRightPopup.tsx} | 22 +- .../dashboard/DashboardInputField.tsx | 4 +- .../dashboard/GenerateSecretMenu.tsx | 14 +- frontend/components/dashboard/SideBar.tsx | 60 +++-- .../utilities/secrets/getSecretsForProject.ts | 4 +- .../components/utilities/secrets/pushKeys.ts | 6 +- .../pages/dashboard/{[id].js => [id].tsx} | 249 ++++++++++-------- 9 files changed, 210 insertions(+), 152 deletions(-) rename frontend/components/basic/popups/{BottomRightPopup.js => BottomRightPopup.tsx} (71%) rename frontend/pages/dashboard/{[id].js => [id].tsx} (76%) diff --git a/frontend/components/basic/Toggle.tsx b/frontend/components/basic/Toggle.tsx index 6a32d588a..7702bb29e 100644 --- a/frontend/components/basic/Toggle.tsx +++ b/frontend/components/basic/Toggle.tsx @@ -28,7 +28,6 @@ interface ToggleProps { * @returns */ export default function Toggle ({ enabled, setEnabled, addOverride, keyName, value, pos, id, deleteOverride }: ToggleProps): JSX.Element { - console.log(755, pos, enabled) return ( void; +} + /** * This is the notification that pops up at the bottom right when a user performs a certain action * @param {object} org @@ -23,16 +33,16 @@ export default function BottonRightPopup({ textLine1, textLine2, setCheckDocsPopUpVisible, -}) { +}: PopupProps): JSX.Element { return (