From 618dc10e45da2dc563a0765236e47db99f85d808 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 28 Dec 2022 12:02:42 -0500 Subject: [PATCH 01/23] Added .NET to available frameworks --- README.md | 18 +++- backend/src/models/secret.ts | 3 +- backend/src/routes/v2/secret.ts | 135 +++++++++++++++++++++++++++- cli/go.mod | 2 + cli/go.sum | 10 +++ cli/packages/cmd/secrets.go | 103 +++++++++++++++++++++ cli/packages/util/crypto.go | 56 +++++++++++- cli/packages/util/secrets.go | 7 +- cli/packages/visualize/secrets.go | 14 +++ cli/packages/visualize/visualize.go | 12 ++- docs/mint.json | 3 +- 11 files changed, 348 insertions(+), 15 deletions(-) create mode 100644 cli/packages/cmd/secrets.go create mode 100644 cli/packages/visualize/secrets.go diff --git a/README.md b/README.md index ac2f2a5b5..b3bcd7ac6 100644 --- a/README.md +++ b/README.md @@ -270,13 +270,13 @@ We're currently setting the foundation and building [integrations](https://infis - - ✔️ Ruby on Rails + + ✔️ Vue - - ✔️ Vue + + ✔️ Ruby on Rails @@ -292,6 +292,16 @@ We're currently setting the foundation and building [integrations](https://infis + + + + ✔️ .NET + + + + And more... + + diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index d36e32b91..bbaaff8c3 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -33,7 +33,8 @@ const secretSchema = new Schema( { version: { type: Number, - required: true + required: true, + default: 1 }, workspace: { type: Schema.Types.ObjectId, diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 17a91d39c..fb2a23f42 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,4 +1,137 @@ -import express from 'express'; +import express, { Request, Response } from 'express'; +import { requireAuth, validateRequest } from '../../middleware'; +import { ISecret, Secret } from '../../models'; +import { decryptSymmetric } from '../../utils/crypto'; +import { getLogger } from '../../utils/logger'; +import { body, param, query, check } from 'express-validator'; +import { BadRequestError } from '../../utils/errors'; const router = express.Router(); +/** + * Create a single secret for a given workspace and environment + */ +router.post( + '/', requireAuth, + body('secret').exists().isObject(), + async (req: Request, res: Response) => { + try { + const { secret }: { secret: ISecret[] } = req.body; + const newlyCreatedSecret = await Secret.create(secret) + res.status(200).json(newlyCreatedSecret) + } catch { + throw BadRequestError({ message: "Unable to create the secret" }) + } + } +); + +/** + * Create many secrets + */ +router.post( + '/bulk-create', requireAuth, + body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + async (req: Request, res: Response) => { + try { + const { secrets }: { secrets: ISecret[] } = req.body; + const newlyCreatedSecrets = await Secret.insertMany(secrets) + res.status(200).json(newlyCreatedSecrets) + } catch { + throw BadRequestError({ message: "Unable to create the secret" }) + } + } +); + +/** + * Get a single secret by secret id + */ +router.get( + '/:secretId', requireAuth, param('secretId').exists().trim(), + validateRequest, async (req: Request, res: Response) => { + try { + const secretFromDB = await Secret.findById(req.params.secretId) + return res.status(200).send(secretFromDB); + } catch (e) { + throw BadRequestError({ message: "Unable to find the requested secret" }) + } + } +); + +/** + * Get a single secret by secret id + */ +router.get( + '/:bulk', requireAuth, param('secretId').exists().trim(), + validateRequest, async (req: Request, res: Response) => { + try { + const secretFromDB = await Secret.findById(req.params.secretId) + return res.status(200).send(secretFromDB); + } catch (e) { + throw BadRequestError({ message: "Unable to find the requested secret" }) + } + } +); + +/** + * Delete a single secret by secret id + */ +router.delete( + '/:secretId', + requireAuth, + param('secretId').exists().trim(), + validateRequest, async (req: Request, res: Response) => { + try { + const secretFromDB = await Secret.deleteOne({ + _id: req.params.secretId + }) + return res.status(200).send(secretFromDB); + } catch (e) { + throw BadRequestError({ message: "Unable to find the requested secret" }) + } + } +); + +/** + * Delete many secrets by secret ids + */ +router.delete( + '/batch', + requireAuth, + body('secretIds').exists().isArray(), + validateRequest, async (req: Request, res: Response) => { + try { + const secretIdsToDelete: string[] = req.body.secretIds + const secretFromDB = await Secret.deleteMany({ + _id: { $in: secretIdsToDelete } + }) + return res.status(200).send(secretFromDB); + } catch (error) { + throw BadRequestError({ message: `Unable to delete the requested secrets by ids [${req.body.secretIds}]` }) + } + } +); + +/** + * Apply modifications to many existing secrets + */ +router.patch( + '/bulk-update', + requireAuth, + body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + validateRequest, async (req: Request, res: Response) => { + try { + const { secrets }: { secrets: ISecret[] } = req.body; + + const operations = secrets.map((secretToUpdate: ISecret) => ({ + updateOne: { filter: { _id: secretToUpdate._id }, update: secretToUpdate }, + })); + + const bulkModificationInfo = await Secret.bulkWrite(operations); + + return res.status(200).json(bulkModificationInfo) + } catch (error) { + throw BadRequestError({ message: `Unable to process the bulk update. Double check the ids of the secrets` }) + } + } +); + export default router; diff --git a/cli/go.mod b/cli/go.mod index c48e3e2f9..956e9bb29 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -13,6 +13,7 @@ require ( require ( github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/Luzifer/go-openssl/v4 v4.1.0 // indirect github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/danieljoos/wincred v1.1.2 // indirect @@ -34,6 +35,7 @@ require ( ) require ( + github.com/Luzifer/go-openssl v2.0.0+incompatible github.com/go-resty/resty/v2 v2.7.0 github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jedib0t/go-pretty v4.3.0+incompatible diff --git a/cli/go.sum b/cli/go.sum index d169d7b89..2b8836515 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -2,6 +2,10 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= +github.com/Luzifer/go-openssl v2.0.0+incompatible h1:EpNNxrPDji4rRzE0KeOeIeV7pHyKe8zF9oNnAXy4mBY= +github.com/Luzifer/go-openssl v2.0.0+incompatible/go.mod h1:t2qnLjT8WQ3usGU1R8uAqjY4T7CK7eMg9vhQ3l9Ue/Y= +github.com/Luzifer/go-openssl/v4 v4.1.0 h1:8qi3Z6f8Aflwub/Cs4FVSmKUEg/lC8GlODbR2TyZ+nM= +github.com/Luzifer/go-openssl/v4 v4.1.0/go.mod h1:3i1T3Pe6eQK19d86WhuQzjLyMwBaNmGmt3ZceWpWVa4= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -96,15 +100,20 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg= go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -116,6 +125,7 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go new file mode 100644 index 000000000..f8efd6724 --- /dev/null +++ b/cli/packages/cmd/secrets.go @@ -0,0 +1,103 @@ +/* +Copyright © 2022 NAME HERE +*/ +package cmd + +import ( + "fmt" + + "github.com/Infisical/infisical-merge/packages/models" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/Infisical/infisical-merge/packages/visualize" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var secretsCmd = &cobra.Command{ + Example: `infisical secrets"`, + Short: "Used to create, read update and delete secrets", + Use: "secrets", + DisableFlagsInUseLine: true, + PreRun: toggleDebug, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + + secrets, err := util.GetAllEnvironmentVariables("", "dev") + secrets = util.SubstituteSecrets(secrets) + if err != nil { + log.Debugln(err) + return + } + visualize.PrintAllSecretDetails(secrets) + }, +} + +var secretsGetCmd = &cobra.Command{ + Example: `secrets get ..."`, + Short: "Used to retrieve secrets by name", + Use: "get [secrets]", + DisableFlagsInUseLine: true, + Args: cobra.MinimumNArgs(1), + PreRun: toggleDebug, + Run: getSecretsByNames, +} + +var secretsSetCmd = &cobra.Command{ + Example: `secrets set ..."`, + Short: "Used update retrieve secrets by name", + Use: "set [secrets]", + DisableFlagsInUseLine: true, + PreRun: toggleDebug, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("set secret") + }, +} + +var secretsDeleteCmd = &cobra.Command{ + Example: `secrets delete ..."`, + Short: "Used to delete secrets by name", + Use: "delete [secrets]", + DisableFlagsInUseLine: true, + PreRun: toggleDebug, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Delete secret") + }, +} + +func init() { + secretsCmd.AddCommand(secretsGetCmd) + secretsCmd.AddCommand(secretsSetCmd) + secretsCmd.AddCommand(secretsDeleteCmd) + rootCmd.AddCommand(secretsCmd) +} + +func getSecretsByNames(cmd *cobra.Command, args []string) { + secrets, err := util.GetAllEnvironmentVariables("", "dev") + if err != nil { + log.Error("Unable to retrieve secrets. Run with -d to see full logs") + log.Debug(err) + } + + requestedSecrets := []models.SingleEnvironmentVariable{} + + secretsMap := make(map[string]models.SingleEnvironmentVariable) + for _, secret := range secrets { + secretsMap[secret.Key] = secret + } + + for _, secretKeyFromArg := range args { + if value, ok := secretsMap[secretKeyFromArg]; ok { + requestedSecrets = append(requestedSecrets, value) + } else { + requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{ + Key: secretKeyFromArg, + Type: "NOT FOUND", + Value: "NOT FOUND", + }) + } + } + + visualize.PrintAllSecretDetails(requestedSecrets) +} diff --git a/cli/packages/util/crypto.go b/cli/packages/util/crypto.go index c6eee2d0c..0431989de 100644 --- a/cli/packages/util/crypto.go +++ b/cli/packages/util/crypto.go @@ -3,21 +3,26 @@ package util import ( "crypto/aes" "crypto/cipher" + "crypto/rand" + "io" + + "golang.org/x/crypto/nacl/box" ) -func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []byte) ([]byte, error) { +// will decrypt cipher text to plain text using iv and tag +func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } - aesgcm, err := cipher.NewGCMWithNonceSize(block, len(IV)) + aesgcm, err := cipher.NewGCMWithNonceSize(block, len(iv)) if err != nil { return nil, err } - var nonce = IV - var ciphertext = append(encryptedPrivateKey, tag...) + var nonce = iv + var ciphertext = append(cipherText, tag...) // the aesgcm open method expects auth tag at the end of the cipher text plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) if err != nil { @@ -26,3 +31,46 @@ func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []b return plaintext, nil } + +func GenerateNewKey() (newKey []byte, keyErr error) { + key := make([]byte, 16) // block size defaults to 16 so this is fine + _, err := rand.Read(key) + return key, err +} + +// Will encrypt a plain text with the provided key +func EncryptSymmetric(plaintext []byte, key []byte) (cipherText []byte, iv []byte, tag []byte, err error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, nil, nil, err + } + + aesgcm, err := cipher.NewGCMWithNonceSize(block, 16) // default is 12, 16 because https://github.com/Infisical/infisical/blob/bea0ff6e05a4de73a5db625d4ae181a015b50855/backend/src/utils/aes-gcm.ts#L4 + if err != nil { + return nil, nil, nil, err + } + + // create a nonce + nonce := make([]byte, aesgcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + panic(err) + } + + ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil) + + ciphertextOnly := ciphertext[:len(ciphertext)-16] // combines the auth tag with the cipher text so we need to extract it + + authTag := ciphertext[len(ciphertext)-16:] + + return ciphertextOnly, nonce, authTag, nil +} + +func DecryptAsymmetric(ciphertext []byte, nonce []byte, publicKey []byte, privateKey []byte) (plainText []byte) { + plainTextToReturn, _ := box.Open(nil, ciphertext, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey)) + return plainTextToReturn +} + +func EncryptAssymmetric(message []byte, nonce []byte, publicKey []byte, privateKey []byte) (encryptedMessage []byte) { + encryptedPlainText := box.Seal(nil, message, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey)) + return encryptedPlainText +} diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index f88aed96b..f41e73a0f 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -11,7 +11,6 @@ import ( "github.com/Infisical/infisical-merge/packages/models" "github.com/go-resty/resty/v2" log "github.com/sirupsen/logrus" - "golang.org/x/crypto/nacl/box" ) const PERSONAL_SECRET_TYPE_NAME = "personal" @@ -56,7 +55,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, } // log.Debugln("workspaceKey", workspaceKey, "nonce", nonce, "senderPublicKey", senderPublicKey, "currentUsersPrivateKey", currentUsersPrivateKey) - workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey)) + workspaceKeyInBytes := DecryptAsymmetric(workspaceKey, nonce, senderPublicKey, currentUsersPrivateKey) var listOfEnv []models.SingleEnvironmentVariable for _, secret := range pullSecretsRequestResponse.Secrets { @@ -166,7 +165,8 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, return nil, err } - workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey)) + // workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey)) + workspaceKeyInBytes := DecryptAsymmetric(workspaceKey, nonce, senderPublicKey, currentUsersPrivateKey) var listOfEnv []models.SingleEnvironmentVariable for _, secret := range pullSecretsByInfisicalTokenResponse.Secrets { @@ -223,6 +223,7 @@ func GetAllEnvironmentVariables(projectId string, envName string) ([]models.Sing return nil, err } + // TODO: Should be based on flag. I.e only get all workspaces if desired, otherwise only get the one in the current root of project workspaceConfigs, err := GetAllWorkSpaceConfigsStartingFromCurrentPath() if err != nil { return nil, fmt.Errorf("unable to check if you have a %s file in your current directory", INFISICAL_WORKSPACE_CONFIG_FILE_NAME) diff --git a/cli/packages/visualize/secrets.go b/cli/packages/visualize/secrets.go new file mode 100644 index 000000000..e732d06af --- /dev/null +++ b/cli/packages/visualize/secrets.go @@ -0,0 +1,14 @@ +package visualize + +import "github.com/Infisical/infisical-merge/packages/models" + +func PrintAllSecretDetails(secrets []models.SingleEnvironmentVariable) { + rows := [][]string{} + for _, secret := range secrets { + rows = append(rows, []string{secret.Key, secret.Value, secret.Type}) + } + + headers := []string{"Secret name", "Secret vaule", "Secret type"} + + Table(headers, rows) +} diff --git a/cli/packages/visualize/visualize.go b/cli/packages/visualize/visualize.go index 85d946a32..5658197ee 100644 --- a/cli/packages/visualize/visualize.go +++ b/cli/packages/visualize/visualize.go @@ -6,13 +6,23 @@ import ( "github.com/jedib0t/go-pretty/table" ) +type TableOptions struct { + Title string +} + +// func GetDefaultTableOptions() TableOptions{ +// return TableOptions{ +// Title: "", +// } +// } + // Given headers and rows, this function will print out a table func Table(headers []string, rows [][]string) { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.SetStyle(table.StyleLight) - // t.SetTitle("Title") + // t.SetTitle(tableOptions.Title) t.Style().Options.DrawBorder = true t.Style().Options.SeparateHeader = true t.Style().Options.SeparateColumns = true diff --git a/docs/mint.json b/docs/mint.json index cbac56d5a..e94b70a6b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -166,7 +166,8 @@ "integrations/frameworks/django", "integrations/frameworks/flask", "integrations/frameworks/laravel", - "integrations/frameworks/rails" + "integrations/frameworks/rails", + "integrations/frameworks/dotnet" ] }, { From 60445727e9c33eb60abaa88ac3ebda8f2d06e880 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 31 Dec 2022 17:48:56 -0500 Subject: [PATCH 02/23] merge with own change --- backend/src/routes/v2/secret.ts | 94 +++++++++++++++++++++++++++---- backend/src/types/secret/index.ts | 4 ++ package-lock.json | 30 ++++++++++ package.json | 4 ++ 4 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 backend/src/types/secret/index.ts diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index fb2a23f42..42badbff8 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,10 +1,15 @@ import express, { Request, Response } from 'express'; -import { requireAuth, validateRequest } from '../../middleware'; +import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware'; import { ISecret, Secret } from '../../models'; import { decryptSymmetric } from '../../utils/crypto'; import { getLogger } from '../../utils/logger'; import { body, param, query, check } from 'express-validator'; -import { BadRequestError } from '../../utils/errors'; +import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { ModifySecretPayload } from '../../types/secret'; +import { AnyBulkWriteOperation } from 'mongodb'; +import to from 'await-to-js'; + const router = express.Router(); /** @@ -13,6 +18,10 @@ const router = express.Router(); router.post( '/', requireAuth, body('secret').exists().isObject(), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), async (req: Request, res: Response) => { try { const { secret }: { secret: ISecret[] } = req.body; @@ -29,6 +38,10 @@ router.post( */ router.post( '/bulk-create', requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), async (req: Request, res: Response) => { try { @@ -46,6 +59,10 @@ router.post( */ router.get( '/:secretId', requireAuth, param('secretId').exists().trim(), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), validateRequest, async (req: Request, res: Response) => { try { const secretFromDB = await Secret.findById(req.params.secretId) @@ -61,6 +78,10 @@ router.get( */ router.get( '/:bulk', requireAuth, param('secretId').exists().trim(), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), validateRequest, async (req: Request, res: Response) => { try { const secretFromDB = await Secret.findById(req.params.secretId) @@ -77,6 +98,10 @@ router.get( router.delete( '/:secretId', requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), param('secretId').exists().trim(), validateRequest, async (req: Request, res: Response) => { try { @@ -96,6 +121,10 @@ router.delete( router.delete( '/batch', requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), body('secretIds').exists().isArray(), validateRequest, async (req: Request, res: Response) => { try { @@ -111,25 +140,66 @@ router.delete( ); /** - * Apply modifications to many existing secrets + * Apply modifications to many existing secrets in a given workspace and environment + * Note: although we do not check access for environments, we will in the future */ router.patch( - '/bulk-update', + '/bulk-modify/:workspaceId/:environmentName', requireAuth, body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + param('workspaceId').exists().trim(), + param('environmentName').exists().trim(), + // requireWorkspaceAuth({ + // acceptedRoles: [ADMIN, MEMBER], + // acceptedStatuses: [COMPLETED, GRANTED] + // }), validateRequest, async (req: Request, res: Response) => { try { - const { secrets }: { secrets: ISecret[] } = req.body; + const { workspaceId, environmentName } = req.params + const secretsModificationsRequested: ModifySecretPayload[] = req.body.secrets; - const operations = secrets.map((secretToUpdate: ISecret) => ({ - updateOne: { filter: { _id: secretToUpdate._id }, update: secretToUpdate }, - })); + const secretsUserCanModify: ISecret[] = await Secret.find({ workspace: workspaceId, environment: environmentName }) - const bulkModificationInfo = await Secret.bulkWrite(operations); + const secretsUserCanModifyMapBySecretId: Map = new Map(); + secretsUserCanModify.forEach(secret => secretsUserCanModifyMapBySecretId.set(secret._id.toString(), secret)) - return res.status(200).json(bulkModificationInfo) - } catch (error) { - throw BadRequestError({ message: `Unable to process the bulk update. Double check the ids of the secrets` }) + // Check if the entity has access to the secret ids it wants to modify + const updateOperationsToPerform: AnyBulkWriteOperation[] = [] + secretsModificationsRequested.forEach(userModifiedSecret => { + const canModifyRequestedSecret = secretsUserCanModifyMapBySecretId.has(userModifiedSecret._id.toString()) + if (canModifyRequestedSecret) { + const oldSecretInDB = secretsUserCanModifyMapBySecretId.get(userModifiedSecret._id.toString()) + + if (oldSecretInDB !== undefined) { + oldSecretInDB.secretKeyCiphertext = userModifiedSecret.secretKeyCiphertext + oldSecretInDB.secretKeyIV = userModifiedSecret.secretKeyIV + oldSecretInDB.secretKeyTag = userModifiedSecret.secretKeyTag + oldSecretInDB.secretKeyHash = userModifiedSecret.secretKeyHash + oldSecretInDB.secretValueCiphertext = userModifiedSecret.secretValueCiphertext + oldSecretInDB.secretValueIV = userModifiedSecret.secretValueIV + oldSecretInDB.secretValueTag = userModifiedSecret.secretValueTag + oldSecretInDB.secretValueHash = userModifiedSecret.secretValueHash + oldSecretInDB.secretCommentCiphertext = userModifiedSecret.secretCommentCiphertext + oldSecretInDB.secretCommentIV = userModifiedSecret.secretCommentIV + oldSecretInDB.secretCommentTag = userModifiedSecret.secretCommentTag + oldSecretInDB.secretCommentHash = userModifiedSecret.secretCommentHash + + const updateOperation = { updateOne: { filter: { _id: oldSecretInDB._id, workspace: oldSecretInDB.workspace }, update: { $inc: { version: 1 }, $set: oldSecretInDB } } } + updateOperationsToPerform.push(updateOperation) + } + } else { + throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) + } + }) + + const bulkModificationInfo = await Secret.bulkWrite(updateOperationsToPerform); + + return res.status(200).json({ + bulkModificationInfo + }) + + } catch (e) { + throw BadRequestError() } } ); diff --git a/backend/src/types/secret/index.ts b/backend/src/types/secret/index.ts new file mode 100644 index 000000000..11696484e --- /dev/null +++ b/backend/src/types/secret/index.ts @@ -0,0 +1,4 @@ +import { Omit } from 'utility-types'; +import { ISecret } from '../../models'; + +export type ModifySecretPayload = Omit; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cf02a1760..0ba1a0659 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,10 @@ "": { "name": "infisical", "license": "ISC", + "dependencies": { + "await-to-js": "^3.0.0", + "utility-types": "^3.10.0" + }, "devDependencies": { "eslint": "^8.29.0", "husky": "^8.0.2" @@ -169,6 +173,14 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1092,6 +1104,14 @@ "punycode": "^2.1.0" } }, + "node_modules/utility-types": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", + "engines": { + "node": ">= 4" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1248,6 +1268,11 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1910,6 +1935,11 @@ "punycode": "^2.1.0" } }, + "utility-types": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==" + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 4d042a13d..5b50c5859 100644 --- a/package.json +++ b/package.json @@ -21,5 +21,9 @@ "devDependencies": { "eslint": "^8.29.0", "husky": "^8.0.2" + }, + "dependencies": { + "await-to-js": "^3.0.0", + "utility-types": "^3.10.0" } } From a5e874144293aa411f4d3e670a0f5a52ce8f15e4 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 31 Dec 2022 17:57:07 -0500 Subject: [PATCH 03/23] update json5 --- backend/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 85b168c1e..0b557b81c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -6497,9 +6497,9 @@ "dev": true }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -16776,9 +16776,9 @@ "dev": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "jsonwebtoken": { From 3c6b1e51b59083fba9306993b66ad89fc05fa93a Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 31 Dec 2022 20:43:49 -0500 Subject: [PATCH 04/23] Add non try catch error handle and fix bulk patch --- backend/package-lock.json | 28 +++++ backend/package.json | 2 + backend/src/middleware/requireAuth.ts | 12 +-- backend/src/routes/v2/secret.ts | 100 ++++++++---------- .../src/types/secret/{index.ts => types.ts} | 4 +- backend/tsconfig.json | 19 +++- package-lock.json | 30 ------ package.json | 4 - 8 files changed, 100 insertions(+), 99 deletions(-) rename backend/src/types/secret/{index.ts => types.ts} (50%) diff --git a/backend/package-lock.json b/backend/package-lock.json index 0b557b81c..95f146812 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,6 +15,7 @@ "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", + "await-to-js": "^3.0.0", "axios": "^1.1.3", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", @@ -38,6 +39,7 @@ "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "typescript": "^4.9.3", + "utility-types": "^3.10.0", "winston": "^3.8.2", "winston-loki": "^6.0.6" }, @@ -3678,6 +3680,14 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/axios": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", @@ -11276,6 +11286,14 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/utility-types": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", + "engines": { + "node": ">= 4" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -14656,6 +14674,11 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==" + }, "axios": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", @@ -20178,6 +20201,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "utility-types": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index d1a03a74c..1eb5e3b45 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,6 +6,7 @@ "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", + "await-to-js": "^3.0.0", "axios": "^1.1.3", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", @@ -29,6 +30,7 @@ "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "typescript": "^4.9.3", + "utility-types": "^3.10.0", "winston": "^3.8.2", "winston-loki": "^6.0.6" }, diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index d917d362a..172f7b68e 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -20,10 +20,10 @@ declare module 'jsonwebtoken' { */ const requireAuth = async (req: Request, res: Response, next: NextFunction) => { // JWT authentication middleware - const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] - if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) - if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) - if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) + const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] + if (AUTH_TOKEN_TYPE === null) return next(BadRequestError({ message: `Missing Authorization Header in the request header.` })) + if (AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` })) + if (AUTH_TOKEN_VALUE === null) return next(BadRequestError({ message: 'Missing Authorization Body in the request header' })) const decodedToken = ( jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET) @@ -33,9 +33,9 @@ const requireAuth = async (req: Request, res: Response, next: NextFunction) => { _id: decodedToken.userId }).select('+publicKey'); - if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'})) + if (!user) return next(AccountNotFoundError({ message: 'Failed to locate User account' })) if (!user?.publicKey) - return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'})) + return next(UnauthorizedRequestError({ message: 'Unable to authenticate due to partially set up account' })) req.user = user; return next(); diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 42badbff8..078a04106 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -4,11 +4,12 @@ import { ISecret, Secret } from '../../models'; import { decryptSymmetric } from '../../utils/crypto'; import { getLogger } from '../../utils/logger'; import { body, param, query, check } from 'express-validator'; -import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors'; +import { BadRequestError, InternalServerError, UnauthorizedRequestError } from '../../utils/errors'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; -import { ModifySecretPayload } from '../../types/secret'; +import { ModifySecretPayload, SafeUpdateSecret } from '../../types/secret/types'; import { AnyBulkWriteOperation } from 'mongodb'; import to from 'await-to-js'; +import { Types } from 'mongoose'; const router = express.Router(); @@ -141,66 +142,59 @@ router.delete( /** * Apply modifications to many existing secrets in a given workspace and environment - * Note: although we do not check access for environments, we will in the future */ router.patch( '/bulk-modify/:workspaceId/:environmentName', requireAuth, body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), - param('workspaceId').exists().trim(), + param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), - // requireWorkspaceAuth({ - // acceptedRoles: [ADMIN, MEMBER], - // acceptedStatuses: [COMPLETED, GRANTED] - // }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), validateRequest, async (req: Request, res: Response) => { - try { - const { workspaceId, environmentName } = req.params - const secretsModificationsRequested: ModifySecretPayload[] = req.body.secrets; + const { workspaceId, environmentName } = req.params + const secretsModificationsRequested: ModifySecretPayload[] = req.body.secrets; - const secretsUserCanModify: ISecret[] = await Secret.find({ workspace: workspaceId, environment: environmentName }) - - const secretsUserCanModifyMapBySecretId: Map = new Map(); - secretsUserCanModify.forEach(secret => secretsUserCanModifyMapBySecretId.set(secret._id.toString(), secret)) - - // Check if the entity has access to the secret ids it wants to modify - const updateOperationsToPerform: AnyBulkWriteOperation[] = [] - secretsModificationsRequested.forEach(userModifiedSecret => { - const canModifyRequestedSecret = secretsUserCanModifyMapBySecretId.has(userModifiedSecret._id.toString()) - if (canModifyRequestedSecret) { - const oldSecretInDB = secretsUserCanModifyMapBySecretId.get(userModifiedSecret._id.toString()) - - if (oldSecretInDB !== undefined) { - oldSecretInDB.secretKeyCiphertext = userModifiedSecret.secretKeyCiphertext - oldSecretInDB.secretKeyIV = userModifiedSecret.secretKeyIV - oldSecretInDB.secretKeyTag = userModifiedSecret.secretKeyTag - oldSecretInDB.secretKeyHash = userModifiedSecret.secretKeyHash - oldSecretInDB.secretValueCiphertext = userModifiedSecret.secretValueCiphertext - oldSecretInDB.secretValueIV = userModifiedSecret.secretValueIV - oldSecretInDB.secretValueTag = userModifiedSecret.secretValueTag - oldSecretInDB.secretValueHash = userModifiedSecret.secretValueHash - oldSecretInDB.secretCommentCiphertext = userModifiedSecret.secretCommentCiphertext - oldSecretInDB.secretCommentIV = userModifiedSecret.secretCommentIV - oldSecretInDB.secretCommentTag = userModifiedSecret.secretCommentTag - oldSecretInDB.secretCommentHash = userModifiedSecret.secretCommentHash - - const updateOperation = { updateOne: { filter: { _id: oldSecretInDB._id, workspace: oldSecretInDB.workspace }, update: { $inc: { version: 1 }, $set: oldSecretInDB } } } - updateOperationsToPerform.push(updateOperation) - } - } else { - throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) - } - }) - - const bulkModificationInfo = await Secret.bulkWrite(updateOperationsToPerform); - - return res.status(200).json({ - bulkModificationInfo - }) - - } catch (e) { - throw BadRequestError() + const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) + if (secretIdsUserCanModifyError) { + throw InternalServerError({ message: "Unable to fetch secrets you own" }) } + + const secretsUserCanModifySet: Set = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString())); + const updateOperationsToPerform: any = [] + + secretsModificationsRequested.forEach(userModifiedSecret => { + if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { + const safeUpdateFields: SafeUpdateSecret = { + secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext, + secretKeyIV: userModifiedSecret.secretKeyIV, + secretKeyTag: userModifiedSecret.secretKeyTag, + secretKeyHash: userModifiedSecret.secretKeyHash, + secretValueCiphertext: userModifiedSecret.secretValueCiphertext, + secretValueIV: userModifiedSecret.secretValueIV, + secretValueTag: userModifiedSecret.secretValueTag, + secretValueHash: userModifiedSecret.secretValueHash, + secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext, + secretCommentIV: userModifiedSecret.secretCommentIV, + secretCommentTag: userModifiedSecret.secretCommentTag, + secretCommentHash: userModifiedSecret.secretCommentHash, + } + + const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: safeUpdateFields } } } + updateOperationsToPerform.push(updateOperation) + } else { + throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) + } + }) + + const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(updateOperationsToPerform).then()) + if (bulkModificationInfoError) { + throw InternalServerError({ message: "Unable to apply modifications, please try again" }) + } + + return res.status(200).send() } ); diff --git a/backend/src/types/secret/index.ts b/backend/src/types/secret/types.ts similarity index 50% rename from backend/src/types/secret/index.ts rename to backend/src/types/secret/types.ts index 11696484e..c4a8d0cc9 100644 --- a/backend/src/types/secret/index.ts +++ b/backend/src/types/secret/types.ts @@ -1,4 +1,6 @@ import { Omit } from 'utility-types'; import { ISecret } from '../../models'; -export type ModifySecretPayload = Omit; \ No newline at end of file +export type ModifySecretPayload = Omit; + +export type SafeUpdateSecret = Partial>; diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 0bfe3c372..98ea0808a 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,7 +1,9 @@ { "compilerOptions": { "target": "es2016", - "lib": ["es6"], + "lib": [ + "es6" + ], "module": "commonjs", "rootDir": "src", "resolveJsonModule": true, @@ -13,8 +15,15 @@ "strict": true, "noImplicitAny": true, "skipLibCheck": true, - "typeRoots": ["./src/types", "./node_modules/@types"] + "typeRoots": [ + "./src/types", + "./node_modules/@types" + ] }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0ba1a0659..cf02a1760 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,6 @@ "": { "name": "infisical", "license": "ISC", - "dependencies": { - "await-to-js": "^3.0.0", - "utility-types": "^3.10.0" - }, "devDependencies": { "eslint": "^8.29.0", "husky": "^8.0.2" @@ -173,14 +169,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/await-to-js": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", - "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1104,14 +1092,6 @@ "punycode": "^2.1.0" } }, - "node_modules/utility-types": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", - "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", - "engines": { - "node": ">= 4" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1268,11 +1248,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "await-to-js": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", - "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==" - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1935,11 +1910,6 @@ "punycode": "^2.1.0" } }, - "utility-types": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", - "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==" - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 5b50c5859..4d042a13d 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,5 @@ "devDependencies": { "eslint": "^8.29.0", "husky": "^8.0.2" - }, - "dependencies": { - "await-to-js": "^3.0.0", - "utility-types": "^3.10.0" } } From f015e6be6e465a1fcd271572fe17c3472193d990 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 1 Jan 2023 02:12:24 -0500 Subject: [PATCH 05/23] Add batch delete api and batch create api --- backend/src/middleware/validateRequest.ts | 6 +- backend/src/routes/v2/secret.ts | 180 ++++++++++------------ backend/src/types/secret/types.ts | 9 +- 3 files changed, 95 insertions(+), 100 deletions(-) diff --git a/backend/src/middleware/validateRequest.ts b/backend/src/middleware/validateRequest.ts index 484b02cab..3bc106189 100644 --- a/backend/src/middleware/validateRequest.ts +++ b/backend/src/middleware/validateRequest.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { validationResult } from 'express-validator'; -import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors'; /** * Validate intended inputs on [req] via express-validator @@ -15,12 +15,12 @@ const validate = (req: Request, res: Response, next: NextFunction) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { - return next(BadRequestError({context: {errors: errors.array}})) + return next(ValidationError({ context: { errors: `One or more of your paramters are invalid [error=${errors.array}]` } })) } return next(); } catch (err) { - return next(UnauthorizedRequestError({message: 'Unauthenticated requests are not allowed. Try logging in'})) + return next(UnauthorizedRequestError({ message: 'Unauthenticated requests are not allowed. Try logging in' })) } }; diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 078a04106..e9c6a9bd9 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,57 +1,70 @@ import express, { Request, Response } from 'express'; import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware'; import { ISecret, Secret } from '../../models'; -import { decryptSymmetric } from '../../utils/crypto'; -import { getLogger } from '../../utils/logger'; import { body, param, query, check } from 'express-validator'; -import { BadRequestError, InternalServerError, UnauthorizedRequestError } from '../../utils/errors'; +import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; -import { ModifySecretPayload, SafeUpdateSecret } from '../../types/secret/types'; -import { AnyBulkWriteOperation } from 'mongodb'; +import { SanitizedSecretModify, SecretUserInput, SanitizedSecretForCreate } from '../../types/secret/types'; import to from 'await-to-js'; -import { Types } from 'mongoose'; +import mongoose, { Types } from 'mongoose'; +import { AnyBulkWriteOperation } from 'mongodb'; +const { ValidationError } = mongoose.Error; const router = express.Router(); /** - * Create a single secret for a given workspace and environment + * Create many secrets for a given workspace and environmentName */ router.post( - '/', requireAuth, - body('secret').exists().isObject(), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), - async (req: Request, res: Response) => { - try { - const { secret }: { secret: ISecret[] } = req.body; - const newlyCreatedSecret = await Secret.create(secret) - res.status(200).json(newlyCreatedSecret) - } catch { - throw BadRequestError({ message: "Unable to create the secret" }) - } - } -); - -/** - * Create many secrets - */ -router.post( - '/bulk-create', requireAuth, + '/batch-create/workspace/:workspaceId/environment/:environmentName', + requireAuth, requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] }), + param('workspaceId').exists().isMongoId().trim(), + param('environmentName').exists().trim(), body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + validateRequest, async (req: Request, res: Response) => { - try { - const { secrets }: { secrets: ISecret[] } = req.body; - const newlyCreatedSecrets = await Secret.insertMany(secrets) - res.status(200).json(newlyCreatedSecrets) - } catch { - throw BadRequestError({ message: "Unable to create the secret" }) + const secretsToCreate: SecretUserInput[] = req.body.secrets; + const { workspaceId, environmentName } = req.params + const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] + + secretsToCreate.forEach(rawSecret => { + const safeUpdateFields: SanitizedSecretForCreate = { + secretKeyCiphertext: rawSecret.secretKeyCiphertext, + secretKeyIV: rawSecret.secretKeyIV, + secretKeyTag: rawSecret.secretKeyTag, + secretKeyHash: rawSecret.secretKeyHash, + secretValueCiphertext: rawSecret.secretValueCiphertext, + secretValueIV: rawSecret.secretValueIV, + secretValueTag: rawSecret.secretValueTag, + secretValueHash: rawSecret.secretValueHash, + secretCommentCiphertext: rawSecret.secretCommentCiphertext, + secretCommentIV: rawSecret.secretCommentIV, + secretCommentTag: rawSecret.secretCommentTag, + secretCommentHash: rawSecret.secretCommentHash, + workspace: new Types.ObjectId(workspaceId), + environment: environmentName, + type: rawSecret.type, + user: new Types.ObjectId(req.user._id) + } + + sanitizedSecretesToCreate.push(safeUpdateFields) + }) + + const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then()) + + if (bulkCreateError) { + if (bulkCreateError instanceof ValidationError) { + throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack }) + } + + throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack }) } + + res.status(200).send(newlyCreatedSecrets) } ); @@ -75,68 +88,45 @@ router.get( ); /** - * Get a single secret by secret id - */ -router.get( - '/:bulk', requireAuth, param('secretId').exists().trim(), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), - validateRequest, async (req: Request, res: Response) => { - try { - const secretFromDB = await Secret.findById(req.params.secretId) - return res.status(200).send(secretFromDB); - } catch (e) { - throw BadRequestError({ message: "Unable to find the requested secret" }) - } - } -); - -/** - * Delete a single secret by secret id + * Batch delete secrets in a given workspace and environment name */ router.delete( - '/:secretId', + '/batch/workspace/:workspaceId/environment/:environmentName', requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), - param('secretId').exists().trim(), - validateRequest, async (req: Request, res: Response) => { - try { - const secretFromDB = await Secret.deleteOne({ - _id: req.params.secretId - }) - return res.status(200).send(secretFromDB); - } catch (e) { - throw BadRequestError({ message: "Unable to find the requested secret" }) - } - } -); - -/** - * Delete many secrets by secret ids - */ -router.delete( - '/batch', - requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), + param('workspaceId').exists().isMongoId().trim(), + param('environmentName').exists().trim(), body('secretIds').exists().isArray(), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), validateRequest, async (req: Request, res: Response) => { - try { - const secretIdsToDelete: string[] = req.body.secretIds - const secretFromDB = await Secret.deleteMany({ - _id: { $in: secretIdsToDelete } - }) - return res.status(200).send(secretFromDB); - } catch (error) { - throw BadRequestError({ message: `Unable to delete the requested secrets by ids [${req.body.secretIds}]` }) + const { workspaceId, environmentName } = req.params + const secretIdsToDelete: string[] = req.body.secretIds + + const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) + if (secretIdsUserCanDeleteError) { + throw InternalServerError({ message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` }) } + + const secretsUserCanDeleteSet: Set = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString())); + const deleteOperationsToPerform: AnyBulkWriteOperation[] = [] + + secretIdsToDelete.forEach(secretIdToDelete => { + if (secretsUserCanDeleteSet.has(secretIdToDelete)) { + const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } } + deleteOperationsToPerform.push(deleteOperation) + } else { + throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" }) + } + }) + + const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(deleteOperationsToPerform).then()) + if (bulkModificationInfoError) { + throw InternalServerError({ message: "Unable to apply modifications, please try again" }) + } + + res.status(200).send() } ); @@ -144,7 +134,7 @@ router.delete( * Apply modifications to many existing secrets in a given workspace and environment */ router.patch( - '/bulk-modify/:workspaceId/:environmentName', + '/batch-modify/:workspaceId/:environmentName', requireAuth, body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), param('workspaceId').exists().isMongoId().trim(), @@ -155,7 +145,7 @@ router.patch( }), validateRequest, async (req: Request, res: Response) => { const { workspaceId, environmentName } = req.params - const secretsModificationsRequested: ModifySecretPayload[] = req.body.secrets; + const secretsModificationsRequested: SecretUserInput[] = req.body.secrets; const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) if (secretIdsUserCanModifyError) { @@ -167,7 +157,7 @@ router.patch( secretsModificationsRequested.forEach(userModifiedSecret => { if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { - const safeUpdateFields: SafeUpdateSecret = { + const sanitizedSecret: SanitizedSecretModify = { secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext, secretKeyIV: userModifiedSecret.secretKeyIV, secretKeyTag: userModifiedSecret.secretKeyTag, @@ -182,7 +172,7 @@ router.patch( secretCommentHash: userModifiedSecret.secretCommentHash, } - const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: safeUpdateFields } } } + const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } } updateOperationsToPerform.push(updateOperation) } else { throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) diff --git a/backend/src/types/secret/types.ts b/backend/src/types/secret/types.ts index c4a8d0cc9..a773d778c 100644 --- a/backend/src/types/secret/types.ts +++ b/backend/src/types/secret/types.ts @@ -1,6 +1,11 @@ import { Omit } from 'utility-types'; import { ISecret } from '../../models'; -export type ModifySecretPayload = Omit; +// User input for CRUD operations on secrets routes +export type SecretUserInput = Omit; -export type SafeUpdateSecret = Partial>; +// Used for modeling sanitized secrets before uplaod. To be used for converting user input for uploading +export type SanitizedSecretModify = Partial>; + +// Used for modeling sanitized secrets before create. To be used for converting user input for creating new secrets +export type SanitizedSecretForCreate = Omit; From 939e9ba0757a58e6815ccfbc140eb39f85d7a978 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 1 Jan 2023 10:39:23 -0500 Subject: [PATCH 06/23] rename secret route with workspace and environment hierarchy --- backend/src/routes/v2/secret.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index e9c6a9bd9..65a926783 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -55,7 +55,6 @@ router.post( }) const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then()) - if (bulkCreateError) { if (bulkCreateError instanceof ValidationError) { throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack }) @@ -64,7 +63,7 @@ router.post( throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack }) } - res.status(200).send(newlyCreatedSecrets) + res.status(200).send() } ); @@ -134,7 +133,7 @@ router.delete( * Apply modifications to many existing secrets in a given workspace and environment */ router.patch( - '/batch-modify/:workspaceId/:environmentName', + '/batch-modify/workspace/:workspaceId/environment/:environmentName', requireAuth, body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), param('workspaceId').exists().isMongoId().trim(), From 776b4c29225c20976179405e0fa8769452b521a2 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 1 Jan 2023 11:18:00 -0500 Subject: [PATCH 07/23] update types for request body in secrets v2 api --- backend/src/routes/v2/secret.ts | 6 +++--- backend/src/types/secret/types.ts | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 65a926783..5691dda49 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -4,7 +4,7 @@ import { ISecret, Secret } from '../../models'; import { body, param, query, check } from 'express-validator'; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; -import { SanitizedSecretModify, SecretUserInput, SanitizedSecretForCreate } from '../../types/secret/types'; +import { SanitizedSecretModify, CreateSecretRequestBody, SanitizedSecretForCreate, ModifySecretRequestBody } from '../../types/secret/types'; import to from 'await-to-js'; import mongoose, { Types } from 'mongoose'; import { AnyBulkWriteOperation } from 'mongodb'; @@ -27,7 +27,7 @@ router.post( body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), validateRequest, async (req: Request, res: Response) => { - const secretsToCreate: SecretUserInput[] = req.body.secrets; + const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; const { workspaceId, environmentName } = req.params const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] @@ -144,7 +144,7 @@ router.patch( }), validateRequest, async (req: Request, res: Response) => { const { workspaceId, environmentName } = req.params - const secretsModificationsRequested: SecretUserInput[] = req.body.secrets; + const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) if (secretIdsUserCanModifyError) { diff --git a/backend/src/types/secret/types.ts b/backend/src/types/secret/types.ts index a773d778c..177df8c0f 100644 --- a/backend/src/types/secret/types.ts +++ b/backend/src/types/secret/types.ts @@ -1,11 +1,14 @@ -import { Omit } from 'utility-types'; +import { Assign, Omit } from 'utility-types'; import { ISecret } from '../../models'; -// User input for CRUD operations on secrets routes -export type SecretUserInput = Omit; +// Everything is required, except the omitted types +export type CreateSecretRequestBody = Omit; + +// Omit the listed properties, then make everything optional and then make _id required +export type ModifySecretRequestBody = Assign>, { _id: string }>; // Used for modeling sanitized secrets before uplaod. To be used for converting user input for uploading export type SanitizedSecretModify = Partial>; -// Used for modeling sanitized secrets before create. To be used for converting user input for creating new secrets +// Everything is required, except the omitted types export type SanitizedSecretForCreate = Omit; From f2bd4aec39ca98ef39da9767fb3a011b0f8ccbe4 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 1 Jan 2023 13:13:03 -0500 Subject: [PATCH 08/23] Show full validation error --- backend/src/middleware/validateRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/middleware/validateRequest.ts b/backend/src/middleware/validateRequest.ts index 3bc106189..1b0364766 100644 --- a/backend/src/middleware/validateRequest.ts +++ b/backend/src/middleware/validateRequest.ts @@ -15,7 +15,7 @@ const validate = (req: Request, res: Response, next: NextFunction) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { - return next(ValidationError({ context: { errors: `One or more of your paramters are invalid [error=${errors.array}]` } })) + return next(ValidationError({ context: { errors: `One or more of your parameters are invalid [error(s)=${(JSON.stringify(errors))}]` } })) } return next(); From ac4b67d98ea3c9ddb099352dfa15ef47fcedcf7a Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 1 Jan 2023 19:22:09 -0500 Subject: [PATCH 09/23] delete, get and create via cli --- backend/src/routes/v2/secret.ts | 4 +- cli/packages/cmd/root.go | 2 +- cli/packages/cmd/secrets.go | 173 +++++++++++++++++++++++++++++-- cli/packages/http/api.go | 82 +++++++++++++++ cli/packages/models/api.go | 63 +++++++++++ cli/packages/models/cli.go | 7 ++ cli/packages/util/credentials.go | 53 ++++++++++ cli/packages/util/crypto.go | 13 ++- cli/packages/util/secrets.go | 2 + 9 files changed, 385 insertions(+), 14 deletions(-) create mode 100644 cli/packages/http/api.go diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 5691dda49..69b70c37e 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -24,7 +24,7 @@ router.post( }), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), - body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')), validateRequest, async (req: Request, res: Response) => { const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; @@ -94,7 +94,7 @@ router.delete( requireAuth, param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), - body('secretIds').exists().isArray(), + body('secretIds').exists().isArray().custom(array => array.length > 0), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index f09f08800..0d505e968 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -30,7 +30,7 @@ func Execute() { func init() { rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") rootCmd.PersistentFlags().BoolVarP(&debugLogging, "debug", "d", false, "Enable verbose logging") - rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "https://app.infisical.com/api", "Point the CLI to your own backend") + rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "http://localhost:8080/api", "Point the CLI to your own backend") // rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { // } } diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index f8efd6724..a926a4293 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -4,11 +4,17 @@ Copyright © 2022 NAME HERE package cmd import ( + "encoding/base64" "fmt" + "strings" + "crypto/sha256" + + "github.com/Infisical/infisical-merge/packages/http" "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/visualize" + "github.com/go-resty/resty/v2" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -21,14 +27,15 @@ var secretsCmd = &cobra.Command{ PreRun: toggleDebug, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - secrets, err := util.GetAllEnvironmentVariables("", "dev") secrets = util.SubstituteSecrets(secrets) if err != nil { log.Debugln(err) return } + visualize.PrintAllSecretDetails(secrets) + }, } @@ -48,9 +55,95 @@ var secretsSetCmd = &cobra.Command{ Use: "set [secrets]", DisableFlagsInUseLine: true, PreRun: toggleDebug, - Args: cobra.NoArgs, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - fmt.Println("set secret") + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + if err != nil { + log.Error(err) + return + } + + if !loggedInUserDetails.IsUserLoggedIn { + log.Error("You are not logged in yet. Please run [infisical login] then try again") + return + } + + if loggedInUserDetails.IsUserLoggedIn && loggedInUserDetails.LoginExpired { + log.Error("Your login has expired. Please run [infisical login] then try again") + return + } + + httpClient := resty.New(). + SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetHeader("Accept", "application/json") + + request := models.GetEncryptedWorkspaceKeyRequest{ + WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + } + + workspaceKeyResponse, err := http.CallGetEncryptedWorkspaceKey(httpClient, request) + if err != nil { + log.Errorf("unable to get your encrypted workspace key. [err=%v]", err) + return + } + + encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.EncryptedKey) + encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.Sender.PublicKey) + encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.Nonce) + currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey) + + // decrypt workspace key + plainTextEncryptionKey := util.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) + secretsToUpload := []models.Secret{} + for _, arg := range args { + splitKeyValueFromArg := strings.SplitN(arg, "=", 2) + if len(splitKeyValueFromArg) < 2 { + splitKeyValueFromArg[1] = "" + } + + key := splitKeyValueFromArg[0] + value := splitKeyValueFromArg[1] + + encryptedKey, err := util.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey)) + if err != nil { + log.Errorf("unable to encrypt your secrets [err=%v]", err) + } + + hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) + + encryptedValue, err := util.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey)) + if err != nil { + log.Errorf("unable to encrypt your secrets [err=%v]", err) + } + + hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value))) + + fullEncryptedSecret := models.Secret{ + SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText), + SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce), + SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag), + SecretKeyHash: hashedKey, + SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), + SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), + SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), + SecretValueHash: hashedValue, + Type: "shared", + } + secretsToUpload = append(secretsToUpload, fullEncryptedSecret) + } + + batchCreateRequest := models.BatchCreateSecretsByWorkspaceAndEnvRequest{ + WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + EnvironmentName: "dev", + Secrets: secretsToUpload, + } + err = http.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest) + if err != nil { + log.Errorf("Unable to complete your request because %v", err) + return + } + + log.Infof("secret name(s) [%v] have been created", strings.Join(args, ", ")) }, } @@ -60,9 +153,65 @@ var secretsDeleteCmd = &cobra.Command{ Use: "delete [secrets]", DisableFlagsInUseLine: true, PreRun: toggleDebug, - Args: cobra.NoArgs, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Delete secret") + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + if err != nil { + log.Error(err) + return + } + + if !loggedInUserDetails.IsUserLoggedIn { + log.Error("You are not logged in yet. Please run [infisical login] then try again") + return + } + + if loggedInUserDetails.IsUserLoggedIn && loggedInUserDetails.LoginExpired { + log.Error("Your login has expired. Please run [infisical login] then try again") + return + } + + secrets, err := util.GetAllEnvironmentVariables("", "dev") + if err != nil { + log.Error("Unable to retrieve secrets. Run with -d to see full logs") + log.Debug(err) + } + + secretByKey := getSecretsByKeys(secrets) + validSecretIdsToDelete := []string{} + invalidSecretNamesThatDoNotExist := []string{} + + for _, secretKeyFromArg := range args { + if value, ok := secretByKey[secretKeyFromArg]; ok { + validSecretIdsToDelete = append(validSecretIdsToDelete, value.ID) + } else { + invalidSecretNamesThatDoNotExist = append(invalidSecretNamesThatDoNotExist, secretKeyFromArg) + } + } + + if len(invalidSecretNamesThatDoNotExist) != 0 { + log.Errorf("secret name(s) [%v] does not exist in your project. Please remove and re-run the command", strings.Join(invalidSecretNamesThatDoNotExist, ", ")) + return + } + + request := models.BatchDeleteSecretsBySecretIdsRequest{ + WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + EnvironmentName: "dev", + SecretIds: validSecretIdsToDelete, + } + + httpClient := resty.New(). + SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetHeader("Accept", "application/json") + + err = http.CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient, request) + if err != nil { + log.Errorf("Unable to complete your request because %v", err) + return + } + + log.Infof("secret name(s) [%v] have been deleted from your project", strings.Join(args, ", ")) + }, } @@ -93,11 +242,21 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { } else { requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{ Key: secretKeyFromArg, - Type: "NOT FOUND", - Value: "NOT FOUND", + Type: "*not found*", + Value: "*not found*", }) } } visualize.PrintAllSecretDetails(requestedSecrets) } + +func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable { + secretMapByName := make(map[string]models.SingleEnvironmentVariable) + + for _, secret := range secrets { + secretMapByName[secret.Key] = secret + } + + return secretMapByName +} diff --git a/cli/packages/http/api.go b/cli/packages/http/api.go new file mode 100644 index 000000000..67f8042c2 --- /dev/null +++ b/cli/packages/http/api.go @@ -0,0 +1,82 @@ +package http + +import ( + "fmt" + + "github.com/Infisical/infisical-merge/packages/models" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/go-resty/resty/v2" +) + +func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchModifySecretsByWorkspaceAndEnvRequest) error { + endpoint := fmt.Sprintf("%v/v2/secret/batch-modify/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName) + response, err := httpClient. + R(). + SetBody(request). + Patch(endpoint) + + if err != nil { + return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) + } + + return nil +} + +func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchCreateSecretsByWorkspaceAndEnvRequest) error { + endpoint := fmt.Sprintf("%v/v2/secret/batch-create/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName) + response, err := httpClient. + R(). + SetBody(request). + Post(endpoint) + + if err != nil { + return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) + } + + return nil +} + +func CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchDeleteSecretsBySecretIdsRequest) error { + endpoint := fmt.Sprintf("%v/v2/secret/batch/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName) + response, err := httpClient. + R(). + SetBody(request). + Delete(endpoint) + + if err != nil { + return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) + } + + return nil +} + +func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request models.GetEncryptedWorkspaceKeyRequest) (models.GetEncryptedWorkspaceKeyResponse, error) { + endpoint := fmt.Sprintf("%v/v1/key/%v/latest", util.INFISICAL_URL, request.WorkspaceId) + var result models.GetEncryptedWorkspaceKeyResponse + response, err := httpClient. + R(). + SetResult(&result). + Get(endpoint) + + if err != nil { + return models.GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return models.GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response) + } + + return result, nil +} diff --git a/cli/packages/models/api.go b/cli/packages/models/api.go index 552c7b9bc..5566cdb8b 100644 --- a/cli/packages/models/api.go +++ b/cli/packages/models/api.go @@ -128,3 +128,66 @@ type Workspace struct { V int `json:"__v"` Organization string `json:"organization,omitempty"` } + +type Secret struct { + SecretKeyCiphertext string `json:"secretKeyCiphertext,omitempty"` + SecretKeyIV string `json:"secretKeyIV,omitempty"` + SecretKeyTag string `json:"secretKeyTag,omitempty"` + SecretKeyHash string `json:"secretKeyHash,omitempty"` + SecretValueCiphertext string `json:"secretValueCiphertext,omitempty"` + SecretValueIV string `json:"secretValueIV,omitempty"` + SecretValueTag string `json:"secretValueTag,omitempty"` + SecretValueHash string `json:"secretValueHash,omitempty"` + SecretCommentCiphertext string `json:"secretCommentCiphertext,omitempty"` + SecretCommentIV string `json:"secretCommentIV,omitempty"` + SecretCommentTag string `json:"secretCommentTag,omitempty"` + SecretCommentHash string `json:"secretCommentHash,omitempty"` + Type string `json:"type,omitempty"` + ID string `json:"_id,omitempty"` +} + +type BatchCreateSecretsByWorkspaceAndEnvRequest struct { + EnvironmentName string `json:"environmentName"` + WorkspaceId string `json:"workspaceId"` + Secrets []Secret `json:"secrets"` +} + +type BatchModifySecretsByWorkspaceAndEnvRequest struct { + EnvironmentName string `json:"environmentName"` + WorkspaceId string `json:"workspaceId"` + Secrets []Secret `json:"secrets"` +} + +type BatchDeleteSecretsBySecretIdsRequest struct { + EnvironmentName string `json:"environmentName"` + WorkspaceId string `json:"workspaceId"` + SecretIds []string `json:"secretIds"` +} + +type GetEncryptedWorkspaceKeyRequest struct { + WorkspaceId string `json:"workspaceId"` +} + +type GetEncryptedWorkspaceKeyResponse struct { + LatestKey struct { + ID string `json:"_id"` + EncryptedKey string `json:"encryptedKey"` + Nonce string `json:"nonce"` + Sender struct { + ID string `json:"_id"` + Email string `json:"email"` + RefreshVersion int `json:"refreshVersion"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + V int `json:"__v"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + PublicKey string `json:"publicKey"` + } `json:"sender"` + Receiver string `json:"receiver"` + Workspace string `json:"workspace"` + V int `json:"__v"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + } `json:"latestKey"` +} diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 8ba1c4627..70484de81 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -18,8 +18,15 @@ type SingleEnvironmentVariable struct { Key string `json:"key"` Value string `json:"value"` Type string `json:"type"` + ID string `json:"_id"` } type WorkspaceConfigFile struct { WorkspaceId string `json:"workspaceId"` } + +type SymmetricEncryptionResult struct { + CipherText []byte + Nonce []byte + AuthTag []byte +} diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index 80c98fa9a..bd0e67deb 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -12,6 +12,12 @@ import ( const SERVICE_NAME = "infisical" +type LoggedInUserDetails struct { + IsUserLoggedIn bool + LoginExpired bool + UserCredentials models.UserCredentials +} + // To do: what happens if the user doesn't have a keyring in their system? func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error { userCredMarshalled, err := json.Marshal(userCred) @@ -102,3 +108,50 @@ func IsUserLoggedIn() (hasUserLoggedIn bool, theUsersEmail string, err error) { return false, "", nil } } + +func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { + if ConfigFileExists() { + configFile, err := GetConfigFile() + if err != nil { + return LoggedInUserDetails{}, fmt.Errorf("getCurrentLoggedInUserDetails: unable to get logged in user from config file [err=%s]", err) + } + + if configFile.LoggedInUserEmail == "" { + return LoggedInUserDetails{}, nil + } + + userCreds, err := GetUserCredsFromKeyRing(configFile.LoggedInUserEmail) + if err != nil { + return LoggedInUserDetails{}, fmt.Errorf("getCurrentLoggedInUserDetails: unable to your credentials from Keyring [err=%s]", err) + } + + // check to to see if the JWT is still valid + httpClient := resty.New(). + SetAuthToken(userCreds.JTWToken). + SetHeader("Accept", "application/json") + + response, err := httpClient. + R(). + Post(fmt.Sprintf("%v/v1/auth/checkAuth", INFISICAL_URL)) + + if err != nil { + return LoggedInUserDetails{}, err + } + + if response.StatusCode() > 299 { + return LoggedInUserDetails{ + IsUserLoggedIn: true, + LoginExpired: true, + UserCredentials: userCreds, + }, nil + } + + return LoggedInUserDetails{ + IsUserLoggedIn: true, + LoginExpired: false, + UserCredentials: userCreds, + }, nil + } else { + return LoggedInUserDetails{}, nil + } +} diff --git a/cli/packages/util/crypto.go b/cli/packages/util/crypto.go index 0431989de..23e117f4b 100644 --- a/cli/packages/util/crypto.go +++ b/cli/packages/util/crypto.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "io" + "github.com/Infisical/infisical-merge/packages/models" "golang.org/x/crypto/nacl/box" ) @@ -39,15 +40,15 @@ func GenerateNewKey() (newKey []byte, keyErr error) { } // Will encrypt a plain text with the provided key -func EncryptSymmetric(plaintext []byte, key []byte) (cipherText []byte, iv []byte, tag []byte, err error) { +func EncryptSymmetric(plaintext []byte, key []byte) (result models.SymmetricEncryptionResult, err error) { block, err := aes.NewCipher(key) if err != nil { - return nil, nil, nil, err + return models.SymmetricEncryptionResult{}, err } aesgcm, err := cipher.NewGCMWithNonceSize(block, 16) // default is 12, 16 because https://github.com/Infisical/infisical/blob/bea0ff6e05a4de73a5db625d4ae181a015b50855/backend/src/utils/aes-gcm.ts#L4 if err != nil { - return nil, nil, nil, err + return models.SymmetricEncryptionResult{}, err } // create a nonce @@ -62,7 +63,11 @@ func EncryptSymmetric(plaintext []byte, key []byte) (cipherText []byte, iv []byt authTag := ciphertext[len(ciphertext)-16:] - return ciphertextOnly, nonce, authTag, nil + return models.SymmetricEncryptionResult{ + CipherText: ciphertextOnly, + AuthTag: authTag, + Nonce: nonce, + }, nil } func DecryptAsymmetric(ciphertext []byte, nonce []byte, publicKey []byte, privateKey []byte) (plainText []byte) { diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index f41e73a0f..8d9719698 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -81,6 +81,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, Key: string(plainTextKey), Value: string(plainTextValue), Type: string(secret.Type), + ID: secret.ID, } listOfEnv = append(listOfEnv, env) @@ -192,6 +193,7 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, Key: string(plainTextKey), Value: string(plainTextValue), Type: string(secret.Type), + ID: secret.ID, } listOfEnv = append(listOfEnv, env) From a07d4e6dd15625294434f50197a0cffe0c9a515a Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 2 Jan 2023 11:23:48 -0500 Subject: [PATCH 10/23] update types name for secrets v2 api --- backend/src/routes/v2/secret.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 69b70c37e..8c87db882 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -120,9 +120,12 @@ router.delete( } }) - const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(deleteOperationsToPerform).then()) - if (bulkModificationInfoError) { - throw InternalServerError({ message: "Unable to apply modifications, please try again" }) + const [bulkDeleteError, bulkDelete] = await to(Secret.bulkWrite(deleteOperationsToPerform).then()) + if (bulkDeleteError) { + if (bulkDeleteError instanceof ValidationError) { + throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkDeleteError.stack }) + } + throw InternalServerError() } res.status(200).send() @@ -135,7 +138,7 @@ router.delete( router.patch( '/batch-modify/workspace/:workspaceId/environment/:environmentName', requireAuth, - body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + body('secrets').exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), requireWorkspaceAuth({ @@ -145,7 +148,6 @@ router.patch( validateRequest, async (req: Request, res: Response) => { const { workspaceId, environmentName } = req.params const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; - const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) if (secretIdsUserCanModifyError) { throw InternalServerError({ message: "Unable to fetch secrets you own" }) @@ -154,6 +156,7 @@ router.patch( const secretsUserCanModifySet: Set = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString())); const updateOperationsToPerform: any = [] + secretsModificationsRequested.forEach(userModifiedSecret => { if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { const sanitizedSecret: SanitizedSecretModify = { @@ -180,7 +183,11 @@ router.patch( const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(updateOperationsToPerform).then()) if (bulkModificationInfoError) { - throw InternalServerError({ message: "Unable to apply modifications, please try again" }) + if (bulkModificationInfoError instanceof ValidationError) { + throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkModificationInfoError.stack }) + } + + throw InternalServerError() } return res.status(200).send() From ccb1c3141398262539d938d121c68d1245d3e5cd Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 2 Jan 2023 11:24:41 -0500 Subject: [PATCH 11/23] add set command for crud cli --- cli/packages/cmd/secrets.go | 97 ++++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 27 deletions(-) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index a926a4293..5b98a9f22 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -35,7 +35,6 @@ var secretsCmd = &cobra.Command{ } visualize.PrintAllSecretDetails(secrets) - }, } @@ -94,7 +93,19 @@ var secretsSetCmd = &cobra.Command{ // decrypt workspace key plainTextEncryptionKey := util.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) - secretsToUpload := []models.Secret{} + + // pull current secrets + secrets, err := util.GetAllEnvironmentVariables("", "dev") + if err != nil { + log.Error("Unable to retrieve secrets. Run with -d to see full logs") + log.Debug(err) + } + + secretsToCreate := []models.Secret{} + secretsToModify := []models.Secret{} + + secretByKey := getSecretsByKeys(secrets) + for _, arg := range args { splitKeyValueFromArg := strings.SplitN(arg, "=", 2) if len(splitKeyValueFromArg) < 2 { @@ -104,46 +115,78 @@ var secretsSetCmd = &cobra.Command{ key := splitKeyValueFromArg[0] value := splitKeyValueFromArg[1] + fmt.Println("key", key, "value", value) + + hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) encryptedKey, err := util.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey)) if err != nil { log.Errorf("unable to encrypt your secrets [err=%v]", err) } - hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) - + hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value))) encryptedValue, err := util.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey)) if err != nil { log.Errorf("unable to encrypt your secrets [err=%v]", err) } - hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value))) - - fullEncryptedSecret := models.Secret{ - SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText), - SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce), - SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag), - SecretKeyHash: hashedKey, - SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), - SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), - SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), - SecretValueHash: hashedValue, - Type: "shared", + if value, ok := secretByKey[key]; ok { + // case: secret exists in project so it needs to be modified + encryptedSecretDetails := models.Secret{ + ID: value.ID, + SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), + SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), + SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), + SecretValueHash: hashedValue, + } + secretsToModify = append(secretsToModify, encryptedSecretDetails) + } else { + // case: secret doesn't exist in project so it needs to be created + encryptedSecretDetails := models.Secret{ + SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText), + SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce), + SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag), + SecretKeyHash: hashedKey, + SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), + SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), + SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), + SecretValueHash: hashedValue, + Type: "shared", + } + secretsToCreate = append(secretsToCreate, encryptedSecretDetails) } - secretsToUpload = append(secretsToUpload, fullEncryptedSecret) } - batchCreateRequest := models.BatchCreateSecretsByWorkspaceAndEnvRequest{ - WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", - EnvironmentName: "dev", - Secrets: secretsToUpload, - } - err = http.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest) - if err != nil { - log.Errorf("Unable to complete your request because %v", err) - return + if len(secretsToCreate) > 0 { + fmt.Println("create") + batchCreateRequest := models.BatchCreateSecretsByWorkspaceAndEnvRequest{ + WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + EnvironmentName: "dev", + Secrets: secretsToCreate, + } + + err = http.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest) + if err != nil { + log.Errorf("Unable to process new secret creations because %v", err) + return + } } - log.Infof("secret name(s) [%v] have been created", strings.Join(args, ", ")) + if len(secretsToModify) > 0 { + fmt.Println("modify") + batchModifyRequest := models.BatchModifySecretsByWorkspaceAndEnvRequest{ + WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + EnvironmentName: "dev", + Secrets: secretsToModify, + } + + err = http.CallBatchModifySecretsByWorkspaceAndEnv(httpClient, batchModifyRequest) + if err != nil { + log.Errorf("Unable to process the modifications to your secrets because %v", err) + return + } + } + + log.Infof("secret name(s) [%v] have been set", strings.Join(args, ", ")) }, } From daf8a73529f1d0f965d68bed29d70fb1ab900b82 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 2 Jan 2023 22:41:15 -0500 Subject: [PATCH 12/23] add dynmaic workspace and user creds for secrets cmd --- cli/packages/cmd/secrets.go | 125 ++++++++++++++++++++++++++++------- cli/packages/util/common.go | 2 + cli/packages/util/secrets.go | 14 ++++ 3 files changed, 117 insertions(+), 24 deletions(-) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 5b98a9f22..8960b6603 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "fmt" "strings" + "unicode" "crypto/sha256" @@ -27,7 +28,14 @@ var secretsCmd = &cobra.Command{ PreRun: toggleDebug, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - secrets, err := util.GetAllEnvironmentVariables("", "dev") + environmentName, err := cmd.Flags().GetString("env") + if err != nil { + log.Errorln("Unable to parse the environment name flag") + log.Debugln(err) + return + } + + secrets, err := util.GetAllEnvironmentVariables("", environmentName) secrets = util.SubstituteSecrets(secrets) if err != nil { log.Debugln(err) @@ -56,6 +64,41 @@ var secretsSetCmd = &cobra.Command{ PreRun: toggleDebug, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { + secretType, err := cmd.Flags().GetString("type") + if err != nil { + log.Errorln("Unable to parse the secret type flag") + log.Debugln(err) + return + } + + if !util.IsSecretTypeValid(secretType) { + log.Errorf("secret type can only be `personal` or `shared`. You have entered [%v]", secretType) + return + } + + environmentName, err := cmd.Flags().GetString("env") + if err != nil { + log.Errorln("Unable to parse the environment name flag") + log.Debugln(err) + return + } + + if !util.IsSecretEnvironmentValid(environmentName) { + log.Errorln("You have entered a invalid environment name. Environment names can only be prod, dev, test or staging") + return + } + + workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() + if !workspaceFileExists { + log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") + } + + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + log.Error(err) + return + } + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() if err != nil { log.Error(err) @@ -77,7 +120,7 @@ var secretsSetCmd = &cobra.Command{ SetHeader("Accept", "application/json") request := models.GetEncryptedWorkspaceKeyRequest{ - WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + WorkspaceId: workspaceFile.WorkspaceId, } workspaceKeyResponse, err := http.CallGetEncryptedWorkspaceKey(httpClient, request) @@ -95,9 +138,9 @@ var secretsSetCmd = &cobra.Command{ plainTextEncryptionKey := util.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) // pull current secrets - secrets, err := util.GetAllEnvironmentVariables("", "dev") + secrets, err := util.GetAllEnvironmentVariables("", environmentName) if err != nil { - log.Error("Unable to retrieve secrets. Run with -d to see full logs") + log.Error("unable to retrieve secrets. Run with -d to see full logs") log.Debug(err) } @@ -108,15 +151,19 @@ var secretsSetCmd = &cobra.Command{ for _, arg := range args { splitKeyValueFromArg := strings.SplitN(arg, "=", 2) - if len(splitKeyValueFromArg) < 2 { - splitKeyValueFromArg[1] = "" + if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" { + log.Error("ensure that each secret has a none empty key and value. Modify the input and try again") + return + } + + if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) { + log.Error("keys of secrets cannot start with a number. Modify the key name(s) and try again") + return } key := splitKeyValueFromArg[0] value := splitKeyValueFromArg[1] - fmt.Println("key", key, "value", value) - hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) encryptedKey, err := util.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey)) if err != nil { @@ -129,16 +176,21 @@ var secretsSetCmd = &cobra.Command{ log.Errorf("unable to encrypt your secrets [err=%v]", err) } - if value, ok := secretByKey[key]; ok { + if existingSecret, ok := secretByKey[key]; ok { // case: secret exists in project so it needs to be modified encryptedSecretDetails := models.Secret{ - ID: value.ID, + ID: existingSecret.ID, SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueHash: hashedValue, } - secretsToModify = append(secretsToModify, encryptedSecretDetails) + + // Only add to modifications if the value is different + if existingSecret.Value != value { + secretsToModify = append(secretsToModify, encryptedSecretDetails) + } + } else { // case: secret doesn't exist in project so it needs to be created encryptedSecretDetails := models.Secret{ @@ -150,17 +202,16 @@ var secretsSetCmd = &cobra.Command{ SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueHash: hashedValue, - Type: "shared", + Type: secretType, } secretsToCreate = append(secretsToCreate, encryptedSecretDetails) } } if len(secretsToCreate) > 0 { - fmt.Println("create") batchCreateRequest := models.BatchCreateSecretsByWorkspaceAndEnvRequest{ - WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", - EnvironmentName: "dev", + WorkspaceId: workspaceFile.WorkspaceId, + EnvironmentName: environmentName, Secrets: secretsToCreate, } @@ -172,10 +223,9 @@ var secretsSetCmd = &cobra.Command{ } if len(secretsToModify) > 0 { - fmt.Println("modify") batchModifyRequest := models.BatchModifySecretsByWorkspaceAndEnvRequest{ - WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", - EnvironmentName: "dev", + WorkspaceId: workspaceFile.WorkspaceId, + EnvironmentName: environmentName, Secrets: secretsToModify, } @@ -186,7 +236,7 @@ var secretsSetCmd = &cobra.Command{ } } - log.Infof("secret name(s) [%v] have been set", strings.Join(args, ", ")) + log.Infoln("secrets have been successfully set") }, } @@ -198,6 +248,13 @@ var secretsDeleteCmd = &cobra.Command{ PreRun: toggleDebug, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { + environmentName, err := cmd.Flags().GetString("env") + if err != nil { + log.Errorln("Unable to parse the environment name flag") + log.Debugln(err) + return + } + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() if err != nil { log.Error(err) @@ -214,7 +271,18 @@ var secretsDeleteCmd = &cobra.Command{ return } - secrets, err := util.GetAllEnvironmentVariables("", "dev") + workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() + if !workspaceFileExists { + log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") + } + + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + log.Error(err) + return + } + + secrets, err := util.GetAllEnvironmentVariables("", environmentName) if err != nil { log.Error("Unable to retrieve secrets. Run with -d to see full logs") log.Debug(err) @@ -233,13 +301,13 @@ var secretsDeleteCmd = &cobra.Command{ } if len(invalidSecretNamesThatDoNotExist) != 0 { - log.Errorf("secret name(s) [%v] does not exist in your project. Please remove and re-run the command", strings.Join(invalidSecretNamesThatDoNotExist, ", ")) + log.Errorf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", ")) return } request := models.BatchDeleteSecretsBySecretIdsRequest{ - WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", - EnvironmentName: "dev", + WorkspaceId: workspaceFile.WorkspaceId, + EnvironmentName: environmentName, SecretIds: validSecretIdsToDelete, } @@ -260,13 +328,22 @@ var secretsDeleteCmd = &cobra.Command{ func init() { secretsCmd.AddCommand(secretsGetCmd) + secretsSetCmd.Flags().String("type", "shared", "Used to set the type for secrets") secretsCmd.AddCommand(secretsSetCmd) secretsCmd.AddCommand(secretsDeleteCmd) + secretsCmd.PersistentFlags().String("env", "dev", "Used to define the environment name on which actions should be taken on") rootCmd.AddCommand(secretsCmd) } func getSecretsByNames(cmd *cobra.Command, args []string) { - secrets, err := util.GetAllEnvironmentVariables("", "dev") + environmentName, err := cmd.Flags().GetString("env") + if err != nil { + log.Errorln("Unable to parse the environment name flag") + log.Debugln(err) + return + } + + secrets, err := util.GetAllEnvironmentVariables("", environmentName) if err != nil { log.Error("Unable to retrieve secrets. Run with -d to see full logs") log.Debug(err) diff --git a/cli/packages/util/common.go b/cli/packages/util/common.go index f3ee274b3..44f14a12b 100644 --- a/cli/packages/util/common.go +++ b/cli/packages/util/common.go @@ -10,6 +10,8 @@ const ( CONFIG_FOLDER_NAME = ".infisical" INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" + SECRET_TYPE_PERSONAL = "personal" + SECRET_TYPE_SHARED = "shared" ) var INFISICAL_URL = "https://app.infisical.com/api" diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 8d9719698..66d2b56e3 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -388,3 +388,17 @@ func OverrideWithPersonalSecrets(secrets []models.SingleEnvironmentVariable) []m return secretsToReturn } + +func IsSecretEnvironmentValid(env string) bool { + if env == "prod" || env == "dev" || env == "test" || env == "staging" { + return true + } + return false +} + +func IsSecretTypeValid(s string) bool { + if s == "personal" || s == "shared" { + return true + } + return false +} From 7e71e3ca570a02649a291c033a5ef99766fffdcb Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 3 Jan 2023 16:09:47 -0500 Subject: [PATCH 13/23] v1 crud secrets complete --- cli/packages/cmd/secrets.go | 76 +++++++++++++++++++++++++------ cli/packages/http/api.go | 20 ++++++++ cli/packages/models/api.go | 5 ++ cli/packages/visualize/secrets.go | 2 +- 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 8960b6603..d76890ea6 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -35,6 +35,12 @@ var secretsCmd = &cobra.Command{ return } + workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() + if !workspaceFileExists { + log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") + return + } + secrets, err := util.GetAllEnvironmentVariables("", environmentName) secrets = util.SubstituteSecrets(secrets) if err != nil { @@ -57,24 +63,24 @@ var secretsGetCmd = &cobra.Command{ } var secretsSetCmd = &cobra.Command{ - Example: `secrets set ..."`, - Short: "Used update retrieve secrets by name", + Example: `secrets set ..."`, + Short: "Used set secrets", Use: "set [secrets]", DisableFlagsInUseLine: true, PreRun: toggleDebug, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - secretType, err := cmd.Flags().GetString("type") - if err != nil { - log.Errorln("Unable to parse the secret type flag") - log.Debugln(err) - return - } + // secretType, err := cmd.Flags().GetString("type") + // if err != nil { + // log.Errorln("Unable to parse the secret type flag") + // log.Debugln(err) + // return + // } - if !util.IsSecretTypeValid(secretType) { - log.Errorf("secret type can only be `personal` or `shared`. You have entered [%v]", secretType) - return - } + // if !util.IsSecretTypeValid(secretType) { + // log.Errorf("secret type can only be `personal` or `shared`. You have entered [%v]", secretType) + // return + // } environmentName, err := cmd.Flags().GetString("env") if err != nil { @@ -91,6 +97,7 @@ var secretsSetCmd = &cobra.Command{ workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() if !workspaceFileExists { log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") + return } workspaceFile, err := util.GetWorkSpaceFromFile() @@ -144,8 +151,15 @@ var secretsSetCmd = &cobra.Command{ log.Debug(err) } + type SecretSetOperation struct { + SecretKey string + SecretValue string + SecretOperation string + } + secretsToCreate := []models.Secret{} secretsToModify := []models.Secret{} + secretOperations := []SecretSetOperation{} secretByKey := getSecretsByKeys(secrets) @@ -161,7 +175,8 @@ var secretsSetCmd = &cobra.Command{ return } - key := splitKeyValueFromArg[0] + // Key and value from argument + key := strings.ToUpper(splitKeyValueFromArg[0]) value := splitKeyValueFromArg[1] hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) @@ -189,6 +204,18 @@ var secretsSetCmd = &cobra.Command{ // Only add to modifications if the value is different if existingSecret.Value != value { secretsToModify = append(secretsToModify, encryptedSecretDetails) + secretOperations = append(secretOperations, SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET VALUE MODIFIED", + }) + } else { + // Current value is same as exisitng so no change + secretOperations = append(secretOperations, SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET VALUE UNCHANGED", + }) } } else { @@ -202,9 +229,14 @@ var secretsSetCmd = &cobra.Command{ SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueHash: hashedValue, - Type: secretType, + Type: util.SECRET_TYPE_SHARED, } secretsToCreate = append(secretsToCreate, encryptedSecretDetails) + secretOperations = append(secretOperations, SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET CREATED", + }) } } @@ -236,7 +268,14 @@ var secretsSetCmd = &cobra.Command{ } } - log.Infoln("secrets have been successfully set") + // Print secret operations + headers := []string{"SECRET NAME", "SECRET VALUE", "STATUS"} + rows := [][]string{} + for _, secretOperation := range secretOperations { + rows = append(rows, []string{secretOperation.SecretKey, secretOperation.SecretValue, secretOperation.SecretOperation}) + } + + visualize.Table(headers, rows) }, } @@ -274,6 +313,7 @@ var secretsDeleteCmd = &cobra.Command{ workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() if !workspaceFileExists { log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") + return } workspaceFile, err := util.GetWorkSpaceFromFile() @@ -343,6 +383,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { return } + workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() + if !workspaceFileExists { + log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") + return + } + secrets, err := util.GetAllEnvironmentVariables("", environmentName) if err != nil { log.Error("Unable to retrieve secrets. Run with -d to see full logs") diff --git a/cli/packages/http/api.go b/cli/packages/http/api.go index 67f8042c2..01b8a8a40 100644 --- a/cli/packages/http/api.go +++ b/cli/packages/http/api.go @@ -80,3 +80,23 @@ func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request models.GetEn return result, nil } + +func CallGetEncryptedSecretsByWorkspaceIdAndEnv(httpClient resty.Client, request models.GetSecretsByWorkspaceIdAndEnvironmentRequest) (models.PullSecretsResponse, error) { + var pullSecretsRequestResponse models.PullSecretsResponse + response, err := httpClient. + R(). + SetQueryParam("environment", request.EnvironmentName). + SetQueryParam("channel", "cli"). + SetResult(&pullSecretsRequestResponse). + Get(fmt.Sprintf("%v/v1/secret/%v", util.INFISICAL_URL, request.WorkspaceId)) + + if err != nil { + return models.PullSecretsResponse{}, fmt.Errorf("CallGetEncryptedSecretsByWorkspaceIdAndEnv: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return models.PullSecretsResponse{}, fmt.Errorf("CallGetEncryptedSecretsByWorkspaceIdAndEnv: Unsuccessful response: [response=%s]", response) + } + + return pullSecretsRequestResponse, nil +} diff --git a/cli/packages/models/api.go b/cli/packages/models/api.go index 5566cdb8b..d17200b87 100644 --- a/cli/packages/models/api.go +++ b/cli/packages/models/api.go @@ -191,3 +191,8 @@ type GetEncryptedWorkspaceKeyResponse struct { UpdatedAt time.Time `json:"updatedAt"` } `json:"latestKey"` } + +type GetSecretsByWorkspaceIdAndEnvironmentRequest struct { + EnvironmentName string `json:"environmentName"` + WorkspaceId string `json:"workspaceId"` +} diff --git a/cli/packages/visualize/secrets.go b/cli/packages/visualize/secrets.go index e732d06af..e9ac5d297 100644 --- a/cli/packages/visualize/secrets.go +++ b/cli/packages/visualize/secrets.go @@ -8,7 +8,7 @@ func PrintAllSecretDetails(secrets []models.SingleEnvironmentVariable) { rows = append(rows, []string{secret.Key, secret.Value, secret.Type}) } - headers := []string{"Secret name", "Secret vaule", "Secret type"} + headers := []string{"SECRET NAME", "SECRET VALUE", "SECRET TYPE"} Table(headers, rows) } From 59f5ad7710fe904b738f09acdaa993729a31c736 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 3 Jan 2023 16:39:33 -0500 Subject: [PATCH 14/23] add expand flag to crud sli --- cli/packages/cmd/secrets.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index d76890ea6..5e6c4ca16 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -35,6 +35,13 @@ var secretsCmd = &cobra.Command{ return } + shouldExpandSecrets, err := cmd.Flags().GetBool("expand") + if err != nil { + log.Errorln("Unable to parse the substitute flag") + log.Debugln(err) + return + } + workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() if !workspaceFileExists { log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") @@ -42,7 +49,11 @@ var secretsCmd = &cobra.Command{ } secrets, err := util.GetAllEnvironmentVariables("", environmentName) - secrets = util.SubstituteSecrets(secrets) + + if shouldExpandSecrets { + secrets = util.SubstituteSecrets(secrets) + } + if err != nil { log.Debugln(err) return @@ -333,7 +344,7 @@ var secretsDeleteCmd = &cobra.Command{ invalidSecretNamesThatDoNotExist := []string{} for _, secretKeyFromArg := range args { - if value, ok := secretByKey[secretKeyFromArg]; ok { + if value, ok := secretByKey[strings.ToUpper(secretKeyFromArg)]; ok { validSecretIdsToDelete = append(validSecretIdsToDelete, value.ID) } else { invalidSecretNamesThatDoNotExist = append(invalidSecretNamesThatDoNotExist, secretKeyFromArg) @@ -368,10 +379,11 @@ var secretsDeleteCmd = &cobra.Command{ func init() { secretsCmd.AddCommand(secretsGetCmd) - secretsSetCmd.Flags().String("type", "shared", "Used to set the type for secrets") + // secretsSetCmd.Flags().String("type", "shared", "Used to set the type for secrets") secretsCmd.AddCommand(secretsSetCmd) secretsCmd.AddCommand(secretsDeleteCmd) secretsCmd.PersistentFlags().String("env", "dev", "Used to define the environment name on which actions should be taken on") + secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") rootCmd.AddCommand(secretsCmd) } @@ -403,7 +415,7 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { } for _, secretKeyFromArg := range args { - if value, ok := secretsMap[secretKeyFromArg]; ok { + if value, ok := secretsMap[strings.ToUpper(secretKeyFromArg)]; ok { requestedSecrets = append(requestedSecrets, value) } else { requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{ From 3e945dd5522fcc2c35a6b0a5a32d165894e257e3 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 3 Jan 2023 16:40:08 -0500 Subject: [PATCH 15/23] move v2 secret api to controller --- backend/src/controllers/v2/index.ts | 6 +- .../src/controllers/v2/secretController.ts | 138 +++++++++++++++++ backend/src/routes/v2/secret.ts | 144 ++---------------- 3 files changed, 153 insertions(+), 135 deletions(-) diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index dc6977c91..24ca015ac 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -1,5 +1,7 @@ import * as workspaceController from './workspaceController'; +import * as secretController from './secretController'; -export { - workspaceController +export { + workspaceController, + secretController } diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts index e69de29bb..e29a8d19b 100644 --- a/backend/src/controllers/v2/secretController.ts +++ b/backend/src/controllers/v2/secretController.ts @@ -0,0 +1,138 @@ +import to from "await-to-js"; +import { Request, Response } from "express"; +import mongoose, { Types } from "mongoose"; +import Secret, { ISecret } from "../../models/secret"; +import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCreate, SanitizedSecretModify } from "../../types/secret/types"; +const { ValidationError } = mongoose.Error; +import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; +import { AnyBulkWriteOperation } from 'mongodb'; + +export const batchCreateSecrets = async (req: Request, res: Response) => { + const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; + const { workspaceId, environmentName } = req.params + const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] + + secretsToCreate.forEach(rawSecret => { + const safeUpdateFields: SanitizedSecretForCreate = { + secretKeyCiphertext: rawSecret.secretKeyCiphertext, + secretKeyIV: rawSecret.secretKeyIV, + secretKeyTag: rawSecret.secretKeyTag, + secretKeyHash: rawSecret.secretKeyHash, + secretValueCiphertext: rawSecret.secretValueCiphertext, + secretValueIV: rawSecret.secretValueIV, + secretValueTag: rawSecret.secretValueTag, + secretValueHash: rawSecret.secretValueHash, + secretCommentCiphertext: rawSecret.secretCommentCiphertext, + secretCommentIV: rawSecret.secretCommentIV, + secretCommentTag: rawSecret.secretCommentTag, + secretCommentHash: rawSecret.secretCommentHash, + workspace: new Types.ObjectId(workspaceId), + environment: environmentName, + type: rawSecret.type, + user: new Types.ObjectId(req.user._id) + } + + sanitizedSecretesToCreate.push(safeUpdateFields) + }) + + const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then()) + if (bulkCreateError) { + if (bulkCreateError instanceof ValidationError) { + throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack }) + } + + throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack }) + } + + res.status(200).send() +} + + +export const createSingleSecret = async (req: Request, res: Response) => { + try { + const secretFromDB = await Secret.findById(req.params.secretId) + return res.status(200).send(secretFromDB); + } catch (e) { + throw BadRequestError({ message: "Unable to find the requested secret" }) + } +} + +export const batchDeleteSecrets = async (req: Request, res: Response) => { + const { workspaceId, environmentName } = req.params + const secretIdsToDelete: string[] = req.body.secretIds + + const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) + if (secretIdsUserCanDeleteError) { + throw InternalServerError({ message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` }) + } + + const secretsUserCanDeleteSet: Set = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString())); + const deleteOperationsToPerform: AnyBulkWriteOperation[] = [] + + secretIdsToDelete.forEach(secretIdToDelete => { + if (secretsUserCanDeleteSet.has(secretIdToDelete)) { + const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } } + deleteOperationsToPerform.push(deleteOperation) + } else { + throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" }) + } + }) + + const [bulkDeleteError, bulkDelete] = await to(Secret.bulkWrite(deleteOperationsToPerform).then()) + if (bulkDeleteError) { + if (bulkDeleteError instanceof ValidationError) { + throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkDeleteError.stack }) + } + throw InternalServerError() + } + + res.status(200).send() +} + +export const batchModifySecrets = async (req: Request, res: Response) => { + const { workspaceId, environmentName } = req.params + const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; + const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) + if (secretIdsUserCanModifyError) { + throw InternalServerError({ message: "Unable to fetch secrets you own" }) + } + + const secretsUserCanModifySet: Set = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString())); + const updateOperationsToPerform: any = [] + + + secretsModificationsRequested.forEach(userModifiedSecret => { + if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { + const sanitizedSecret: SanitizedSecretModify = { + secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext, + secretKeyIV: userModifiedSecret.secretKeyIV, + secretKeyTag: userModifiedSecret.secretKeyTag, + secretKeyHash: userModifiedSecret.secretKeyHash, + secretValueCiphertext: userModifiedSecret.secretValueCiphertext, + secretValueIV: userModifiedSecret.secretValueIV, + secretValueTag: userModifiedSecret.secretValueTag, + secretValueHash: userModifiedSecret.secretValueHash, + secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext, + secretCommentIV: userModifiedSecret.secretCommentIV, + secretCommentTag: userModifiedSecret.secretCommentTag, + secretCommentHash: userModifiedSecret.secretCommentHash, + } + + const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } } + updateOperationsToPerform.push(updateOperation) + } else { + throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) + } + }) + + const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(updateOperationsToPerform).then()) + if (bulkModificationInfoError) { + if (bulkModificationInfoError instanceof ValidationError) { + throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkModificationInfoError.stack }) + } + + throw InternalServerError() + } + + return res.status(200).send() +} \ No newline at end of file diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 8c87db882..477e0039e 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,14 +1,9 @@ -import express, { Request, Response } from 'express'; +import express from 'express'; import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware'; -import { ISecret, Secret } from '../../models'; -import { body, param, query, check } from 'express-validator'; -import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; +import { body, param } from 'express-validator'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; -import { SanitizedSecretModify, CreateSecretRequestBody, SanitizedSecretForCreate, ModifySecretRequestBody } from '../../types/secret/types'; -import to from 'await-to-js'; -import mongoose, { Types } from 'mongoose'; -import { AnyBulkWriteOperation } from 'mongodb'; -const { ValidationError } = mongoose.Error; +import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret/types'; +import { secretController } from '../../controllers/v2'; const router = express.Router(); @@ -26,45 +21,7 @@ router.post( param('environmentName').exists().trim(), body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')), validateRequest, - async (req: Request, res: Response) => { - const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; - const { workspaceId, environmentName } = req.params - const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] - - secretsToCreate.forEach(rawSecret => { - const safeUpdateFields: SanitizedSecretForCreate = { - secretKeyCiphertext: rawSecret.secretKeyCiphertext, - secretKeyIV: rawSecret.secretKeyIV, - secretKeyTag: rawSecret.secretKeyTag, - secretKeyHash: rawSecret.secretKeyHash, - secretValueCiphertext: rawSecret.secretValueCiphertext, - secretValueIV: rawSecret.secretValueIV, - secretValueTag: rawSecret.secretValueTag, - secretValueHash: rawSecret.secretValueHash, - secretCommentCiphertext: rawSecret.secretCommentCiphertext, - secretCommentIV: rawSecret.secretCommentIV, - secretCommentTag: rawSecret.secretCommentTag, - secretCommentHash: rawSecret.secretCommentHash, - workspace: new Types.ObjectId(workspaceId), - environment: environmentName, - type: rawSecret.type, - user: new Types.ObjectId(req.user._id) - } - - sanitizedSecretesToCreate.push(safeUpdateFields) - }) - - const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then()) - if (bulkCreateError) { - if (bulkCreateError instanceof ValidationError) { - throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack }) - } - - throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack }) - } - - res.status(200).send() - } + secretController.batchCreateSecrets ); /** @@ -76,14 +33,8 @@ router.get( acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] }), - validateRequest, async (req: Request, res: Response) => { - try { - const secretFromDB = await Secret.findById(req.params.secretId) - return res.status(200).send(secretFromDB); - } catch (e) { - throw BadRequestError({ message: "Unable to find the requested secret" }) - } - } + validateRequest, + secretController.createSingleSecret ); /** @@ -99,37 +50,9 @@ router.delete( acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] }), - validateRequest, async (req: Request, res: Response) => { - const { workspaceId, environmentName } = req.params - const secretIdsToDelete: string[] = req.body.secretIds + validateRequest, + secretController.batchDeleteSecrets - const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) - if (secretIdsUserCanDeleteError) { - throw InternalServerError({ message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` }) - } - - const secretsUserCanDeleteSet: Set = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString())); - const deleteOperationsToPerform: AnyBulkWriteOperation[] = [] - - secretIdsToDelete.forEach(secretIdToDelete => { - if (secretsUserCanDeleteSet.has(secretIdToDelete)) { - const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } } - deleteOperationsToPerform.push(deleteOperation) - } else { - throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" }) - } - }) - - const [bulkDeleteError, bulkDelete] = await to(Secret.bulkWrite(deleteOperationsToPerform).then()) - if (bulkDeleteError) { - if (bulkDeleteError instanceof ValidationError) { - throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkDeleteError.stack }) - } - throw InternalServerError() - } - - res.status(200).send() - } ); /** @@ -145,53 +68,8 @@ router.patch( acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] }), - validateRequest, async (req: Request, res: Response) => { - const { workspaceId, environmentName } = req.params - const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; - const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) - if (secretIdsUserCanModifyError) { - throw InternalServerError({ message: "Unable to fetch secrets you own" }) - } - - const secretsUserCanModifySet: Set = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString())); - const updateOperationsToPerform: any = [] - - - secretsModificationsRequested.forEach(userModifiedSecret => { - if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { - const sanitizedSecret: SanitizedSecretModify = { - secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext, - secretKeyIV: userModifiedSecret.secretKeyIV, - secretKeyTag: userModifiedSecret.secretKeyTag, - secretKeyHash: userModifiedSecret.secretKeyHash, - secretValueCiphertext: userModifiedSecret.secretValueCiphertext, - secretValueIV: userModifiedSecret.secretValueIV, - secretValueTag: userModifiedSecret.secretValueTag, - secretValueHash: userModifiedSecret.secretValueHash, - secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext, - secretCommentIV: userModifiedSecret.secretCommentIV, - secretCommentTag: userModifiedSecret.secretCommentTag, - secretCommentHash: userModifiedSecret.secretCommentHash, - } - - const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } } - updateOperationsToPerform.push(updateOperation) - } else { - throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) - } - }) - - const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(updateOperationsToPerform).then()) - if (bulkModificationInfoError) { - if (bulkModificationInfoError instanceof ValidationError) { - throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkModificationInfoError.stack }) - } - - throw InternalServerError() - } - - return res.status(200).send() - } + validateRequest, + secretController.batchModifySecrets ); export default router; From 078c67f27c972621555036c697d5542ca769fce7 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 3 Jan 2023 17:43:40 -0500 Subject: [PATCH 16/23] Add crud cli docs --- docs/cli/commands/secrets.mdx | 93 +++++++++++++++++++++++++++++++++++ docs/mint.json | 1 + 2 files changed, 94 insertions(+) create mode 100644 docs/cli/commands/secrets.mdx diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx new file mode 100644 index 000000000..32ddb2255 --- /dev/null +++ b/docs/cli/commands/secrets.mdx @@ -0,0 +1,93 @@ +--- +title: "infisical secrets" +--- + +``` +infisical secrets +``` + +## Description +This command enables you to perform CRUD (create, read, update, delete) operations on secrets within your Infisical project. With it, you can view, create, update, and delete secrets in your environment. + +### Sub-commands + + Use this command to print out all of the secrets in your project + + ``` + $ infisical secrets + + ## Example + $ infisical secrets + ┌─────────────┬──────────────┬─────────────┐ + │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │ + ├─────────────┼──────────────┼─────────────┤ + │ DOMAIN │ example.com │ shared │ + │ HASH │ jebhfbwe │ shared │ + └─────────────┴──────────────┴─────────────┘ + ``` + + ### flags + + Parse shell parameter expansions in your secrets + + Default value: `true` + + + + + + This command allows you selectively print the requested secrets by name + + ``` + $ infisical secrets get ... + + # Example + $ infisical secrets get DOMAIN + ┌─────────────┬──────────────┬─────────────┐ + │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │ + ├─────────────┼──────────────┼─────────────┤ + │ DOMAIN │ example.com │ shared │ + └─────────────┴──────────────┴─────────────┘ + + ``` + + ### Flags + None + + + +This command allows you to set or update secrets in your environment. If the secret key provided already exists, its value will be updated with the new value. +If the secret key does not exist, a new secret will be created using both the key and value provided. + +``` +$ infisical secrets set ... + +## Example +$ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe +┌────────────────┬───────────────┬────────────────────────┐ +│ SECRET NAME │ SECRET VALUE │ STATUS │ +├────────────────┼───────────────┼────────────────────────┤ +│ STRIPE_API_KEY │ sjdgwkeudyjwe │ SECRET VALUE UNCHANGED │ +│ DOMAIN │ example.com │ SECRET VALUE MODIFIED │ +│ HASH │ jebhfbwe │ SECRET CREATED │ +└────────────────┴───────────────┴────────────────────────┘ +``` + + ### Flags + None + + + + This command allows you to delete secrets by their name(s). + + ``` + $ infisical secrets delete ... + + ## Example + $ infisical secrets delete STRIPE_API_KEY DOMAIN HASH + secret name(s) [STRIPE_API_KEY, DOMAIN, HASH] have been deleted from your project + ``` + + ### Flags + None + \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index e94b70a6b..9604e7beb 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -94,6 +94,7 @@ "cli/commands/login", "cli/commands/init", "cli/commands/run", + "cli/commands/secrets", "cli/commands/export", "cli/commands/vault" ] From fe05732c46ae71974652bd6ece0e4dc7d389aea4 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 10:39:32 -0500 Subject: [PATCH 17/23] update host to prod host --- cli/packages/cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index 0d505e968..f09f08800 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -30,7 +30,7 @@ func Execute() { func init() { rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") rootCmd.PersistentFlags().BoolVarP(&debugLogging, "debug", "d", false, "Enable verbose logging") - rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "http://localhost:8080/api", "Point the CLI to your own backend") + rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "https://app.infisical.com/api", "Point the CLI to your own backend") // rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { // } } From 68a8471292a70e43362652801199eed827c8a10c Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 10:58:00 -0500 Subject: [PATCH 18/23] remove accepted roles from secrets v2 api --- backend/src/routes/v2/secret.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 477e0039e..40d35d200 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,7 +1,7 @@ import express from 'express'; import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware'; import { body, param } from 'express-validator'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { ADMIN, MEMBER } from '../../variables'; import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret/types'; import { secretController } from '../../controllers/v2'; @@ -14,8 +14,7 @@ router.post( '/batch-create/workspace/:workspaceId/environment/:environmentName', requireAuth, requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), @@ -30,8 +29,7 @@ router.post( router.get( '/:secretId', requireAuth, param('secretId').exists().trim(), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), validateRequest, secretController.createSingleSecret @@ -47,8 +45,7 @@ router.delete( param('environmentName').exists().trim(), body('secretIds').exists().isArray().custom(array => array.length > 0), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), validateRequest, secretController.batchDeleteSecrets @@ -65,8 +62,7 @@ router.patch( param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), validateRequest, secretController.batchModifySecrets From 68c488b8ee9a66bc9e08131b2dbdf2e9ff8663ef Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 14:58:16 -0500 Subject: [PATCH 19/23] Add acceptedAuthModes for v2 secrets --- backend/src/routes/v2/secret.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 40d35d200..83108823f 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -12,7 +12,9 @@ const router = express.Router(); */ router.post( '/batch-create/workspace/:workspaceId/environment/:environmentName', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER] }), @@ -27,7 +29,11 @@ router.post( * Get a single secret by secret id */ router.get( - '/:secretId', requireAuth, param('secretId').exists().trim(), + '/:secretId', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + param('secretId').exists().trim(), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER] }), @@ -40,7 +46,9 @@ router.get( */ router.delete( '/batch/workspace/:workspaceId/environment/:environmentName', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), body('secretIds').exists().isArray().custom(array => array.length > 0), @@ -57,7 +65,9 @@ router.delete( */ router.patch( '/batch-modify/workspace/:workspaceId/environment/:environmentName', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('secrets').exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), From fba40b5d4b96a2acea4f431a4fb2d3bf7524bfb0 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 15:32:45 -0500 Subject: [PATCH 20/23] print requestError logs in backend when in dev mode --- backend/src/middleware/requestErrorHandler.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index 36f1dce49..3c59c72c3 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -4,26 +4,33 @@ import * as Sentry from '@sentry/node'; import { InternalServerError } from "../utils/errors"; import { getLogger } from "../utils/logger"; import RequestError, { LogLevel } from "../utils/requestError"; +import { NODE_ENV } from "../config"; -export const requestErrorHandler: ErrorRequestHandler = (error: RequestError|Error, req, res, next) => { - if(res.headersSent) return next(); +export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => { + if (res.headersSent) return next(); + if (NODE_ENV !== "production" && error instanceof RequestError) { + /* eslint-disable no-console */ + console.log(error) + /* eslint-enable no-console */ + } + //TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError - if(!(error instanceof RequestError)){ - error = InternalServerError({context: {exception: error.message}, stack: error.stack}) + if (!(error instanceof RequestError)) { + error = InternalServerError({ context: { exception: error.message }, stack: error.stack }) getLogger('backend-main').log((error).levelName.toLowerCase(), (error).message) } - + //* Set Sentry user identification if req.user is populated - if(req.user !== undefined && req.user !== null){ + if (req.user !== undefined && req.user !== null) { Sentry.setUser({ email: req.user.email }) } //* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL' //* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on - if([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((error).level)){ + if ([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((error).level)) { Sentry.captureException(error) } - + res.status((error).statusCode).json((error).format(req)) next() } \ No newline at end of file From 880f4d25a923e41c33c05e73c7ceeec0cc2c1cf8 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 17:35:40 -0500 Subject: [PATCH 21/23] print all errors during backend dev --- backend/src/middleware/requestErrorHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index 3c59c72c3..50044387e 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -9,7 +9,7 @@ import { NODE_ENV } from "../config"; export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => { if (res.headersSent) return next(); - if (NODE_ENV !== "production" && error instanceof RequestError) { + if (NODE_ENV !== "production") { /* eslint-disable no-console */ console.log(error) /* eslint-enable no-console */ From d75d9ec324956fdb4d9a1d9b3856f9fc44aec159 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 20:05:00 -0500 Subject: [PATCH 22/23] Add get call secrets route for service token and jwt --- backend/src/app.ts | 8 ++--- .../src/controllers/v2/secretController.ts | 30 +++++++++++++++++++ .../src/controllers/v2/workspaceController.ts | 16 +++++----- backend/src/routes/v2/secret.ts | 16 +++++----- backend/src/routes/v2/workspace.ts | 6 ++-- 5 files changed, 53 insertions(+), 23 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 461ede0c5..140521ee3 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -94,13 +94,13 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); // v2 routes app.use('/api/v2/workspace', v2WorkspaceRouter); app.use('/api/v2/secret', v2SecretRouter); -app.use('/api/v2/service-token-data', v2ServiceTokenDataRouter); +app.use('/api/v2/service-token', v2ServiceTokenDataRouter); app.use('/api/v2/api-key-data', v2APIKeyDataRouter); //* Handle unrouted requests and respond with proper error message as well as status code -app.use((req, res, next)=>{ - if(res.headersSent) return next(); - next(RouteNotFoundError({message: `The requested source '(${req.method})${req.url}' was not found`})) +app.use((req, res, next) => { + if (res.headersSent) return next(); + next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` })) }) //* Error Handling Middleware (must be after all routing logic) diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts index e29a8d19b..472859de9 100644 --- a/backend/src/controllers/v2/secretController.ts +++ b/backend/src/controllers/v2/secretController.ts @@ -6,6 +6,7 @@ import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCre const { ValidationError } = mongoose.Error; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { AnyBulkWriteOperation } from 'mongodb'; +import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; export const batchCreateSecrets = async (req: Request, res: Response) => { const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; @@ -135,4 +136,33 @@ export const batchModifySecrets = async (req: Request, res: Response) => { } return res.status(200).send() +} + +export const fetchAllSecrets = async (req: Request, res: Response) => { + const { environment } = req.query; + const { workspaceId } = req.params; + + let userId: string | undefined = undefined // Used for choosing the personal secrets to fetch in + if (req.user) { + userId = req.user._id.toString(); + } + + if (req.serviceTokenData) { + userId = req.serviceTokenData.user._id + } + + const [retriveAllSecretsError, allSecrets] = await to(Secret.find( + { + workspace: workspaceId, + environment, + $or: [{ user: userId }, { user: { $exists: false } }], + type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + } + ).then()) + + if (retriveAllSecretsError instanceof ValidationError) { + throw RouteValidationError({ message: "Unable to get secrets, please try again", stack: retriveAllSecretsError.stack }) + } + + return res.json(allSecrets) } \ No newline at end of file diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index fb8fd99e0..54317690d 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -6,7 +6,7 @@ import { MembershipOrg, Integration, IntegrationAuth, - Key, + Key, IUser, ServiceToken, ServiceTokenData @@ -78,7 +78,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { workspaceId, keys }); - + if (postHogClient) { postHogClient.capture({ event: 'secrets pushed', @@ -125,7 +125,7 @@ export const pullSecrets = async (req: Request, res: Response) => { const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; - + let userId; if (req.user) { userId = req.user._id.toString(); @@ -138,7 +138,7 @@ export const pullSecrets = async (req: Request, res: Response) => { workspaceId, environment }); - + if (channel !== 'cli') { secrets = reformatPullSecrets({ secrets }); } @@ -178,7 +178,7 @@ export const getWorkspaceKey = async (req: Request, res: Response) => { workspace: workspaceId, receiver: req.user._id }).populate('sender', '+publicKey'); - + if (!key) throw new Error('Failed to find workspace key'); } catch (err) { Sentry.setUser({ email: req.user.email }); @@ -188,9 +188,7 @@ export const getWorkspaceKey = async (req: Request, res: Response) => { }); } - return res.status(200).send({ - key - }); + return res.status(200).json(key); } export const getWorkspaceServiceTokenData = async ( req: Request, @@ -213,7 +211,7 @@ export const getWorkspaceServiceTokenData = async ( message: 'Failed to get workspace service token data' }); } - + return res.status(200).send({ serviceTokenData }); diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 83108823f..95ce3e6b0 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,9 +1,10 @@ -import express from 'express'; +import express, { Request, Response } from 'express'; import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware'; -import { body, param } from 'express-validator'; +import { body, param, query } from 'express-validator'; import { ADMIN, MEMBER } from '../../variables'; import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret/types'; import { secretController } from '../../controllers/v2'; +import { fetchAllSecrets } from '../../controllers/v2/secretController'; const router = express.Router(); @@ -26,19 +27,20 @@ router.post( ); /** - * Get a single secret by secret id + * Get all secrets for a given environment and workspace id */ router.get( - '/:secretId', + '/workspace/:workspaceId', + param('workspaceId').exists().trim(), + query("environment").exists(), requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'serviceToken'] }), - param('secretId').exists().trim(), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER] }), validateRequest, - secretController.createSingleSecret + fetchAllSecrets ); /** diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index eaa8e58a3..c90834d6d 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -42,15 +42,15 @@ router.get( ); router.get( - '/:workspaceId/key', + '/:workspaceId/encrypted-key', requireAuth({ acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER] - }), + }), param('workspaceId').exists().trim(), - validateRequest, + validateRequest, workspaceController.getWorkspaceKey ); From 5428766bf677f39778a3f381b5a6d919a544b645 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 20:17:11 -0500 Subject: [PATCH 23/23] modify getServiceTokenData to return single json --- .../v2/serviceTokenDataController.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index d8f4d4eea..83c8fea94 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -15,9 +15,7 @@ import { * @param res * @returns */ -export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).send({ - serviceTokenData: req.serviceTokenData -}); +export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).json(req.serviceTokenData); /** * Create new service token data for workspace with id [workspaceId] and @@ -29,9 +27,9 @@ export const getServiceTokenData = async (req: Request, res: Response) => res.st export const createServiceTokenData = async (req: Request, res: Response) => { let serviceToken, serviceTokenData; try { - const { + const { name, - workspaceId, + workspaceId, environment, encryptedKey, iv, @@ -41,10 +39,10 @@ export const createServiceTokenData = async (req: Request, res: Response) => { const secret = crypto.randomBytes(16).toString('hex'); const secretHash = await bcrypt.hash(secret, SALT_ROUNDS); - - const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - + + const expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + serviceTokenData = await new ServiceTokenData({ name, workspace: workspaceId, @@ -56,12 +54,12 @@ export const createServiceTokenData = async (req: Request, res: Response) => { iv, tag }).save(); - + // return service token data without sensitive data serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); - + if (!serviceTokenData) throw new Error('Failed to find service token data'); - + serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; } catch (err) { @@ -90,7 +88,7 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { const { serviceTokenDataId } = req.params; serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); - + } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); @@ -98,7 +96,7 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { message: 'Failed to delete service token data' }); } - + return res.status(200).send({ serviceTokenData });