diff --git a/frontend/components/utilities/attemptLogin.js b/frontend/components/utilities/attemptLogin.ts
similarity index 68%
rename from frontend/components/utilities/attemptLogin.js
rename to frontend/components/utilities/attemptLogin.ts
index db4725c0d..d729987e7 100644
--- a/frontend/components/utilities/attemptLogin.js
+++ b/frontend/components/utilities/attemptLogin.ts
@@ -1,35 +1,46 @@
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 pushKeys from './secrets/pushKeys';
+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 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
+ * 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 {*} isSignUp
+ * @param {boolean} isSignUp - whether this log in is a part of signup
+ * @param {boolean} isLogin - ?
* @returns
*/
const attemptLogin = async (
- email,
- password,
- setErrorLogin,
- router,
- isSignUp,
- isLogin
+ email: string,
+ password: string,
+ setErrorLogin: (value: boolean) => void,
+ router: any,
+ isSignUp: boolean,
+ isLogin: boolean
) => {
try {
const telemetry = new Telemetry().getInstance();
@@ -76,7 +87,7 @@ const attemptLogin = async (
});
const userOrgs = await getOrganizations();
- const userOrgsData = userOrgs.map((org) => org._id);
+ const userOrgsData = userOrgs.map((org: { _id: string; }) => org._id);
let orgToLogin;
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
@@ -90,7 +101,7 @@ const attemptLogin = async (
orgId: orgToLogin
});
- orgUserProjects = orgUserProjects?.map((project) => project._id);
+ orgUserProjects = orgUserProjects?.map((project: { _id: string; }) => project._id);
let projectToLogin;
if (
orgUserProjects.includes(localStorage.getItem('projectData.id'))
@@ -104,26 +115,7 @@ const attemptLogin = async (
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');
@@ -133,6 +125,7 @@ const attemptLogin = async (
router.push('/dashboard/');
}
} catch (error) {
+ console.log(error)
setErrorLogin(true);
console.log('Login response not available');
}
diff --git a/frontend/components/utilities/secrets/checkOverrides.ts b/frontend/components/utilities/secrets/checkOverrides.ts
new file mode 100644
index 000000000..fae465d88
--- /dev/null
+++ b/frontend/components/utilities/secrets/checkOverrides.ts
@@ -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;
diff --git a/frontend/components/utilities/secrets/downloadDotEnv.ts b/frontend/components/utilities/secrets/downloadDotEnv.ts
new file mode 100644
index 000000000..dbb29497b
--- /dev/null
+++ b/frontend/components/utilities/secrets/downloadDotEnv.ts
@@ -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;
diff --git a/frontend/components/utilities/secrets/downloadYaml.ts b/frontend/components/utilities/secrets/downloadYaml.ts
new file mode 100644
index 000000000..66a679bbc
--- /dev/null
+++ b/frontend/components/utilities/secrets/downloadYaml.ts
@@ -0,0 +1,52 @@
+import YAML from 'yaml';
+import { YAMLSeq } from 'yaml/types'
+
+import { envMapping } from "../../../public/data/frequentConstants";
+import checkOverrides from './checkOverrides';
+
+
+interface SecretDataProps {
+ type: 'personal' | 'shared';
+ pos: number;
+ key: string;
+ value: string;
+ id: string;
+ comment: string;
+}
+
+/**
+ * This function downloads the secrets as a .yml file
+ * @param {object} obj
+ * @param {SecretDataProps[]} obj.data - secrets that we want to download
+ * @param {string} obj.env - used for naming the file
+ * @returns
+ */
+const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
+ if (!data) return;
+ const doc = new YAML.Document();
+ doc.contents = new YAMLSeq()
+ const secrets = await checkOverrides({ data });
+ secrets.forEach((secret) => {
+ const pair = YAML.createNode({ [secret.key]: secret.value });
+ pair.commentBefore = secret.comment
+ .split('\n')
+ .map((line) => (line ? ' '.concat(line) : ''))
+ .join('\n');
+ doc.add(pair);
+ });
+
+ const file = doc
+ .toString()
+ .split('\n')
+ .map((line) => (line.startsWith('-') ? line.replace('- ', '') : line))
+ .join('\n');
+
+ const blob = new Blob([file]);
+ const fileDownloadUrl = URL.createObjectURL(blob);
+ const alink = document.createElement('a');
+ alink.href = fileDownloadUrl;
+ alink.download = envMapping[env] + '.yml';
+ alink.click();
+}
+
+export default downloadYaml;
diff --git a/frontend/components/utilities/secrets/encryptSecrets.ts b/frontend/components/utilities/secrets/encryptSecrets.ts
new file mode 100644
index 000000000..87c47d29a
--- /dev/null
+++ b/frontend/components/utilities/secrets/encryptSecrets.ts
@@ -0,0 +1,116 @@
+import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
+
+const crypto = require("crypto");
+const {
+ decryptAssymmetric,
+ encryptSymmetric,
+} = require("../cryptography/crypto");
+const nacl = require("tweetnacl");
+nacl.util = require("tweetnacl-util");
+
+
+interface SecretDataProps {
+ type: 'personal' | 'shared';
+ pos: number;
+ key: string;
+ value: string;
+ id: string;
+ comment: string;
+}
+
+interface EncryptedSecretProps {
+ id: string;
+ createdAt: string;
+ environment: string;
+ secretCommentCiphertext: string;
+ secretCommentIV: string;
+ secretCommentTag: string;
+ secretKeyCiphertext: string;
+ secretKeyIV: string;
+ secretKeyTag: string;
+ secretValueCiphertext: string;
+ secretValueIV: string;
+ secretValueTag: string;
+ type: "personal" | "shared";
+}
+
+/**
+ * Encypt secrets before pushing the to the DB
+ * @param {object} obj
+ * @param {object} obj.secretsToEncrypt - secrets that we want to encrypt
+ * @param {object} obj.workspaceId - the id of a project in which we are encrypting secrets
+ * @returns
+ */
+const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsToEncrypt: SecretDataProps[]; workspaceId: string; env: string; }) => {
+ const sharedKey = await getLatestFileKey({ workspaceId });
+
+ const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
+
+ let randomBytes: string;
+ if (Object.keys(sharedKey).length > 0) {
+ // case: a (shared) key exists for the workspace
+ randomBytes = decryptAssymmetric({
+ ciphertext: sharedKey.latestKey.encryptedKey,
+ nonce: sharedKey.latestKey.nonce,
+ publicKey: sharedKey.latestKey.sender.publicKey,
+ privateKey: PRIVATE_KEY,
+ });
+ } else {
+ // case: a (shared) key does not exist for the workspace
+ randomBytes = crypto.randomBytes(16).toString("hex");
+ }
+
+ const secrets = secretsToEncrypt.map((secret) => {
+ // encrypt key
+ const {
+ ciphertext: secretKeyCiphertext,
+ iv: secretKeyIV,
+ tag: secretKeyTag,
+ } = encryptSymmetric({
+ plaintext: secret.key,
+ key: randomBytes,
+ });
+
+ // encrypt value
+ const {
+ ciphertext: secretValueCiphertext,
+ iv: secretValueIV,
+ tag: secretValueTag,
+ } = encryptSymmetric({
+ plaintext: secret.value,
+ key: randomBytes,
+ });
+
+ // encrypt comment
+ const {
+ ciphertext: secretCommentCiphertext,
+ iv: secretCommentIV,
+ tag: secretCommentTag,
+ } = encryptSymmetric({
+ plaintext: secret.comment ?? '',
+ key: randomBytes,
+ });
+
+ const result: EncryptedSecretProps = {
+ id: secret.id,
+ createdAt: '',
+ environment: env,
+ secretKeyCiphertext,
+ secretKeyIV,
+ secretKeyTag,
+ secretValueCiphertext,
+ secretValueIV,
+ secretValueTag,
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag,
+ type: secret.type,
+ };
+
+ return result;
+ });
+
+ return secrets;
+}
+
+export default encryptSecrets;
diff --git a/frontend/components/utilities/secrets/getSecretsForProject.ts b/frontend/components/utilities/secrets/getSecretsForProject.ts
index 57f0e4a16..81de27be0 100644
--- a/frontend/components/utilities/secrets/getSecretsForProject.ts
+++ b/frontend/components/utilities/secrets/getSecretsForProject.ts
@@ -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.');
}
diff --git a/frontend/components/utilities/secrets/pushKeys.ts b/frontend/components/utilities/secrets/pushKeys.ts
deleted file mode 100644
index 1bedf357c..000000000
--- a/frontend/components/utilities/secrets/pushKeys.ts
+++ /dev/null
@@ -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;
diff --git a/frontend/components/utilities/secrets/pushKeysIntegration.ts b/frontend/components/utilities/secrets/pushKeysIntegration.ts
deleted file mode 100644
index 949778927..000000000
--- a/frontend/components/utilities/secrets/pushKeysIntegration.ts
+++ /dev/null
@@ -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
;
- 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;
diff --git a/frontend/ee/api/secrets/GetActionData.ts b/frontend/ee/api/secrets/GetActionData.ts
index 122870d69..4fdf18436 100644
--- a/frontend/ee/api/secrets/GetActionData.ts
+++ b/frontend/ee/api/secrets/GetActionData.ts
@@ -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 {
diff --git a/frontend/ee/api/secrets/PerformSecretRollback.ts b/frontend/ee/api/secrets/PerformSecretRollback.ts
new file mode 100644
index 000000000..6b51410a2
--- /dev/null
+++ b/frontend/ee/api/secrets/PerformSecretRollback.ts
@@ -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;
diff --git a/frontend/ee/components/PITRecoverySidebar.tsx b/frontend/ee/components/PITRecoverySidebar.tsx
index cefe2e2d1..085c483ed 100644
--- a/frontend/ee/components/PITRecoverySidebar.tsx
+++ b/frontend/ee/components/PITRecoverySidebar.tsx
@@ -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
diff --git a/frontend/pages/api/files/AddSecrets.ts b/frontend/pages/api/files/AddSecrets.ts
new file mode 100644
index 000000000..cdee8e7ba
--- /dev/null
+++ b/frontend/pages/api/files/AddSecrets.ts
@@ -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;
diff --git a/frontend/pages/api/files/DeleteSecrets.ts b/frontend/pages/api/files/DeleteSecrets.ts
new file mode 100644
index 000000000..cc178b7bb
--- /dev/null
+++ b/frontend/pages/api/files/DeleteSecrets.ts
@@ -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;
diff --git a/frontend/pages/api/files/GetSecrets.ts b/frontend/pages/api/files/GetSecrets.ts
index 8ff22b57c..2fed6a400 100644
--- a/frontend/pages/api/files/GetSecrets.ts
+++ b/frontend/pages/api/files/GetSecrets.ts
@@ -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');
}
diff --git a/frontend/pages/api/files/UpdateSecrets.ts b/frontend/pages/api/files/UpdateSecrets.ts
new file mode 100644
index 000000000..2ec67fc2e
--- /dev/null
+++ b/frontend/pages/api/files/UpdateSecrets.ts
@@ -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;
diff --git a/frontend/pages/dashboard/[id].tsx b/frontend/pages/dashboard/[id].tsx
index e56436b47..5eaf54678 100644
--- a/frontend/pages/dashboard/[id].tsx
+++ b/frontend/pages/dashboard/[id].tsx
@@ -9,7 +9,6 @@ import {
faArrowLeft,
faCheck,
faClockRotateLeft,
- faDownload,
faEye,
faEyeSlash,
faFolderOpen,
@@ -18,28 +17,32 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import getProjectSercetSnapshotsCount from 'ee/api/secrets/GetProjectSercetSnapshotsCount';
+import performSecretRollback from 'ee/api/secrets/PerformSecretRollback';
import PITRecoverySidebar from 'ee/components/PITRecoverySidebar';
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';
@@ -50,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;
@@ -87,7 +99,7 @@ function findDuplicates(arr: any[]) {
*/
export default function Dashboard() {
const [data, setData] = useState();
- const [fileState, setFileState] = useState([]);
+ const [initialData, setInitialData] = useState([]);
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter();
const [workspaceId, setWorkspaceId] = useState('');
@@ -194,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(
@@ -234,14 +246,6 @@ export default function Dashboard() {
]);
};
- interface overrideProps {
- id: string;
- keyName: string;
- value: string;
- pos: number;
- comment: string;
- }
-
/**
* This function add an ovverrided version of a certain secret to the current user
* @param {object} obj
@@ -273,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);
@@ -291,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")
};
@@ -311,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;
@@ -336,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);
}, []);
@@ -347,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))))
@@ -383,10 +373,37 @@ export default function Dashboard() {
});
}
- // Once "Save changed is clicked", disable that button
+ // Once "Save changes" is clicked, disable that button
setButtonReady(false);
- console.log(envToPush ? envToPush : env, env, envToPush)
- pushKeys({ obj, workspaceId: String(router.query.id), env: envToPush ? envToPush : env });
+
+ const secretsToBeDeleted
+ = initialData
+ .filter(initDataPoint => !newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id))
+ .map(secret => secret.id);
+
+ const secretsToBeAdded
+ = newData!
+ .filter(newDataPoint => !initialData.map(initDataPoint => initDataPoint.id).includes(newDataPoint.id));
+
+ const secretsToBeUpdated
+ = newData!.filter(newDataPoint => initialData
+ .filter(initDataPoint => newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id)
+ && (newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].value != initDataPoint.value
+ || newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].key != initDataPoint.key
+ || newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].comment != initDataPoint.comment))
+ .map(secret => secret.id).includes(newDataPoint.id));
+
+ if (secretsToBeDeleted.length > 0) {
+ await deleteSecrets({ secretIds: secretsToBeDeleted });
+ }
+ if (secretsToBeAdded.length > 0) {
+ const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId, env: envMapping[env] })
+ await addSecrets({ secrets, env: envMapping[env], workspaceId });
+ }
+ if (secretsToBeUpdated.length > 0) {
+ const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated, workspaceId, env: envMapping[env] })
+ await updateSecrets({ secrets });
+ }
// If this user has never saved environment variables before, show them a prompt to read docs
if (!hasUserEverPushed) {
@@ -425,19 +442,6 @@ export default function Dashboard() {
setData(sortedData);
};
- // This function downloads the secrets as a .env file
- const download = () => {
- const file = data!
- .map((item: SecretDataProps) => [item.key, item.value].join('='))
- .join('\n');
- const blob = new Blob([file]);
- const fileDownloadUrl = URL.createObjectURL(blob);
- const alink = document.createElement('a');
- alink.href = fileDownloadUrl;
- alink.download = envMapping[env] + '.env';
- alink.click();
- };
-
const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
deleteRow({ids, secretName});
};
@@ -535,8 +539,6 @@ export default function Dashboard() {
{
- 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
@@ -548,18 +550,9 @@ export default function Dashboard() {
})
);
- // 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]
- );
- });
+ // Perform the rollback globally
+ performSecretRollback({ workspaceId, version: snapshotData.version })
+
setSnapshotData(undefined);
createNotification({
text: `Rollback has been performed successfully.`,
@@ -614,12 +607,7 @@ export default function Dashboard() {
/>
}
{!snapshotData &&
-
+
}
item.key + item.type))?.includes(keyPair.key + keyPair.type)}
+ isDuplicate={findDuplicates(
+ data?.map((item) => item.key + item.type)
+ )?.includes(keyPair.key + keyPair.type)}
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={false}
@@ -694,7 +684,9 @@ export default function Dashboard() {
modifyValue={listenChangeValue}
modifyKey={listenChangeKey}
isBlurred={blurred}
- isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)}
+ isDuplicate={findDuplicates(
+ data?.map((item) => item.key + item.type)
+ )?.includes(keyPair.key + keyPair.type)}
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={true}
@@ -728,7 +720,6 @@ export default function Dashboard() {
/>
)}
{
- // fileState.message == 'Access needed to pull the latest file' ||
(!isKeyAvailable && (
<>