This commit is contained in:
Tuan Dang
2022-11-26 16:56:39 -05:00
25 changed files with 290 additions and 105 deletions

View File

@@ -71,7 +71,7 @@ Not sure where to get started? [Book a free, non-pressure pairing sessions with
- [GitHub Discussions](https://github.com/Infisical/infisical/discussions) for help with building and discussion.
- [GitHub Issues](https://github.com/Infisical/infisical-cli/issues) for any bugs and errors you encounter using Infisical.
- [Community Slack](https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA) for hanging out with the community and quick communication with the team.
- [Community Slack](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g) for hanging out with the community and quick communication with the team.
## Status

View File

@@ -33,6 +33,13 @@ var runCmd = &cobra.Command{
return
}
substitute, err := cmd.Flags().GetBool("substitute")
if err != nil {
log.Errorln("Unable to parse the substitute flag")
log.Debugln(err)
return
}
projectId, err := cmd.Flags().GetString("projectId")
if err != nil {
log.Errorln("Unable to parse the project id flag")
@@ -82,7 +89,13 @@ var runCmd = &cobra.Command{
}
}
execCmd(args[0], args[1:], envsFromApi)
if substitute {
substitutions := util.SubstituteSecrets(envsFromApi)
execCmd(args[0], args[1:], substitutions)
} else {
execCmd(args[0], args[1:], envsFromApi)
}
},
}
@@ -90,6 +103,7 @@ func init() {
rootCmd.AddCommand(runCmd)
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from")
runCmd.Flags().Bool("substitute", true, "Parse shell variable substitutions in your secrets")
}
// Credit: inspired by AWS Valut

View File

@@ -0,0 +1,14 @@
package models
import log "github.com/sirupsen/logrus"
// Custom error type so that we can give helpful messages in CLI
type Error struct {
Err error
DebugMessage string
FriendlyMessage string
}
func (e *Error) printFriendlyMessage() {
log.Infoln(e.FriendlyMessage)
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"regexp"
"strings"
"github.com/Infisical/infisical-merge/packages/models"
@@ -205,3 +206,73 @@ func GetWorkSpacesFromAPI(userCreds models.UserCredentials) (workspaces []models
return getWorkSpacesResponse.Workspaces, nil
}
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string {
if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found {
return value
}
for _, secret := range secrets {
if secret.Key == variableWeAreLookingFor {
regex := regexp.MustCompile(`\${([^\}]*)}`)
variablesToPopulate := regex.FindAllString(secret.Value, -1)
// case: variable is a constant so return its value
if len(variablesToPopulate) == 0 {
return secret.Value
}
valueToEdit := secret.Value
for _, variableWithSign := range variablesToPopulate {
variableWithoutSign := strings.Trim(variableWithSign, "}")
variableWithoutSign = strings.Trim(variableWithoutSign, "${")
// case: reference to self
if variableWithoutSign == secret.Key {
hashMapOfSelfRefs[variableWithoutSign] = variableWithoutSign
continue
} else {
var expandedVariableValue string
if preComputedVariable, found := hashMapOfCompleteVariables[variableWithoutSign]; found {
expandedVariableValue = preComputedVariable
} else {
expandedVariableValue = getExpandedEnvVariable(secrets, variableWithoutSign, hashMapOfCompleteVariables, hashMapOfSelfRefs)
hashMapOfCompleteVariables[variableWithoutSign] = expandedVariableValue
}
// If after expanding all the vars above, is the current var a self ref? if so no replacement needed for it
if _, found := hashMapOfSelfRefs[variableWithoutSign]; found {
continue
} else {
valueToEdit = strings.ReplaceAll(valueToEdit, variableWithSign, expandedVariableValue)
}
}
}
return valueToEdit
} else {
continue
}
}
return "${" + variableWeAreLookingFor + "}"
}
func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable {
hashMapOfCompleteVariables := make(map[string]string)
hashMapOfSelfRefs := make(map[string]string)
expandedSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range secrets {
expandedVariable := getExpandedEnvVariable(secrets, secret.Key, hashMapOfCompleteVariables, hashMapOfSelfRefs)
expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{
Key: secret.Key,
Value: expandedVariable,
})
}
return expandedSecrets
}

View File

@@ -0,0 +1,160 @@
package util
import (
"testing"
"github.com/Infisical/infisical-merge/packages/models"
)
// References to self should return the value unaltered
func Test_SubstituteSecrets_When_ReferenceToSelf(t *testing.T) {
var tests = []struct {
Key string
Value string
ExpectedValue string
}{
{Key: "A", Value: "${A}", ExpectedValue: "${A}"},
{Key: "A", Value: "${A} ${A}", ExpectedValue: "${A} ${A}"},
{Key: "A", Value: "${A}${A}", ExpectedValue: "${A}${A}"},
}
for _, test := range tests {
secret := models.SingleEnvironmentVariable{
Key: test.Key,
Value: test.Value,
}
secrets := []models.SingleEnvironmentVariable{secret}
result := SubstituteSecrets(secrets)
if result[0].Value != test.ExpectedValue {
t.Errorf("Test_SubstituteSecrets_When_ReferenceToSelf: expected %s but got %s for input %s", test.ExpectedValue, result[0].Value, test.Value)
}
}
}
func Test_SubstituteSecrets_When_ReferenceDoesNotExist(t *testing.T) {
var tests = []struct {
Key string
Value string
ExpectedValue string
}{
{Key: "A", Value: "${X}", ExpectedValue: "${X}"},
{Key: "A", Value: "${H}HELLO", ExpectedValue: "${H}HELLO"},
{Key: "A", Value: "${L}${S}", ExpectedValue: "${L}${S}"},
}
for _, test := range tests {
secret := models.SingleEnvironmentVariable{
Key: test.Key,
Value: test.Value,
}
secrets := []models.SingleEnvironmentVariable{secret}
result := SubstituteSecrets(secrets)
if result[0].Value != test.ExpectedValue {
t.Errorf("Test_SubstituteSecrets_When_ReferenceToSelf: expected %s but got %s for input %s", test.ExpectedValue, result[0].Value, test.Value)
}
}
}
func Test_SubstituteSecrets_When_ReferenceDoesNotExist_And_Self_Referencing(t *testing.T) {
tests := []struct {
Key string
Value string
ExpectedValue string
}{
{
Key: "O",
Value: "${P} ==$$ ${X} ${UNKNOWN} ${A}",
ExpectedValue: "DOMAIN === ${A} DOMAIN >>> ==$$ DOMAIN ${UNKNOWN} ${A}",
},
{
Key: "X",
Value: "DOMAIN",
ExpectedValue: "DOMAIN",
},
{
Key: "A",
Value: "*${A}* ${X}",
ExpectedValue: "*${A}* DOMAIN",
},
{
Key: "H",
Value: "${X} >>>",
ExpectedValue: "DOMAIN >>>",
},
{
Key: "P",
Value: "DOMAIN === ${A} ${H}",
ExpectedValue: "DOMAIN === ${A} DOMAIN >>>",
},
{
Key: "T",
Value: "${P} ==$$ ${X} ${UNKNOWN} ${A} ${P} ==$$ ${X} ${UNKNOWN} ${A}",
ExpectedValue: "DOMAIN === ${A} DOMAIN >>> ==$$ DOMAIN ${UNKNOWN} ${A} DOMAIN === ${A} DOMAIN >>> ==$$ DOMAIN ${UNKNOWN} ${A}",
},
{
Key: "S",
Value: "${ SSS$$ ${HEY}",
ExpectedValue: "${ SSS$$ ${HEY}",
},
}
secrets := []models.SingleEnvironmentVariable{}
for _, test := range tests {
secrets = append(secrets, models.SingleEnvironmentVariable{Key: test.Key, Value: test.Value})
}
results := SubstituteSecrets(secrets)
for index, expanded := range results {
if expanded.Value != tests[index].ExpectedValue {
t.Errorf("Test_SubstituteSecrets_When_ReferenceToSelf: expected [%s] but got [%s] for input [%s]", tests[index].ExpectedValue, expanded.Value, tests[index].Value)
}
}
}
func Test_SubstituteSecrets_When_No_SubstituteNeeded(t *testing.T) {
tests := []struct {
Key string
Value string
ExpectedValue string
}{
{
Key: "DOMAIN",
Value: "infisical.com",
ExpectedValue: "infisical.com",
},
{
Key: "API_KEY",
Value: "hdgsvjshcgkdckhevdkd",
ExpectedValue: "hdgsvjshcgkdckhevdkd",
},
{
Key: "ENV",
Value: "PROD",
ExpectedValue: "PROD",
},
}
secrets := []models.SingleEnvironmentVariable{}
for _, test := range tests {
secrets = append(secrets, models.SingleEnvironmentVariable{Key: test.Key, Value: test.Value})
}
results := SubstituteSecrets(secrets)
for index, expanded := range results {
if expanded.Value != tests[index].ExpectedValue {
t.Errorf("Test_SubstituteSecrets_When_ReferenceToSelf: expected [%s] but got [%s] for input [%s]", tests[index].ExpectedValue, expanded.Value, tests[index].Value)
}
}
}

View File

@@ -8,7 +8,7 @@ import nacl from "tweetnacl";
import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import { decryptAssymmetric, encryptAssymmetric } from "../../utilities/crypto";
import { decryptAssymmetric, encryptAssymmetric } from "../../utilities/cryptography/crypto";
import Button from "../buttons/Button";
import InputField from "../InputField";
import ListBox from "../Listbox";

View File

@@ -20,7 +20,7 @@ import createWorkspace from "~/pages/api/workspace/createWorkspace";
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
import NavBarDashboard from "../navigation/NavBarDashboard";
import { decryptAssymmetric, encryptAssymmetric } from "../utilities/crypto";
import { decryptAssymmetric, encryptAssymmetric } from "../utilities/cryptography/crypto";
import Button from "./buttons/Button";
import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog";
import Listbox from "./Listbox";

View File

@@ -15,7 +15,7 @@ import Listbox from "../Listbox";
const {
decryptAssymmetric,
encryptAssymmetric,
} = require("../../utilities/crypto");
} = require("../../utilities/cryptography/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");

View File

@@ -1,75 +0,0 @@
import React from "react";
import { faCcMastercard, faCcVisa } from "@fortawesome/free-brands-svg-icons";
import { faCheck, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { faCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export default function Card({ card, changeSelectedCard, selected }) {
function creditCardBrandIcon(cc) {
if (cc == "visa") {
return faCcVisa;
} else if ((cc = "mastercard")) {
return faCcMastercard;
} else return faQuestionCircle;
}
return (
<button onClick={() => changeSelectedCard(card.id)}>
<div
className={`flex flex-col p-3 items-start justify-between mr-2 w-52 h-28 bg-primary/5 rounded-lg duration-200 ${
card.id == selected
? "border-primary text-primary"
: "border-gray-500 text-gray-300"
} hover:border-primary border-2 hover:text-primary cursor-pointer`}
>
<div className="flex flex-row items-center justify-between w-full">
<FontAwesomeIcon
className="text-3xl mr-4"
icon={creditCardBrandIcon(card.card.brand)}
/>
{card.id == selected && (
<FontAwesomeIcon
className="text-xl ml-2 mr-2"
icon={faCheck}
/>
)}
</div>
<div className="flex flex-row items-center justify-between w-full">
<div className="flex flex-row items-center">
<p className="tracking-tighter mr-1 mt-0.5 flex">
{"****".split("").map((_, index) => (
<FontAwesomeIcon
key={index}
className="text-xxxs mr-0.5"
icon={faCircle}
/>
))}
</p>
<p className="tracking-tighter mr-1 mt-0.5 flex">
{"****".split("").map((_, index) => (
<FontAwesomeIcon
key={index}
className="text-xxxs mr-0.5"
icon={faCircle}
/>
))}
</p>
<p className="tracking-tighter mr-1 mt-0.5 flex">
{"****".split("").map((_, index) => (
<FontAwesomeIcon
key={index}
className="text-xxxs mr-0.5"
icon={faCircle}
/>
))}
</p>
<p className="text-xs">{card.card.last4}</p>
</div>
<p className="text-xs">
{card.card.exp_month + "/" + card.card.exp_year}
</p>
</div>
</div>
</button>
);
}

View File

@@ -27,8 +27,8 @@ import guidGenerator from "../utilities/randomId";
const supportOptions = [
[
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />,
"[NEW] Join Slack Forum",
"https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA",
"Join Slack Forum",
"https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g",
],
[
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />,

View File

@@ -1,4 +1,4 @@
import Aes256Gcm from "~/components/aes-256-gcm";
import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm";
import login1 from "~/pages/api/auth/Login1";
import login2 from "~/pages/api/auth/Login2";
import getOrganizations from "~/pages/api/organization/getOrgs";
@@ -6,7 +6,7 @@ import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProj
import { initPostHog } from "../analytics/posthog";
import { ENV } from "./config";
import pushKeys from "./pushKeys";
import pushKeys from "./secrets/pushKeys";
import SecurityClient from "./SecurityClient";
const nacl = require("tweetnacl");

View File

@@ -1,7 +1,7 @@
import changePassword2 from "~/pages/api/auth/ChangePassword2";
import SRP1 from "~/pages/api/auth/SRP1";
import Aes256Gcm from "../aes-256-gcm";
import Aes256Gcm from "./aes-256-gcm";
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
@@ -63,6 +63,7 @@ const changePassword = async (
async () => {
clientNewPassword.createVerifier(
async (err, result) => {
// The Blob part here is needed to account for symbols that count as 2+ bytes (e.g., é, å, ø)
let { ciphertext, iv, tag } = Aes256Gcm.encrypt(
localStorage.getItem("PRIVATE_KEY"),
newPassword

View File

@@ -1,6 +1,6 @@
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
const aes = require("../aes-256-gcm");
const aes = require("./aes-256-gcm");
/**
* Return assymmetrically encrypted [plaintext] using [publicKey] where

View File

@@ -1,8 +1,8 @@
import issueBackupPrivateKey from "~/pages/api/auth/IssueBackupPrivateKey";
import SRP1 from "~/pages/api/auth/SRP1";
import Aes256Gcm from "../aes-256-gcm";
import generateBackupPDF from "./generateBackupPDF";
import Aes256Gcm from "./aes-256-gcm";
import generateBackupPDF from "../generateBackupPDF";
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");

View File

@@ -1,11 +1,11 @@
import getSecrets from "~/pages/api/files/GetSecrets";
import guidGenerator from "./randomId";
import guidGenerator from "../randomId";
const {
decryptAssymmetric,
decryptSymmetric,
} = require("../../components/utilities/crypto");
} = require("../cryptography/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");

View File

@@ -8,7 +8,7 @@ const {
decryptSymmetric,
encryptSymmetric,
encryptAssymmetric,
} = require("../../components/utilities/crypto");
} = require("../cryptography/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");

View File

@@ -2,7 +2,7 @@ import publicKeyInfical from "~/pages/api/auth/publicKeyInfisical";
import changeHerokuConfigVars from "~/pages/api/integrations/ChangeHerokuConfigVars";
const crypto = require("crypto");
const { encryptSymmetric, encryptAssymmetric } = require("./crypto");
const { encryptSymmetric, encryptAssymmetric } = require("../cryptography/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");

View File

@@ -29,9 +29,9 @@ import BottonRightPopup from "~/components/basic/popups/BottomRightPopup";
import DashboardInputField from "~/components/dashboard/DashboardInputField";
import DropZone from "~/components/dashboard/DropZone";
import NavHeader from "~/components/navigation/NavHeader";
import getSecretsForProject from "~/utilities/getSecretsForProject";
import pushKeys from "~/utilities/pushKeys";
import pushKeysIntegration from "~/utilities/pushKeysIntegration";
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 getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
@@ -699,7 +699,7 @@ export default function Dashboard() {
data?.length > 8 ? "h-3/4" : "h-min"
}`}
>
<div className="sticky top-0 z-10 bg-bunker flex flex-row pl-4 pr-5 pt-4 pb-2 items-center justify-between text-gray-300 font-bold">
<div className="sticky top-0 z-40 bg-bunker flex flex-row pl-4 pr-5 pt-4 pb-2 items-center justify-between text-gray-300 font-bold">
{/* <FontAwesomeIcon icon={faAngleDown} /> */}
<div className="flex flex-row items-center">
<p className="pl-2 text-lg">

View File

@@ -13,8 +13,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Button from "~/components/basic/buttons/Button";
import ListBox from "~/components/basic/Listbox";
import NavHeader from "~/components/navigation/NavHeader";
import getSecretsForProject from "~/utilities/getSecretsForProject";
import pushKeysIntegration from "~/utilities/pushKeysIntegration";
import getSecretsForProject from "~/components/utilities/secrets/getSecretsForProject";
import pushKeysIntegration from "~/components/utilities/secrets/pushKeysIntegration";
import guidGenerator from "~/utilities/randomId";
import deleteIntegration from "../api/integrations/DeleteIntegration";

View File

@@ -6,9 +6,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Button from "~/components/basic/buttons/Button";
import InputField from "~/components/basic/InputField";
import NavHeader from "~/components/navigation/NavHeader";
import changePassword from "~/utilities/changePassword";
import changePassword from "~/components/utilities/cryptography/changePassword";
import passwordCheck from "~/utilities/checks/PasswordCheck";
import issueBackupKey from "~/utilities/issueBackupKey";
import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey";
import getUser from "../../api/user/getUser";

View File

@@ -7,13 +7,13 @@ import { useRouter } from "next/router";
import { faCheck, faWarning, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Aes256Gcm from "~/components/aes-256-gcm";
import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm";
import Button from "~/components/basic/buttons/Button";
import Error from "~/components/basic/Error";
import InputField from "~/components/basic/InputField";
import attemptLogin from "~/utilities/attemptLogin";
import passwordCheck from "~/utilities/checks/PasswordCheck";
import issueBackupKey from "~/utilities/issueBackupKey";
import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey";
import checkEmailVerificationCode from "./api/auth/CheckEmailVerificationCode";
import completeAccountInformationSignup from "./api/auth/CompleteAccountInformationSignup";

View File

@@ -6,12 +6,12 @@ import { useRouter } from "next/router";
import { faCheck, faWarning,faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Aes256Gcm from "~/components/aes-256-gcm";
import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm";
import Button from "~/components/basic/buttons/Button";
import InputField from "~/components/basic/InputField";
import attemptLogin from "~/utilities/attemptLogin";
import passwordCheck from "~/utilities/checks/PasswordCheck";
import issueBackupKey from "~/utilities/issueBackupKey";
import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey";
import completeAccountInformationSignupInvite from "./api/auth/CompleteAccountInformationSignupInvite";
import verifySignupInvite from "./api/auth/VerifySignupInvite";

View File

@@ -23,7 +23,7 @@ const crypto = require("crypto");
const {
decryptAssymmetric,
encryptAssymmetric,
} = require("../../components/utilities/crypto");
} = require("../../components/utilities/cryptography/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");