From ff0e7feeee3f0787491267ad8ea7945e802e4a8d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 26 Jul 2024 19:14:21 +0200 Subject: [PATCH 01/41] feat(cli): CLI Keyring improvements --- cli/packages/cmd/vault.go | 47 +++++++++++++++++++++++++++-- cli/packages/models/cli.go | 9 +++--- cli/packages/util/config.go | 18 ++++++++--- cli/packages/util/constants.go | 4 +++ cli/packages/util/keyringwrapper.go | 35 ++++++++++++++++++--- cli/packages/util/vault.go | 6 ++-- 6 files changed, 101 insertions(+), 18 deletions(-) diff --git a/cli/packages/cmd/vault.go b/cli/packages/cmd/vault.go index 01bee147b..948667dc4 100644 --- a/cli/packages/cmd/vault.go +++ b/cli/packages/cmd/vault.go @@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc. package cmd import ( + "encoding/base64" "fmt" "strings" @@ -16,10 +17,49 @@ import ( var AvailableVaultsAndDescriptions = []string{"auto (automatically select native vault on system)", "file (encrypted file vault)"} var AvailableVaults = []string{"auto", "file"} +var vaultSetPassphraseCmd = &cobra.Command{ + Example: `infisical vault set-passphrase [your-passphrase]`, + Use: "set-passphrase [your-passphrase]", + Short: "Used to set the passphrase for the file vault", + DisableFlagsInUseLine: true, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + log.Error().Msgf("Please provide a passphrase to set for the file vault") + return + } + + passphrase := args[0] + + configFile, err := util.GetConfigFile() + if err != nil { + log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err) + return + } + + if configFile.VaultBackendType != "file" { + log.Error().Msgf("You are not using file vault to store your login details. You can only set passphrase for file vault") + return + } + + // encode with base64 + encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase)) + configFile.VaultBackendPassphrase = encodedPassphrase + + err = util.WriteConfigFile(&configFile) + if err != nil { + log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err) + return + } + + fmt.Printf("\nSuccessfully, set passphrase for file vault. You can now store your login details securely at rest\n") + }, +} + var vaultSetCmd = &cobra.Command{ - Example: `infisical vault set pass`, - Use: "set [vault-name]", - Short: "Used to set the vault backend to store your login details securely at rest", + Example: `infisical vault set [file|auto]`, + Use: "set [file|auto]", + Short: "Used to set the type of vault backend to store your login details securely at rest", DisableFlagsInUseLine: true, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { @@ -89,5 +129,6 @@ func printAvailableVaultBackends() { func init() { vaultCmd.AddCommand(vaultSetCmd) + vaultCmd.AddCommand(vaultSetPassphraseCmd) rootCmd.AddCommand(vaultCmd) } diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 4b02cb6f8..65404dafa 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -11,10 +11,11 @@ type UserCredentials struct { // The file struct for Infisical config file type ConfigFile struct { - LoggedInUserEmail string `json:"loggedInUserEmail"` - LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"` - LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"` - VaultBackendType string `json:"vaultBackendType,omitempty"` + LoggedInUserEmail string `json:"loggedInUserEmail"` + LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"` + LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"` + VaultBackendType string `json:"vaultBackendType,omitempty"` + VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"` } type LoggedInUser struct { diff --git a/cli/packages/util/config.go b/cli/packages/util/config.go index 55c9df1b0..02030e1fa 100644 --- a/cli/packages/util/config.go +++ b/cli/packages/util/config.go @@ -1,6 +1,7 @@ package util import ( + "encoding/base64" "encoding/json" "errors" "fmt" @@ -50,10 +51,11 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error { } configFile := models.ConfigFile{ - LoggedInUserEmail: userCredentials.Email, - LoggedInUserDomain: config.INFISICAL_URL, - LoggedInUsers: existingConfigFile.LoggedInUsers, - VaultBackendType: existingConfigFile.VaultBackendType, + LoggedInUserEmail: userCredentials.Email, + LoggedInUserDomain: config.INFISICAL_URL, + LoggedInUsers: existingConfigFile.LoggedInUsers, + VaultBackendType: existingConfigFile.VaultBackendType, + VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase, } configFileMarshalled, err := json.Marshal(configFile) @@ -215,6 +217,14 @@ func GetConfigFile() (models.ConfigFile, error) { return models.ConfigFile{}, err } + if configFile.VaultBackendPassphrase != "" { + decodedPassphrase, err := base64.StdEncoding.DecodeString(configFile.VaultBackendPassphrase) + if err != nil { + return models.ConfigFile{}, fmt.Errorf("GetConfigFile: Unable to decode base64 passphrase [err=%s]", err) + } + os.Setenv("INFISICAL_VAULT_FILE_PASSPHRASE", string(decodedPassphrase)) + } + return configFile, nil } diff --git a/cli/packages/util/constants.go b/cli/packages/util/constants.go index 5b0a93513..5cd66f50b 100644 --- a/cli/packages/util/constants.go +++ b/cli/packages/util/constants.go @@ -8,6 +8,10 @@ const ( INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN" + INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase. + + VAULT_BACKEND_AUTO_MODE = "auto" + VAULT_BACKEND_FILE_MODE = "file" // Universal Auth INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID" diff --git a/cli/packages/util/keyringwrapper.go b/cli/packages/util/keyringwrapper.go index 3bf2dd6c4..4f8fd80d4 100644 --- a/cli/packages/util/keyringwrapper.go +++ b/cli/packages/util/keyringwrapper.go @@ -1,11 +1,18 @@ package util import ( + "strings" + + "github.com/fatih/color" "github.com/zalando/go-keyring" ) const MAIN_KEYRING_SERVICE = "infisical-cli" +func keyringNotConfigured(err error) bool { + return err != nil && strings.Contains(err.Error(), "was not provided by any .service files") +} + type TimeoutError struct { message string } @@ -20,16 +27,30 @@ func SetValueInKeyring(key, value string) error { PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again") } - return keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value) + err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value) + + if err == keyring.ErrUnsupportedPlatform || keyringNotConfigured(err) { + boldGreen := color.New(color.FgGreen).Add(color.Bold) + boldGreen.Printf("Warning: Fallback file keyring is being used") + err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value) + } + + return err } func GetValueInKeyring(key string) (string, error) { currentVaultBackend, err := GetCurrentVaultBackend() if err != nil { - PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again") + PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical reset] then try again") } - return keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key) + value, err := keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key) + + if err == keyring.ErrUnsupportedPlatform || keyringNotConfigured(err) { + value, err = keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key) + } + return value, err + } func DeleteValueInKeyring(key string) error { @@ -38,5 +59,11 @@ func DeleteValueInKeyring(key string) error { return err } - return keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key) + err = keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key) + + if err == keyring.ErrUnsupportedPlatform || keyringNotConfigured(err) { + err = keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key) + } + + return err } diff --git a/cli/packages/util/vault.go b/cli/packages/util/vault.go index 14d6d10d9..5907d93fc 100644 --- a/cli/packages/util/vault.go +++ b/cli/packages/util/vault.go @@ -11,11 +11,11 @@ func GetCurrentVaultBackend() (string, error) { } if configFile.VaultBackendType == "" { - return "auto", nil + return VAULT_BACKEND_AUTO_MODE, nil } - if configFile.VaultBackendType != "auto" && configFile.VaultBackendType != "file" { - return "auto", nil + if configFile.VaultBackendType != VAULT_BACKEND_AUTO_MODE && configFile.VaultBackendType != VAULT_BACKEND_FILE_MODE { + return VAULT_BACKEND_AUTO_MODE, nil } return configFile.VaultBackendType, nil From c3038e3ca17221062a216581aaf59dbf9cd7b6a9 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 26 Jul 2024 22:47:07 +0200 Subject: [PATCH 02/41] docs: passphrase command --- docs/cli/commands/vault.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli/commands/vault.mdx b/docs/cli/commands/vault.mdx index 9030c580c..6ffc24b21 100644 --- a/docs/cli/commands/vault.mdx +++ b/docs/cli/commands/vault.mdx @@ -32,6 +32,6 @@ description: "Change the vault type in Infisical" To safeguard your login details when using the CLI, Infisical places them in a system vault or an encrypted text file, protected by a passphrase that only the user knows. -To avoid constantly entering your passphrase when using the `file` vault type, set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable with your password in your shell +To avoid constantly entering your passphrase when using the `file` vault type, use the `infisical vault set file passphrase` CLI command to specify your password once. From e619cfa31328cf0d13c261c24419a6c115e2f97b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 26 Jul 2024 22:47:37 +0200 Subject: [PATCH 03/41] feat(cli): set persistent file vault password --- cli/packages/cmd/vault.go | 194 +++++++++++++++++++++++++------------- 1 file changed, 126 insertions(+), 68 deletions(-) diff --git a/cli/packages/cmd/vault.go b/cli/packages/cmd/vault.go index 948667dc4..187316a40 100644 --- a/cli/packages/cmd/vault.go +++ b/cli/packages/cmd/vault.go @@ -9,97 +9,88 @@ import ( "strings" "github.com/Infisical/infisical-merge/packages/util" + "github.com/manifoldco/promptui" "github.com/posthog/posthog-go" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) -var AvailableVaultsAndDescriptions = []string{"auto (automatically select native vault on system)", "file (encrypted file vault)"} -var AvailableVaults = []string{"auto", "file"} +type VaultBackendType struct { + Name string + Description string +} -var vaultSetPassphraseCmd = &cobra.Command{ - Example: `infisical vault set-passphrase [your-passphrase]`, - Use: "set-passphrase [your-passphrase]", - Short: "Used to set the passphrase for the file vault", - DisableFlagsInUseLine: true, - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - if len(args) != 1 { - log.Error().Msgf("Please provide a passphrase to set for the file vault") - return - } - - passphrase := args[0] - - configFile, err := util.GetConfigFile() - if err != nil { - log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err) - return - } - - if configFile.VaultBackendType != "file" { - log.Error().Msgf("You are not using file vault to store your login details. You can only set passphrase for file vault") - return - } - - // encode with base64 - encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase)) - configFile.VaultBackendPassphrase = encodedPassphrase - - err = util.WriteConfigFile(&configFile) - if err != nil { - log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err) - return - } - - fmt.Printf("\nSuccessfully, set passphrase for file vault. You can now store your login details securely at rest\n") +var AvailableVaults = []VaultBackendType{ + { + Name: "auto", + Description: "automatically select native vault on system", + }, + { + Name: "file", + Description: "encrypted file vault", }, } var vaultSetCmd = &cobra.Command{ - Example: `infisical vault set [file|auto]`, - Use: "set [file|auto]", + Example: `infisical vault set [file|auto] [option]`, + Use: "set [file|auto] [option]", Short: "Used to set the type of vault backend to store your login details securely at rest", + Long: "Used to set the type of vault backend to store your login details securely at rest", DisableFlagsInUseLine: true, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - wantedVaultTypeName := args[0] - currentVaultBackend, err := util.GetCurrentVaultBackend() - if err != nil { - log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) - return - } - if wantedVaultTypeName == string(currentVaultBackend) { - log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend) - return - } + if len(args) >= 2 { + vaultType := args[0] + option := args[1] - if wantedVaultTypeName == "auto" || wantedVaultTypeName == "file" { - configFile, err := util.GetConfigFile() - if err != nil { - log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) + // Todo, add more vault types / configurations + if vaultType != util.VAULT_BACKEND_FILE_MODE { + log.Error().Msgf("No configuration options are available for vault type [%s]\n", vaultType) return } - configFile.VaultBackendType = wantedVaultTypeName // save selected vault - configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login + switch option { + case "passphrase": + { - err = util.WriteConfigFile(&configFile) - if err != nil { - log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err) - return + passphrasePrompt := promptui.Prompt{ + Label: "File vault passphrase", + } + + passphrase, err := passphrasePrompt.Run() + if err != nil { + log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err) + return + } + + if passphrase == "" || len(passphrase) < 8 { + log.Error().Msgf("Passphrase must be at least 8 characters long") + return + } + setFileVaultPassphrase(passphrase) + } + default: + log.Error().Msgf("Unknown option [%s] for vault set command", option) } - fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName) - - Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION)) - } else { - log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(AvailableVaults, ", ")) + return } + + fmt.Printf("Warning: This command has been deprecated. Please use 'infisical vault use [file|auto]' to select which vault to use.\n") + selectVaultTypeCmd(cmd, args) }, } +var vaultUseCmd = &cobra.Command{ + Example: `infisical vault use [file|auto]`, + Use: "use [file|auto]", + Short: "Used to set the type of vault backend to store your login details securely at rest", + DisableFlagsInUseLine: true, + Args: cobra.MinimumNArgs(1), + Run: selectVaultTypeCmd, +} + // runCmd represents the run command var vaultCmd = &cobra.Command{ Use: "vault", @@ -111,10 +102,35 @@ var vaultCmd = &cobra.Command{ }, } +func setFileVaultPassphrase(passphrase string) { + configFile, err := util.GetConfigFile() + if err != nil { + log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err) + return + } + + if configFile.VaultBackendType != "file" { + log.Error().Msgf("You are not using file vault to store your login details. You can only set passphrase for file vault") + return + } + + // encode with base64 + encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase)) + configFile.VaultBackendPassphrase = encodedPassphrase + + err = util.WriteConfigFile(&configFile) + if err != nil { + log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err) + return + } + + fmt.Printf("\nSuccessfully, set passphrase for file vault. You can now store your login details securely at rest\n") +} + func printAvailableVaultBackends() { fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:") - for _, backend := range AvailableVaultsAndDescriptions { - fmt.Printf("\n- %s", backend) + for _, vaultType := range AvailableVaults { + fmt.Printf("\n- %s (%s)", vaultType.Name, vaultType.Description) } currentVaultBackend, err := util.GetCurrentVaultBackend() @@ -127,8 +143,50 @@ func printAvailableVaultBackends() { fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials\n", string(currentVaultBackend)) } +func selectVaultTypeCmd(cmd *cobra.Command, args []string) { + wantedVaultTypeName := args[0] + currentVaultBackend, err := util.GetCurrentVaultBackend() + if err != nil { + log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) + return + } + + if wantedVaultTypeName == string(currentVaultBackend) { + log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend) + return + } + + if wantedVaultTypeName == util.VAULT_BACKEND_AUTO_MODE || wantedVaultTypeName == util.VAULT_BACKEND_FILE_MODE { + configFile, err := util.GetConfigFile() + if err != nil { + log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) + return + } + + configFile.VaultBackendType = wantedVaultTypeName // save selected vault + configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login + + err = util.WriteConfigFile(&configFile) + if err != nil { + log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err) + return + } + + fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName) + + Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION)) + } else { + var availableVaultsNames []string + for _, vault := range AvailableVaults { + availableVaultsNames = append(availableVaultsNames, vault.Name) + } + log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(availableVaultsNames, ", ")) + } +} + func init() { vaultCmd.AddCommand(vaultSetCmd) - vaultCmd.AddCommand(vaultSetPassphraseCmd) + vaultCmd.AddCommand(vaultUseCmd) + rootCmd.AddCommand(vaultCmd) } From 070eb2aacd75c80a39c85192075f0c0ebd2f343d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 26 Jul 2024 22:47:46 +0200 Subject: [PATCH 04/41] Update keyringwrapper.go --- cli/packages/util/keyringwrapper.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/packages/util/keyringwrapper.go b/cli/packages/util/keyringwrapper.go index 4f8fd80d4..62133616d 100644 --- a/cli/packages/util/keyringwrapper.go +++ b/cli/packages/util/keyringwrapper.go @@ -30,8 +30,10 @@ func SetValueInKeyring(key, value string) error { err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value) if err == keyring.ErrUnsupportedPlatform || keyringNotConfigured(err) { - boldGreen := color.New(color.FgGreen).Add(color.Bold) - boldGreen.Printf("Warning: Fallback file keyring is being used") + boldYellow := color.New(color.FgYellow).Add(color.Bold) + boldYellow.Printf("Warning: Fallback file keyring is being used\n\n") + boldYellow.Printf("You can persist your file passphrase by running the following command:\n") + boldYellow.Printf("infisical vault set file passphrase \n\n") err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value) } From 2177ec6bcc4d3aa78d5ff403f2dd6a69e20545fb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 29 Jul 2024 12:04:34 +0200 Subject: [PATCH 05/41] Update vault.go --- cli/packages/cmd/vault.go | 54 +++++++++++---------------------------- 1 file changed, 15 insertions(+), 39 deletions(-) diff --git a/cli/packages/cmd/vault.go b/cli/packages/cmd/vault.go index 187316a40..91f9313da 100644 --- a/cli/packages/cmd/vault.go +++ b/cli/packages/cmd/vault.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/Infisical/infisical-merge/packages/util" - "github.com/manifoldco/promptui" "github.com/posthog/posthog-go" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -23,7 +22,7 @@ type VaultBackendType struct { var AvailableVaults = []VaultBackendType{ { Name: "auto", - Description: "automatically select native vault on system", + Description: "automatically select the system keyring", }, { Name: "file", @@ -32,52 +31,26 @@ var AvailableVaults = []VaultBackendType{ } var vaultSetCmd = &cobra.Command{ - Example: `infisical vault set [file|auto] [option]`, + Example: `infisical vault set [file|auto]`, Use: "set [file|auto] [option]", - Short: "Used to set the type of vault backend to store your login details securely at rest", - Long: "Used to set the type of vault backend to store your login details securely at rest", + Short: "Used to set the type of vault backend to store sensitive data securely at rest", DisableFlagsInUseLine: true, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - if len(args) >= 2 { - vaultType := args[0] - option := args[1] + vaultType := args[0] - // Todo, add more vault types / configurations - if vaultType != util.VAULT_BACKEND_FILE_MODE { - log.Error().Msgf("No configuration options are available for vault type [%s]\n", vaultType) - return - } - - switch option { - case "passphrase": - { - - passphrasePrompt := promptui.Prompt{ - Label: "File vault passphrase", - } - - passphrase, err := passphrasePrompt.Run() - if err != nil { - log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err) - return - } - - if passphrase == "" || len(passphrase) < 8 { - log.Error().Msgf("Passphrase must be at least 8 characters long") - return - } - setFileVaultPassphrase(passphrase) - } - default: - log.Error().Msgf("Unknown option [%s] for vault set command", option) - } + passphrase, err := cmd.Flags().GetString("passphrase") + if err != nil { + util.HandleError(err, "Unable to get passphrase flag") + } + if vaultType == util.VAULT_BACKEND_FILE_MODE && passphrase != "" { + setFileVaultPassphrase(passphrase) return } - fmt.Printf("Warning: This command has been deprecated. Please use 'infisical vault use [file|auto]' to select which vault to use.\n") + util.PrintWarning("This command has been deprecated. Please use 'infisical vault use [file|auto]' to select which vault to use.\n") selectVaultTypeCmd(cmd, args) }, } @@ -110,7 +83,7 @@ func setFileVaultPassphrase(passphrase string) { } if configFile.VaultBackendType != "file" { - log.Error().Msgf("You are not using file vault to store your login details. You can only set passphrase for file vault") + log.Error().Msgf("You are not using file vault to store your login details. You can only set passphrase for file vault. Use 'infisical vault use file' to switch to file vault") return } @@ -185,6 +158,9 @@ func selectVaultTypeCmd(cmd *cobra.Command, args []string) { } func init() { + + vaultSetCmd.Flags().StringP("passphrase", "p", "", "Set the passphrase for the file vault") + vaultCmd.AddCommand(vaultSetCmd) vaultCmd.AddCommand(vaultUseCmd) From 3d380710ee06e914b1ebb4611bec1938f9d6051a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 29 Jul 2024 12:10:42 +0200 Subject: [PATCH 06/41] Update keyringwrapper.go --- cli/packages/util/keyringwrapper.go | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/cli/packages/util/keyringwrapper.go b/cli/packages/util/keyringwrapper.go index 62133616d..60da5f6c7 100644 --- a/cli/packages/util/keyringwrapper.go +++ b/cli/packages/util/keyringwrapper.go @@ -1,18 +1,11 @@ package util import ( - "strings" - - "github.com/fatih/color" "github.com/zalando/go-keyring" ) const MAIN_KEYRING_SERVICE = "infisical-cli" -func keyringNotConfigured(err error) bool { - return err != nil && strings.Contains(err.Error(), "was not provided by any .service files") -} - type TimeoutError struct { message string } @@ -29,11 +22,9 @@ func SetValueInKeyring(key, value string) error { err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value) - if err == keyring.ErrUnsupportedPlatform || keyringNotConfigured(err) { - boldYellow := color.New(color.FgYellow).Add(color.Bold) - boldYellow.Printf("Warning: Fallback file keyring is being used\n\n") - boldYellow.Printf("You can persist your file passphrase by running the following command:\n") - boldYellow.Printf("infisical vault set file passphrase \n\n") + if err != nil { + + PrintWarning("Fallback file keyring is being used\n\nYou can persist your file passphrase by running the following command:\ninfisical vault set file passphrase \n") err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value) } @@ -48,7 +39,7 @@ func GetValueInKeyring(key string) (string, error) { value, err := keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key) - if err == keyring.ErrUnsupportedPlatform || keyringNotConfigured(err) { + if err != nil { value, err = keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key) } return value, err @@ -63,7 +54,7 @@ func DeleteValueInKeyring(key string) error { err = keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key) - if err == keyring.ErrUnsupportedPlatform || keyringNotConfigured(err) { + if err != nil { err = keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key) } From a9f04a3c1f4854463494dfb0b6647e13d40016b2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 29 Jul 2024 12:13:40 +0200 Subject: [PATCH 07/41] Update keyringwrapper.go --- cli/packages/util/keyringwrapper.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cli/packages/util/keyringwrapper.go b/cli/packages/util/keyringwrapper.go index 60da5f6c7..a2042e289 100644 --- a/cli/packages/util/keyringwrapper.go +++ b/cli/packages/util/keyringwrapper.go @@ -23,7 +23,6 @@ func SetValueInKeyring(key, value string) error { err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value) if err != nil { - PrintWarning("Fallback file keyring is being used\n\nYou can persist your file passphrase by running the following command:\ninfisical vault set file passphrase \n") err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value) } @@ -40,7 +39,7 @@ func GetValueInKeyring(key string) (string, error) { value, err := keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key) if err != nil { - value, err = keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key) + value, err = keyring.Get(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key) } return value, err @@ -55,7 +54,7 @@ func DeleteValueInKeyring(key string) error { err = keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key) if err != nil { - err = keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key) + err = keyring.Delete(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key) } return err From e7a95e6af201d7b3c00cc406cd505d598b2d2be9 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 29 Jul 2024 12:15:53 +0200 Subject: [PATCH 08/41] Update login.go --- cli/packages/cmd/login.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index 4717e0784..7996d0288 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -728,6 +728,8 @@ func askToPasteJwtToken(success chan models.UserCredentials, failure chan error) infisicalPastedToken := strings.TrimSpace(string(bytePassword)) + fmt.Printf("\n\nToken 1: %s\n", infisicalPastedToken) + userCredentials, err := decodePastedBase64Token(infisicalPastedToken) if err != nil { failure <- err @@ -742,6 +744,8 @@ func askToPasteJwtToken(success chan models.UserCredentials, failure chan error) isAuthenticated := api.CallIsAuthenticated(httpClient) if !isAuthenticated { + fmt.Printf("\n\nToken 2: %s\n", infisicalPastedToken) + fmt.Println("Invalid user credentials provided", err) failure <- err os.Exit(1) From 4249ec603070b08ce068e85c90c7bb3cad867eb3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 29 Jul 2024 12:21:31 +0200 Subject: [PATCH 09/41] Update login.go --- cli/packages/cmd/login.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index 7996d0288..4717e0784 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -728,8 +728,6 @@ func askToPasteJwtToken(success chan models.UserCredentials, failure chan error) infisicalPastedToken := strings.TrimSpace(string(bytePassword)) - fmt.Printf("\n\nToken 1: %s\n", infisicalPastedToken) - userCredentials, err := decodePastedBase64Token(infisicalPastedToken) if err != nil { failure <- err @@ -744,8 +742,6 @@ func askToPasteJwtToken(success chan models.UserCredentials, failure chan error) isAuthenticated := api.CallIsAuthenticated(httpClient) if !isAuthenticated { - fmt.Printf("\n\nToken 2: %s\n", infisicalPastedToken) - fmt.Println("Invalid user credentials provided", err) failure <- err os.Exit(1) From 91cee20cc8ebde7e9a135d996abf5ee0a159b075 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 29 Jul 2024 12:21:38 +0200 Subject: [PATCH 10/41] Minor improvemnets --- cli/packages/cmd/vault.go | 7 +------ cli/packages/util/keyringwrapper.go | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/cli/packages/cmd/vault.go b/cli/packages/cmd/vault.go index 91f9313da..0e7dafe19 100644 --- a/cli/packages/cmd/vault.go +++ b/cli/packages/cmd/vault.go @@ -82,11 +82,6 @@ func setFileVaultPassphrase(passphrase string) { return } - if configFile.VaultBackendType != "file" { - log.Error().Msgf("You are not using file vault to store your login details. You can only set passphrase for file vault. Use 'infisical vault use file' to switch to file vault") - return - } - // encode with base64 encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase)) configFile.VaultBackendPassphrase = encodedPassphrase @@ -97,7 +92,7 @@ func setFileVaultPassphrase(passphrase string) { return } - fmt.Printf("\nSuccessfully, set passphrase for file vault. You can now store your login details securely at rest\n") + util.PrintSuccessMessage("\nSuccessfully, set passphrase for file vault.\n") } func printAvailableVaultBackends() { diff --git a/cli/packages/util/keyringwrapper.go b/cli/packages/util/keyringwrapper.go index a2042e289..a1b823765 100644 --- a/cli/packages/util/keyringwrapper.go +++ b/cli/packages/util/keyringwrapper.go @@ -23,7 +23,7 @@ func SetValueInKeyring(key, value string) error { err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value) if err != nil { - PrintWarning("Fallback file keyring is being used\n\nYou can persist your file passphrase by running the following command:\ninfisical vault set file passphrase \n") + PrintWarning("Fallback file keyring is being used\n\nYou can persist your file passphrase by running the following command:\ninfisical vault set file --passphrase \n") err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value) } From 2b630f75aa0250e1a79d1ef9f0780e77311d8e80 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 29 Jul 2024 12:31:02 +0200 Subject: [PATCH 11/41] Update keyringwrapper.go --- cli/packages/util/keyringwrapper.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/cli/packages/util/keyringwrapper.go b/cli/packages/util/keyringwrapper.go index a1b823765..66d60dac3 100644 --- a/cli/packages/util/keyringwrapper.go +++ b/cli/packages/util/keyringwrapper.go @@ -1,7 +1,12 @@ package util import ( + "encoding/base64" + "fmt" + "os" + "github.com/zalando/go-keyring" + "golang.org/x/term" ) const MAIN_KEYRING_SERVICE = "infisical-cli" @@ -23,7 +28,26 @@ func SetValueInKeyring(key, value string) error { err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value) if err != nil { + configFile, _ := GetConfigFile() PrintWarning("Fallback file keyring is being used\n\nYou can persist your file passphrase by running the following command:\ninfisical vault set file --passphrase \n") + + if configFile.VaultBackendPassphrase == "" { + fmt.Print("\n\nEnter the passphrase to use for keyring encryption: ") + bytePassphrase, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return err + } + encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(string(bytePassphrase))) + configFile.VaultBackendPassphrase = encodedPassphrase + err = WriteConfigFile(&configFile) + if err != nil { + return err + } + + // We call this function at last to trigger the environment variable to be set + GetConfigFile() + } + err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value) } From 8777cfe6800281bcc160da550c1100f95ce8c11f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 29 Jul 2024 12:34:35 +0200 Subject: [PATCH 12/41] Update keyringwrapper.go --- cli/packages/util/keyringwrapper.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cli/packages/util/keyringwrapper.go b/cli/packages/util/keyringwrapper.go index 66d60dac3..a24a736c5 100644 --- a/cli/packages/util/keyringwrapper.go +++ b/cli/packages/util/keyringwrapper.go @@ -2,11 +2,9 @@ package util import ( "encoding/base64" - "fmt" - "os" + "github.com/manifoldco/promptui" "github.com/zalando/go-keyring" - "golang.org/x/term" ) const MAIN_KEYRING_SERVICE = "infisical-cli" @@ -32,12 +30,15 @@ func SetValueInKeyring(key, value string) error { PrintWarning("Fallback file keyring is being used\n\nYou can persist your file passphrase by running the following command:\ninfisical vault set file --passphrase \n") if configFile.VaultBackendPassphrase == "" { - fmt.Print("\n\nEnter the passphrase to use for keyring encryption: ") - bytePassphrase, err := term.ReadPassword(int(os.Stdin.Fd())) + passphrasePrompt := promptui.Prompt{ + Label: "\nEnter the passphrase to use for keyring encryption: ", + } + passphrase, err := passphrasePrompt.Run() if err != nil { return err } - encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(string(bytePassphrase))) + + encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase)) configFile.VaultBackendPassphrase = encodedPassphrase err = WriteConfigFile(&configFile) if err != nil { From 879ef2c178e557545b56d54e58e1d37c314cf103 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 29 Jul 2024 12:37:58 +0200 Subject: [PATCH 13/41] Update keyringwrapper.go --- cli/packages/util/keyringwrapper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/packages/util/keyringwrapper.go b/cli/packages/util/keyringwrapper.go index a24a736c5..522e3aa8c 100644 --- a/cli/packages/util/keyringwrapper.go +++ b/cli/packages/util/keyringwrapper.go @@ -31,7 +31,7 @@ func SetValueInKeyring(key, value string) error { if configFile.VaultBackendPassphrase == "" { passphrasePrompt := promptui.Prompt{ - Label: "\nEnter the passphrase to use for keyring encryption: ", + Label: "Enter the passphrase to use for keyring encryption: ", } passphrase, err := passphrasePrompt.Run() if err != nil { From 85653a90d59a60a539068cfa59b21a098fa56c7f Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 29 Jul 2024 22:06:03 -0400 Subject: [PATCH 14/41] update phrasing --- cli/packages/util/keyringwrapper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/packages/util/keyringwrapper.go b/cli/packages/util/keyringwrapper.go index 522e3aa8c..c1df66587 100644 --- a/cli/packages/util/keyringwrapper.go +++ b/cli/packages/util/keyringwrapper.go @@ -27,7 +27,7 @@ func SetValueInKeyring(key, value string) error { if err != nil { configFile, _ := GetConfigFile() - PrintWarning("Fallback file keyring is being used\n\nYou can persist your file passphrase by running the following command:\ninfisical vault set file --passphrase \n") + PrintWarning("System keyring could not be used, switching to `file` vault for local token storage\n\nYou can persist your file vault passphrase by running the following command:\ninfisical vault set file --passphrase \n") if configFile.VaultBackendPassphrase == "" { passphrasePrompt := promptui.Prompt{ From d5f4ce43761336b56eae215bf7046daad82d719e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 30 Jul 2024 10:22:15 +0200 Subject: [PATCH 15/41] Update vault.go --- cli/packages/cmd/vault.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/packages/cmd/vault.go b/cli/packages/cmd/vault.go index 0e7dafe19..4720e094e 100644 --- a/cli/packages/cmd/vault.go +++ b/cli/packages/cmd/vault.go @@ -31,9 +31,9 @@ var AvailableVaults = []VaultBackendType{ } var vaultSetCmd = &cobra.Command{ - Example: `infisical vault set [file|auto]`, - Use: "set [file|auto] [option]", - Short: "Used to set the type of vault backend to store sensitive data securely at rest", + Example: `infisical vault set file --passphrase `, + Use: "set [file|auto] [flags]", + Short: "Used to configure the vault backends", DisableFlagsInUseLine: true, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { @@ -58,7 +58,7 @@ var vaultSetCmd = &cobra.Command{ var vaultUseCmd = &cobra.Command{ Example: `infisical vault use [file|auto]`, Use: "use [file|auto]", - Short: "Used to set the type of vault backend to store your login details securely at rest", + Short: "Used to select the the type of vault backend to store sensitive data securely at rest", DisableFlagsInUseLine: true, Args: cobra.MinimumNArgs(1), Run: selectVaultTypeCmd, From 02e8f20cbfa5205e6f8276d9eb6f496b54a1fd90 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 31 Jul 2024 03:14:06 +0000 Subject: [PATCH 16/41] remove extra : --- cli/packages/util/keyringwrapper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/packages/util/keyringwrapper.go b/cli/packages/util/keyringwrapper.go index c1df66587..5b6de96bc 100644 --- a/cli/packages/util/keyringwrapper.go +++ b/cli/packages/util/keyringwrapper.go @@ -31,7 +31,7 @@ func SetValueInKeyring(key, value string) error { if configFile.VaultBackendPassphrase == "" { passphrasePrompt := promptui.Prompt{ - Label: "Enter the passphrase to use for keyring encryption: ", + Label: "Enter the passphrase to use for keyring encryption", } passphrase, err := passphrasePrompt.Run() if err != nil { From 891cb06de09f8b97e5c06d010d74c6b6ee59c66b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 31 Jul 2024 16:55:53 +0200 Subject: [PATCH 17/41] Update keyringwrapper.go --- cli/packages/util/keyringwrapper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/packages/util/keyringwrapper.go b/cli/packages/util/keyringwrapper.go index 5b6de96bc..cadb72ebd 100644 --- a/cli/packages/util/keyringwrapper.go +++ b/cli/packages/util/keyringwrapper.go @@ -27,9 +27,9 @@ func SetValueInKeyring(key, value string) error { if err != nil { configFile, _ := GetConfigFile() - PrintWarning("System keyring could not be used, switching to `file` vault for local token storage\n\nYou can persist your file vault passphrase by running the following command:\ninfisical vault set file --passphrase \n") if configFile.VaultBackendPassphrase == "" { + PrintWarning("System keyring could not be used, falling back to `file` vault for sensitive data storage.") passphrasePrompt := promptui.Prompt{ Label: "Enter the passphrase to use for keyring encryption", } From 55a67407143e6c574798e395f708a9eb5c9c7e09 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 2 Aug 2024 21:37:48 +0800 Subject: [PATCH 18/41] misc: moved to plan-based rate limit --- backend/src/@types/fastify.d.ts | 2 ++ .../src/ee/services/license/licence-fns.ts | 12 ++++++- .../src/ee/services/license/license-types.ts | 3 ++ .../services/rate-limit/rate-limit-service.ts | 4 +-- .../services/rate-limit/rate-limit-types.ts | 11 ++++++ backend/src/server/config/rateLimiter.ts | 25 ++++++++----- .../src/server/plugins/inject-rate-limits.ts | 35 +++++++++++++++++++ backend/src/server/routes/index.ts | 2 ++ 8 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 backend/src/server/plugins/inject-rate-limits.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 8ae892560..00bc9e77c 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -18,6 +18,7 @@ import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-ser import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service"; import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service"; +import { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types"; import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; import { TScimServiceFactory } from "@app/ee/services/scim/scim-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; @@ -88,6 +89,7 @@ declare module "fastify" { id: string; orgId: string; }; + rateLimits: RateLimitConfiguration; // passport data passportUser: { isUserCompleted: string; diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index fc0fb54a7..c7bdd95ac 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -40,7 +40,17 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ secretRotation: true, caCrl: false, instanceUserManagement: false, - externalKms: false + externalKms: false, + rateLimits: { + readLimit: 60, + publicEndpointLimit: 30, + writeLimit: 200, + secretsLimit: 60, + authRateLimit: 60, + inviteUserRateLimit: 30, + mfaRateLimit: 20, + creationLimit: 30 + } }); export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index df85f008f..262547772 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -1,5 +1,7 @@ import { TOrgPermission } from "@app/lib/types"; +import { RateLimitConfiguration } from "../rate-limit/rate-limit-types"; + export enum InstanceType { OnPrem = "self-hosted", EnterpriseOnPrem = "enterprise-self-hosted", @@ -58,6 +60,7 @@ export type TFeatureSet = { caCrl: false; instanceUserManagement: false; externalKms: false; + rateLimits: RateLimitConfiguration; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/ee/services/rate-limit/rate-limit-service.ts b/backend/src/ee/services/rate-limit/rate-limit-service.ts index df90ca03f..b35b02dd4 100644 --- a/backend/src/ee/services/rate-limit/rate-limit-service.ts +++ b/backend/src/ee/services/rate-limit/rate-limit-service.ts @@ -4,9 +4,9 @@ import { logger } from "@app/lib/logger"; import { TLicenseServiceFactory } from "../license/license-service"; import { TRateLimitDALFactory } from "./rate-limit-dal"; -import { TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types"; +import { RateLimitConfiguration, TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types"; -let rateLimitMaxConfiguration = { +let rateLimitMaxConfiguration: RateLimitConfiguration = { readLimit: 60, publicEndpointLimit: 30, writeLimit: 200, diff --git a/backend/src/ee/services/rate-limit/rate-limit-types.ts b/backend/src/ee/services/rate-limit/rate-limit-types.ts index 19519aafb..7d5859367 100644 --- a/backend/src/ee/services/rate-limit/rate-limit-types.ts +++ b/backend/src/ee/services/rate-limit/rate-limit-types.ts @@ -14,3 +14,14 @@ export type TRateLimit = { createdAt: Date; updatedAt: Date; } & TRateLimitUpdateDTO; + +export type RateLimitConfiguration = { + readLimit: number; + publicEndpointLimit: number; + writeLimit: number; + secretsLimit: number; + authRateLimit: number; + inviteUserRateLimit: number; + mfaRateLimit: number; + creationLimit: number; +}; diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index 79b709ee6..cfadca9cc 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -1,7 +1,6 @@ import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit"; import { Redis } from "ioredis"; -import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service"; import { getConfig } from "@app/lib/config/env"; export const globalRateLimiterCfg = (): RateLimitPluginOptions => { @@ -22,14 +21,16 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => { // GET endpoints export const readLimit: RateLimitOptions = { timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().readLimit, + hook: "preValidation", + max: (req) => req.rateLimits.readLimit, keyGenerator: (req) => req.realIp }; // POST, PATCH, PUT, DELETE endpoints export const writeLimit: RateLimitOptions = { timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().writeLimit, + hook: "preValidation", + max: (req) => req.rateLimits.writeLimit, keyGenerator: (req) => req.realIp }; @@ -37,25 +38,29 @@ export const writeLimit: RateLimitOptions = { export const secretsLimit: RateLimitOptions = { // secrets, folders, secret imports timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().secretsLimit, + hook: "preValidation", + max: (req) => req.rateLimits.secretsLimit, keyGenerator: (req) => req.realIp }; export const authRateLimit: RateLimitOptions = { timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().authRateLimit, + hook: "preValidation", + max: (req) => req.rateLimits.authRateLimit, keyGenerator: (req) => req.realIp }; export const inviteUserRateLimit: RateLimitOptions = { timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().inviteUserRateLimit, + hook: "preValidation", + max: (req) => req.rateLimits.inviteUserRateLimit, keyGenerator: (req) => req.realIp }; export const mfaRateLimit: RateLimitOptions = { timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().mfaRateLimit, + hook: "preValidation", + max: (req) => req.rateLimits.mfaRateLimit, keyGenerator: (req) => { return req.headers.authorization?.split(" ")[1] || req.realIp; } @@ -64,7 +69,8 @@ export const mfaRateLimit: RateLimitOptions = { export const creationLimit: RateLimitOptions = { // identity, project, org timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().creationLimit, + hook: "preValidation", + max: (req) => req.rateLimits.creationLimit, keyGenerator: (req) => req.realIp }; @@ -72,7 +78,8 @@ export const creationLimit: RateLimitOptions = { export const publicEndpointLimit: RateLimitOptions = { // Read Shared Secrets timeWindow: 60 * 1000, - max: () => getRateLimiterConfig().publicEndpointLimit, + hook: "preValidation", + max: (req) => req.rateLimits.publicEndpointLimit, keyGenerator: (req) => req.realIp }; diff --git a/backend/src/server/plugins/inject-rate-limits.ts b/backend/src/server/plugins/inject-rate-limits.ts new file mode 100644 index 000000000..74acc8fcb --- /dev/null +++ b/backend/src/server/plugins/inject-rate-limits.ts @@ -0,0 +1,35 @@ +import fp from "fastify-plugin"; + +import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service"; + +export const injectRateLimits = fp(async (server) => { + server.decorateRequest("rateLimits", null); + server.addHook("onRequest", async (req) => { + const defaultRateLimiterConfig = getRateLimiterConfig(); + if (!req.auth) { + // for public endpoints + req.rateLimits = defaultRateLimiterConfig; + return; + } + + const plan = await server.services.license.getPlan(req.auth.orgId); + const { rateLimits } = plan; + + if (plan.customRateLimits) { + req.rateLimits = defaultRateLimiterConfig; + return; + } + + // we're using the null coalescing operator in order to handle outdated licenses + req.rateLimits = { + readLimit: rateLimits?.readLimit ?? defaultRateLimiterConfig.readLimit, + publicEndpointLimit: rateLimits?.publicEndpointLimit ?? defaultRateLimiterConfig.publicEndpointLimit, + writeLimit: rateLimits?.writeLimit ?? defaultRateLimiterConfig.writeLimit, + secretsLimit: rateLimits?.secretsLimit ?? defaultRateLimiterConfig.secretsLimit, + authRateLimit: rateLimits?.authRateLimit ?? defaultRateLimiterConfig.authRateLimit, + inviteUserRateLimit: rateLimits?.inviteUserRateLimit ?? defaultRateLimiterConfig.inviteUserRateLimit, + mfaRateLimit: rateLimits?.mfaRateLimit ?? defaultRateLimiterConfig.mfaRateLimit, + creationLimit: rateLimits?.creationLimit ?? defaultRateLimiterConfig.creationLimit + }; + }); +}); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index e8f80f020..d8c6b616d 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -183,6 +183,7 @@ import { webhookServiceFactory } from "@app/services/webhook/webhook-service"; import { injectAuditLogInfo } from "../plugins/audit-log"; import { injectIdentity } from "../plugins/auth/inject-identity"; import { injectPermission } from "../plugins/auth/inject-permission"; +import { injectRateLimits } from "../plugins/inject-rate-limits"; import { registerSecretScannerGhApp } from "../plugins/secret-scanner"; import { registerV1Routes } from "./v1"; import { registerV2Routes } from "./v2"; @@ -1130,6 +1131,7 @@ export const registerRoutes = async ( await server.register(injectIdentity, { userDAL, serviceTokenDAL }); await server.register(injectPermission); + await server.register(injectRateLimits); await server.register(injectAuditLogInfo); server.route({ From 13e0dd8e0feae856260f9a133a5fe9984f33ee93 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 2 Aug 2024 16:22:42 +0530 Subject: [PATCH 19/41] feat: completed org admin based project access feature --- backend/src/@types/fastify.d.ts | 2 + .../ee/services/audit-log/audit-log-types.ts | 16 +- backend/src/lib/knex/index.ts | 36 +++- backend/src/server/routes/index.ts | 14 +- backend/src/server/routes/v1/index.ts | 2 + .../src/server/routes/v1/org-admin-router.ts | 90 +++++++++ .../src/services/org-admin/org-admin-dal.ts | 5 + .../services/org-admin/org-admin-service.ts | 178 ++++++++++++++++++ .../src/services/org-admin/org-admin-types.ts | 11 ++ .../project-membership-service.ts | 1 - .../components/v2/Pagination/Pagination.tsx | 2 +- .../src/hooks/api/auditLogs/constants.tsx | 3 +- frontend/src/hooks/api/auditLogs/enums.tsx | 3 +- frontend/src/hooks/api/auditLogs/types.tsx | 13 +- frontend/src/hooks/api/index.tsx | 1 + frontend/src/hooks/api/orgAdmin/index.tsx | 2 + frontend/src/hooks/api/orgAdmin/mutation.tsx | 13 ++ frontend/src/hooks/api/orgAdmin/queries.tsx | 30 +++ frontend/src/hooks/api/orgAdmin/types.ts | 9 + frontend/src/hooks/api/workspace/queries.tsx | 1 + frontend/src/hooks/api/workspace/types.ts | 1 + frontend/src/hooks/usePopUp.tsx | 2 +- frontend/src/layouts/AppLayout/AppLayout.tsx | 15 ++ frontend/src/pages/org/[id]/admin/index.tsx | 21 +++ .../src/views/OrgAdminPage/OrgAdminPage.tsx | 30 +++ .../OrgAdminProjects/OrgAdminProjects.tsx | 155 +++++++++++++++ .../components/OrgAdminProjects/index.tsx | 1 + frontend/src/views/OrgAdminPage/index.tsx | 1 + .../AuditLogsPage/components/LogsTableRow.tsx | 6 + 29 files changed, 649 insertions(+), 15 deletions(-) create mode 100644 backend/src/server/routes/v1/org-admin-router.ts create mode 100644 backend/src/services/org-admin/org-admin-dal.ts create mode 100644 backend/src/services/org-admin/org-admin-service.ts create mode 100644 backend/src/services/org-admin/org-admin-types.ts create mode 100644 frontend/src/hooks/api/orgAdmin/index.tsx create mode 100644 frontend/src/hooks/api/orgAdmin/mutation.tsx create mode 100644 frontend/src/hooks/api/orgAdmin/queries.tsx create mode 100644 frontend/src/hooks/api/orgAdmin/types.ts create mode 100644 frontend/src/pages/org/[id]/admin/index.tsx create mode 100644 frontend/src/views/OrgAdminPage/OrgAdminPage.tsx create mode 100644 frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx create mode 100644 frontend/src/views/OrgAdminPage/components/OrgAdminProjects/index.tsx create mode 100644 frontend/src/views/OrgAdminPage/index.tsx diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 8ae892560..cfffdeac8 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -50,6 +50,7 @@ import { TIntegrationServiceFactory } from "@app/services/integration/integratio import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service"; import { TOrgServiceFactory } from "@app/services/org/org-service"; +import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { TProjectServiceFactory } from "@app/services/project/project-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service"; @@ -165,6 +166,7 @@ declare module "fastify" { rateLimit: TRateLimitServiceFactory; userEngagement: TUserEngagementServiceFactory; externalKms: TExternalKmsServiceFactory; + orgAdmin: TOrgAdminServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index bfc1dbf92..6f78ea0d6 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -147,7 +147,8 @@ export enum EventType { GET_KMS = "get-kms", UPDATE_PROJECT_KMS = "update-project-kms", GET_PROJECT_KMS_BACKUP = "get-project-kms-backup", - LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup" + LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup", + ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project" } interface UserActorMetadata { @@ -1245,6 +1246,16 @@ interface LoadProjectKmsBackupEvent { metadata: Record; // no metadata yet } +interface OrgAdminAccessProjectEvent { + type: EventType.ORG_ADMIN_ACCESS_PROJECT; + metadata: { + userId: string; + username: string; + email: string; + projectId: string; + }; // no metadata yet +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -1354,4 +1365,5 @@ export type Event = | GetKmsEvent | UpdateProjectKmsEvent | GetProjectKmsBackupEvent - | LoadProjectKmsBackupEvent; + | LoadProjectKmsBackupEvent + | OrgAdminAccessProjectEvent; diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index c01d146ec..dcab16218 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -19,23 +19,43 @@ export const withTransaction = (db: Knex, dal: K) => ({ export type TFindFilter = Partial & { $in?: Partial<{ [k in keyof R]: R[k][] }>; + $search?: Partial<{ [k in keyof R]: R[k] }>; }; export const buildFindFilter = - ({ $in, ...filter }: TFindFilter) => + ({ $in, $search, ...filter }: TFindFilter) => (bd: Knex.QueryBuilder) => { void bd.where(filter); if ($in) { Object.entries($in).forEach(([key, val]) => { - void bd.whereIn(key as never, val as never); + if (val) { + void bd.whereIn(key as never, val as never); + } + }); + } + if ($search) { + Object.entries($search).forEach(([key, val]) => { + if (val) { + void bd.whereILike(key as never, val as never); + } }); } return bd; }; -export type TFindOpt = { +export type TFindReturn = Array< + Awaited[0] & + (TCount extends true + ? { + count: string; + } + : unknown) +>; + +export type TFindOpt = { limit?: number; offset?: number; sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>; + count?: TCount; tx?: Knex; }; @@ -66,18 +86,22 @@ export const ormify = (db: Kne throw new DatabaseError({ error, name: "Find one" }); } }, - find: async ( + find: async ( filter: TFindFilter, - { offset, limit, sort, tx }: TFindOpt = {} + { offset, limit, sort, count, tx }: TFindOpt = {} ) => { try { const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter)); + if (count) { + void query.select(db.raw("COUNT(*) OVER() AS count")); + void query.select("*"); + } if (limit) void query.limit(limit); if (offset) void query.offset(offset); if (sort) { void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); } - const res = await query; + const res = (await query) as TFindReturn; return res; } catch (error) { throw new DatabaseError({ error, name: "Find one" }); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index e8f80f020..a6ef33a72 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -129,6 +129,7 @@ import { orgDALFactory } from "@app/services/org/org-dal"; import { orgRoleDALFactory } from "@app/services/org/org-role-dal"; import { orgRoleServiceFactory } from "@app/services/org/org-role-service"; import { orgServiceFactory } from "@app/services/org/org-service"; +import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { projectDALFactory } from "@app/services/project/project-dal"; import { projectQueueFactory } from "@app/services/project/project-queue"; @@ -498,6 +499,16 @@ export const registerRoutes = async ( keyStore, licenseService }); + const orgAdminService = orgAdminServiceFactory({ + projectDAL, + permissionService, + projectUserMembershipRoleDAL, + userDAL, + projectBotDAL, + projectKeyDAL, + projectMembershipDAL + }); + const rateLimitService = rateLimitServiceFactory({ rateLimitDAL, licenseService @@ -1113,7 +1124,8 @@ export const registerRoutes = async ( identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService, secretSharing: secretSharingService, userEngagement: userEngagementService, - externalKms: externalKmsService + externalKms: externalKmsService, + orgAdmin: orgAdminService }); const cronJobs: CronJob[] = []; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 43ce44eaa..bb6b9b57b 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -15,6 +15,7 @@ import { registerIdentityUaRouter } from "./identity-universal-auth-router"; import { registerIntegrationAuthRouter } from "./integration-auth-router"; import { registerIntegrationRouter } from "./integration-router"; import { registerInviteOrgRouter } from "./invite-org-router"; +import { registerOrgAdminRouter } from "./org-admin-router"; import { registerOrgRouter } from "./organization-router"; import { registerPasswordRouter } from "./password-router"; import { registerProjectEnvRouter } from "./project-env-router"; @@ -50,6 +51,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerOrgRouter, { prefix: "/organization" }); await server.register(registerAdminRouter, { prefix: "/admin" }); + await server.register(registerOrgAdminRouter, { prefix: "/org-admin" }); await server.register(registerUserRouter, { prefix: "/user" }); await server.register(registerInviteOrgRouter, { prefix: "/invite-org" }); await server.register(registerUserActionRouter, { prefix: "/user-action" }); diff --git a/backend/src/server/routes/v1/org-admin-router.ts b/backend/src/server/routes/v1/org-admin-router.ts new file mode 100644 index 000000000..71f636452 --- /dev/null +++ b/backend/src/server/routes/v1/org-admin-router.ts @@ -0,0 +1,90 @@ +import { z } from "zod"; + +import { ProjectMembershipsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { SanitizedProjectSchema } from "../sanitizedSchemas"; + +export const registerOrgAdminRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/projects", + config: { + rateLimit: readLimit + }, + schema: { + querystring: z.object({ + search: z.string().optional(), + offset: z.coerce.number().default(0), + limit: z.coerce.number().max(100).default(50) + }), + response: { + 200: z.object({ + projects: SanitizedProjectSchema.array(), + count: z.coerce.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { projects, count } = await server.services.orgAdmin.listOrgProjects({ + limit: req.query.limit, + offset: req.query.offset, + search: req.query.search, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actor: req.permission.type + }); + return { projects, count }; + } + }); + + server.route({ + method: "POST", + url: "/projects/:projectId/access", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectId: z.string() + }), + response: { + 200: z.object({ + membership: ProjectMembershipsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { membership, isExistingMember } = await server.services.orgAdmin.accessProject({ + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actor: req.permission.type, + projectId: req.params.projectId + }); + if (!isExistingMember && req.auth.authMode === AuthMode.JWT) { + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.projectId, + event: { + type: EventType.ORG_ADMIN_ACCESS_PROJECT, + metadata: { + projectId: req.params.projectId, + username: req.auth.user.username, + email: req.auth.user.email || "", + userId: req.auth.userId + } + } + }); + } + + return { membership }; + } + }); +}; diff --git a/backend/src/services/org-admin/org-admin-dal.ts b/backend/src/services/org-admin/org-admin-dal.ts new file mode 100644 index 000000000..da2ccf2f6 --- /dev/null +++ b/backend/src/services/org-admin/org-admin-dal.ts @@ -0,0 +1,5 @@ +export type TOrgAdminDALFactory = ReturnType; + +export const orgAdminDALFactory = () => { + return {}; +}; diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts new file mode 100644 index 000000000..89b516dcd --- /dev/null +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -0,0 +1,178 @@ +import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; + +import { TProjectDALFactory } from "../project/project-dal"; +import { assignWorkspaceKeysToMembers } from "../project/project-fns"; +import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; +import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; +import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; +import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; +import { TUserDALFactory } from "../user/user-dal"; +import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types"; + +type TOrgAdminServiceFactoryDep = { + permissionService: Pick; + projectDAL: Pick; + projectMembershipDAL: Pick; + projectKeyDAL: Pick; + projectBotDAL: Pick; + userDAL: Pick; + projectUserMembershipRoleDAL: Pick; +}; + +export type TOrgAdminServiceFactory = ReturnType; + +export const orgAdminServiceFactory = ({ + permissionService, + projectDAL, + projectMembershipDAL, + projectKeyDAL, + projectBotDAL, + userDAL, + projectUserMembershipRoleDAL +}: TOrgAdminServiceFactoryDep) => { + const listOrgProjects = async ({ + actor, + limit, + actorId, + offset, + search, + actorOrgId, + actorAuthMethod + }: TListOrgProjectsDTO) => { + const { membership } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + const isAdmin = membership.role === OrgMembershipRole.Admin; + if (!isAdmin) throw new UnauthorizedError({ message: "Admin only operation" }); + const projects = await projectDAL.find( + { + orgId: actorOrgId, + $search: { + name: search ? `%${search}%` : undefined + } + }, + { offset, limit, sort: [["name", "asc"]], count: true } + ); + + const count = projects?.[0]?.count ? parseInt(projects?.[0]?.count, 10) : 0; + return { projects, count }; + }; + + const accessProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TAccessProjectDTO) => { + const { membership } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + const isAdmin = membership.role === OrgMembershipRole.Admin; + if (!isAdmin) throw new UnauthorizedError({ message: "Admin only operation" }); + + const project = await projectDAL.findById(projectId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + + if (project.version === ProjectVersion.V1) { + throw new BadRequestError({ message: "Please upgrade your project on your dashboard" }); + } + + // check already there exist a membership if there return it + const projectMembership = await projectMembershipDAL.findOne({ + projectId, + userId: actorId + }); + if (projectMembership) { + // reset and make the user admin + await projectMembershipDAL.transaction(async (tx) => { + await projectUserMembershipRoleDAL.delete({ projectMembershipId: projectMembership.id }, tx); + await projectUserMembershipRoleDAL.create( + { + projectMembershipId: projectMembership.id, + role: ProjectMembershipRole.Admin + }, + tx + ); + }); + return { isExistingMember: true, membership: projectMembership }; + } + + // missing membership thus add admin back as admin to project + const ghostUser = await projectDAL.findProjectGhostUser(projectId); + if (!ghostUser) { + throw new BadRequestError({ + message: "Failed to find sudo user" + }); + } + + const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId); + if (!ghostUserLatestKey) { + throw new BadRequestError({ + message: "Failed to find sudo user latest key" + }); + } + + const bot = await projectBotDAL.findOne({ projectId }); + if (!bot) { + throw new BadRequestError({ + message: "Failed to find bot" + }); + } + + const botPrivateKey = infisicalSymmetricDecrypt({ + keyEncoding: bot.keyEncoding as SecretKeyEncoding, + iv: bot.iv, + tag: bot.tag, + ciphertext: bot.encryptedPrivateKey + }); + + const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId); + if (!userEncryptionKey) throw new BadRequestError({ message: "user encryption key not found" }); + const [newWsMember] = assignWorkspaceKeysToMembers({ + decryptKey: ghostUserLatestKey, + userPrivateKey: botPrivateKey, + members: [ + { + orgMembershipId: membership.id, + projectMembershipRole: ProjectMembershipRole.Admin, + userPublicKey: userEncryptionKey.publicKey + } + ] + }); + + const updatedMembership = await projectMembershipDAL.transaction(async (tx) => { + const newProjectMembership = await projectMembershipDAL.create( + { + projectId, + userId: actorId + }, + tx + ); + await projectUserMembershipRoleDAL.create( + { projectMembershipId: newProjectMembership.id, role: ProjectMembershipRole.Admin }, + tx + ); + + await projectKeyDAL.create( + { + encryptedKey: newWsMember.workspaceEncryptedKey, + nonce: newWsMember.workspaceEncryptedNonce, + senderId: ghostUser.id, + receiverId: actorId, + projectId + }, + tx + ); + return projectMembership; + }); + return { isExistingMember: false, membership: updatedMembership }; + }; + + return { listOrgProjects, accessProject }; +}; diff --git a/backend/src/services/org-admin/org-admin-types.ts b/backend/src/services/org-admin/org-admin-types.ts new file mode 100644 index 000000000..85669fc56 --- /dev/null +++ b/backend/src/services/org-admin/org-admin-types.ts @@ -0,0 +1,11 @@ +import { TOrgPermission } from "@app/lib/types"; + +export type TListOrgProjectsDTO = { + limit?: number; + offset?: number; + search?: string; +} & Omit; + +export type TAccessProjectDTO = { + projectId: string; +} & Omit; diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index a03aec934..8f87e8d55 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -256,7 +256,6 @@ export const projectMembershipServiceFactory = ({ } const bot = await projectBotDAL.findOne({ projectId }); - if (!bot) { throw new BadRequestError({ message: "Failed to find bot" diff --git a/frontend/src/components/v2/Pagination/Pagination.tsx b/frontend/src/components/v2/Pagination/Pagination.tsx index c8afb389b..f0ba950c1 100644 --- a/frontend/src/components/v2/Pagination/Pagination.tsx +++ b/frontend/src/components/v2/Pagination/Pagination.tsx @@ -50,7 +50,7 @@ export const Pagination = ({ >
- {(page - 1) * perPage} - {(page - 1) * perPage + perPage} of {count} + {(page - 1) * perPage} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 082bff02c..819c36c20 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -56,7 +56,8 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.GET_CERT]: "Get certificate", [EventType.DELETE_CERT]: "Delete certificate", [EventType.REVOKE_CERT]: "Revoke certificate", - [EventType.GET_CERT_BODY]: "Get certificate body" + [EventType.GET_CERT_BODY]: "Get certificate body", + [EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project" }; export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index ad49998c5..94963f640 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -70,5 +70,6 @@ export enum EventType { GET_CERT = "get-cert", DELETE_CERT = "delete-cert", REVOKE_CERT = "revoke-cert", - GET_CERT_BODY = "get-cert-body" + GET_CERT_BODY = "get-cert-body", + ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project" } diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index cdc973ed9..1d80d6128 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -579,6 +579,16 @@ interface GetCertBody { }; } +interface OrgAdminAccessProjectEvent { + type: EventType.ORG_ADMIN_ACCESS_PROJECT; + metadata: { + userId: string; + username: string; + email: string; + projectId: string; + }; // no metadata yet +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -635,7 +645,8 @@ export type Event = | GetCert | DeleteCert | RevokeCert - | GetCertBody; + | GetCertBody + | OrgAdminAccessProjectEvent; export type AuditLog = { id: string; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index ea3e7f560..5cbd1fb27 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -19,6 +19,7 @@ export * from "./keys"; export * from "./kms"; export * from "./ldapConfig"; export * from "./oidcConfig"; +export * from "./orgAdmin"; export * from "./organization"; export * from "./projectUserAdditionalPrivilege"; export * from "./rateLimit"; diff --git a/frontend/src/hooks/api/orgAdmin/index.tsx b/frontend/src/hooks/api/orgAdmin/index.tsx new file mode 100644 index 000000000..57eed413a --- /dev/null +++ b/frontend/src/hooks/api/orgAdmin/index.tsx @@ -0,0 +1,2 @@ +export { useOrgAdminAccessProject } from "./mutation"; +export { useOrgAdminGetProjects } from "./queries"; diff --git a/frontend/src/hooks/api/orgAdmin/mutation.tsx b/frontend/src/hooks/api/orgAdmin/mutation.tsx new file mode 100644 index 000000000..3b2c69efa --- /dev/null +++ b/frontend/src/hooks/api/orgAdmin/mutation.tsx @@ -0,0 +1,13 @@ +import { useMutation } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TOrgAdminAccessProjectDTO } from "./types"; + +export const useOrgAdminAccessProject = () => + useMutation({ + mutationFn: async ({ projectId }: TOrgAdminAccessProjectDTO) => { + const { data } = await apiRequest.post(`/api/v1/org-admin/projects/${projectId}/access`); + return data; + } + }); diff --git a/frontend/src/hooks/api/orgAdmin/queries.tsx b/frontend/src/hooks/api/orgAdmin/queries.tsx new file mode 100644 index 000000000..23826a9a4 --- /dev/null +++ b/frontend/src/hooks/api/orgAdmin/queries.tsx @@ -0,0 +1,30 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { Workspace } from "../types"; +import { TOrgAdminGetProjectsDTO } from "./types"; + +export const orgAdminQueryKeys = { + getProjects: (filter: TOrgAdminGetProjectsDTO) => ["org-admin-projects", filter] as const +}; + +export const useOrgAdminGetProjects = ({ search, offset, limit = 50 }: TOrgAdminGetProjectsDTO) => { + return useQuery({ + queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }), + queryFn: async () => { + const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>( + "/api/v1/org-admin/projects", + { + params: { + limit, + offset, + search + } + } + ); + + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/orgAdmin/types.ts b/frontend/src/hooks/api/orgAdmin/types.ts new file mode 100644 index 000000000..87626a466 --- /dev/null +++ b/frontend/src/hooks/api/orgAdmin/types.ts @@ -0,0 +1,9 @@ +export type TOrgAdminGetProjectsDTO = { + limit?: number; + offset?: number; + search?: string; +}; + +export type TOrgAdminAccessProjectDTO = { + projectId: string; +}; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index f3ffaf86e..57087feb9 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -317,6 +317,7 @@ export const useDeleteWorkspace = () => { }, onSuccess: () => { queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(["org-admin-projects"]); } }); }; diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 783b15f1e..51bb08e3d 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -21,6 +21,7 @@ export type Workspace = { pitVersionLimit: number; auditLogsRetentionDays: number; slug: string; + createdAt: string; }; export type WorkspaceEnv = { diff --git a/frontend/src/hooks/usePopUp.tsx b/frontend/src/hooks/usePopUp.tsx index e9d8257e3..28780db9e 100644 --- a/frontend/src/hooks/usePopUp.tsx +++ b/frontend/src/hooks/usePopUp.tsx @@ -13,7 +13,7 @@ interface UsePopUpProps { export type UsePopUpState | UsePopUpProps[]> = { [P in T extends UsePopUpProps[] ? T[number]["name"] : T[number]]: { isOpen: boolean; - data?: unknown; + data?: any; }; }; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 87d4c8458..0ec57ba0d 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -59,6 +59,7 @@ import { OrgPermissionActions, OrgPermissionSubjects, useOrganization, + useOrgPermission, useSubscription, useUser, useWorkspace @@ -131,6 +132,8 @@ export const AppLayout = ({ children }: LayoutProps) => { const { workspaces, currentWorkspace } = useWorkspace(); const { orgs, currentOrg } = useOrganization(); + const { membership } = useOrgPermission(); + const isOrgAdmin = membership?.role === "admin"; const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!); const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); @@ -748,6 +751,18 @@ export const AppLayout = ({ children }: LayoutProps) => { )} + {isOrgAdmin && ( + + + + Admin + + + + )} + + {t("common.head-title", { title: t("settings.org.title") })} + + + + + ); +} + +SettingsOrg.requireAuth = true; diff --git a/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx b/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx new file mode 100644 index 000000000..eadd732d3 --- /dev/null +++ b/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx @@ -0,0 +1,30 @@ +import { useState } from "react"; + +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; + +import { OrgAdminProjects } from "./components/OrgAdminProjects"; + +enum TabSections { + Projects = "projects" +} + +export const OrgAdminPage = () => { + const [activeTab, setActiveTab] = useState(TabSections.Projects); + return ( +
+
+
+

Admin Panel

+
+ setActiveTab(el as TabSections)}> + + Projects + + + + + +
+
+ ); +}; diff --git a/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx new file mode 100644 index 000000000..d2b66ad18 --- /dev/null +++ b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx @@ -0,0 +1,155 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +import { faEllipsis, faMagnifyingGlass, faSignIn } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; +import { motion } from "framer-motion"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + EmptyState, + Input, + Pagination, + Spinner, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr} from "@app/components/v2"; +import { useDebounce } from "@app/hooks"; +import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api"; + +export const OrgAdminProjects = () => { + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search); + const [perPage, setPerPage] = useState(25); + const router = useRouter(); + const orgAdminAccessProject = useOrgAdminAccessProject(); + + const { data, isLoading: isProjectsLoading } = useOrgAdminGetProjects({ + offset: (page - 1) * perPage, + limit: perPage, + search: debouncedSearch || undefined + }); + + const projects = data?.projects || []; + const projectCount = data?.count || 0; + const isEmpty = !isProjectsLoading && projects.length === 0; + + const handleAccessProject = async (projectId: string) => { + try { + await orgAdminAccessProject.mutateAsync({ + projectId + }); + await router.push({ + pathname: "/project/[projectId]/secrets/overview", + query: { + projectId + } + }); + } catch { + createNotification({ + text: "Failed to access project", + type: "error" + }); + } + }; + + return ( + +
+
+

Projects

+
+
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search by project name" + /> + + + + + + + + + + + {isProjectsLoading && } + {!isProjectsLoading && + projects?.map(({ name, slug, createdAt, id }) => ( + + + + + + + ))} + +
NameSlugCreated At +
{name}{slug}{format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa")} +
+ + + + + + { + e.stopPropagation(); + e.preventDefault(); + handleAccessProject(id); + }} + icon={} + disabled={ + orgAdminAccessProject.variables?.projectId === id && + orgAdminAccessProject.isLoading + } + > + Access{" "} + {orgAdminAccessProject.variables?.projectId === id && + orgAdminAccessProject.isLoading && } + + + +
+
+ {!isProjectsLoading && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {isEmpty && } +
+
+
+
+ ); +}; diff --git a/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/index.tsx b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/index.tsx new file mode 100644 index 000000000..b331589a4 --- /dev/null +++ b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/index.tsx @@ -0,0 +1 @@ +export { OrgAdminProjects } from "./OrgAdminProjects"; diff --git a/frontend/src/views/OrgAdminPage/index.tsx b/frontend/src/views/OrgAdminPage/index.tsx new file mode 100644 index 000000000..1fe7b5541 --- /dev/null +++ b/frontend/src/views/OrgAdminPage/index.tsx @@ -0,0 +1 @@ +export { OrgAdminPage } from "./OrgAdminPage"; diff --git a/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx b/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx index 5eada2663..668b5e5c6 100644 --- a/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx +++ b/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx @@ -317,6 +317,12 @@ export const LogsTableRow = ({ auditLog }: Props) => { })} ); + case EventType.ORG_ADMIN_ACCESS_PROJECT: + return ( + +

{`Email: ${event.metadata.email}`}

+ + ); case EventType.CREATE_CA: case EventType.GET_CA: case EventType.UPDATE_CA: From cf5260b38352a6ef86a518da0738e4cdfec7449a Mon Sep 17 00:00:00 2001 From: = Date: Fri, 2 Aug 2024 19:28:31 +0530 Subject: [PATCH 20/41] feat: minor bug fix on access operation --- backend/src/server/routes/v1/org-admin-router.ts | 4 ++-- backend/src/services/org-admin/org-admin-service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/server/routes/v1/org-admin-router.ts b/backend/src/server/routes/v1/org-admin-router.ts index 71f636452..5339d91b5 100644 --- a/backend/src/server/routes/v1/org-admin-router.ts +++ b/backend/src/server/routes/v1/org-admin-router.ts @@ -61,14 +61,14 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { membership, isExistingMember } = await server.services.orgAdmin.accessProject({ + const { membership } = await server.services.orgAdmin.accessProject({ actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, actor: req.permission.type, projectId: req.params.projectId }); - if (!isExistingMember && req.auth.authMode === AuthMode.JWT) { + if (req.auth.authMode === AuthMode.JWT) { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: req.params.projectId, diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 89b516dcd..aef1342b4 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -169,7 +169,7 @@ export const orgAdminServiceFactory = ({ }, tx ); - return projectMembership; + return newProjectMembership; }); return { isExistingMember: false, membership: updatedMembership }; }; From b97bbe5beb8d627ca2cc9a86452c19da680ed1b4 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 2 Aug 2024 19:28:52 +0530 Subject: [PATCH 21/41] feat: text change in sidebar --- frontend/src/layouts/AppLayout/AppLayout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 0ec57ba0d..8a794f901 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -479,7 +479,7 @@ export const AppLayout = ({ children }: LayoutProps) => { {user?.superAdmin && ( - Admin Panel + Server Admin Panel )} @@ -758,7 +758,7 @@ export const AppLayout = ({ children }: LayoutProps) => { isSelected={router.asPath === `/org/${currentOrg?.id}/admin`} icon="system-outline-109-slider-toggle-settings" > - Admin + Admin Panel
From 1ad286ca871ab6f8401d93e2b18b1b1b55c0311c Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 2 Aug 2024 22:58:53 +0800 Subject: [PATCH 22/41] misc: name updates and more comments --- .../src/server/plugins/inject-rate-limits.ts | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/backend/src/server/plugins/inject-rate-limits.ts b/backend/src/server/plugins/inject-rate-limits.ts index 74acc8fcb..487fdf1a6 100644 --- a/backend/src/server/plugins/inject-rate-limits.ts +++ b/backend/src/server/plugins/inject-rate-limits.ts @@ -1,35 +1,40 @@ import fp from "fastify-plugin"; import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service"; +import { getConfig } from "@app/lib/config/env"; export const injectRateLimits = fp(async (server) => { server.decorateRequest("rateLimits", null); server.addHook("onRequest", async (req) => { - const defaultRateLimiterConfig = getRateLimiterConfig(); + const appCfg = getConfig(); + + const instanceRateLimiterConfig = getRateLimiterConfig(); if (!req.auth) { - // for public endpoints - req.rateLimits = defaultRateLimiterConfig; + // for public endpoints, we always use the instance-wide default rate limits + req.rateLimits = instanceRateLimiterConfig; return; } const plan = await server.services.license.getPlan(req.auth.orgId); const { rateLimits } = plan; - if (plan.customRateLimits) { - req.rateLimits = defaultRateLimiterConfig; + if (plan.customRateLimits && !appCfg.isCloud) { + // we do this because for self-hosted/dedicated instances, we want custom rate limits to be based on admin configuration + // note that the syncing of custom rate limit happens on the instanceRateLimiterConfig object + req.rateLimits = instanceRateLimiterConfig; return; } // we're using the null coalescing operator in order to handle outdated licenses req.rateLimits = { - readLimit: rateLimits?.readLimit ?? defaultRateLimiterConfig.readLimit, - publicEndpointLimit: rateLimits?.publicEndpointLimit ?? defaultRateLimiterConfig.publicEndpointLimit, - writeLimit: rateLimits?.writeLimit ?? defaultRateLimiterConfig.writeLimit, - secretsLimit: rateLimits?.secretsLimit ?? defaultRateLimiterConfig.secretsLimit, - authRateLimit: rateLimits?.authRateLimit ?? defaultRateLimiterConfig.authRateLimit, - inviteUserRateLimit: rateLimits?.inviteUserRateLimit ?? defaultRateLimiterConfig.inviteUserRateLimit, - mfaRateLimit: rateLimits?.mfaRateLimit ?? defaultRateLimiterConfig.mfaRateLimit, - creationLimit: rateLimits?.creationLimit ?? defaultRateLimiterConfig.creationLimit + readLimit: rateLimits?.readLimit ?? instanceRateLimiterConfig.readLimit, + publicEndpointLimit: rateLimits?.publicEndpointLimit ?? instanceRateLimiterConfig.publicEndpointLimit, + writeLimit: rateLimits?.writeLimit ?? instanceRateLimiterConfig.writeLimit, + secretsLimit: rateLimits?.secretsLimit ?? instanceRateLimiterConfig.secretsLimit, + authRateLimit: rateLimits?.authRateLimit ?? instanceRateLimiterConfig.authRateLimit, + inviteUserRateLimit: rateLimits?.inviteUserRateLimit ?? instanceRateLimiterConfig.inviteUserRateLimit, + mfaRateLimit: rateLimits?.mfaRateLimit ?? instanceRateLimiterConfig.mfaRateLimit, + creationLimit: rateLimits?.creationLimit ?? instanceRateLimiterConfig.creationLimit }; }); }); From 33193a47ae99948f687b89407c79180b7ddaaf83 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 3 Aug 2024 01:52:04 +0800 Subject: [PATCH 23/41] misc: updated default onprem rate limits --- backend/src/ee/services/license/licence-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index c7bdd95ac..ad6a98573 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -45,7 +45,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ readLimit: 60, publicEndpointLimit: 30, writeLimit: 200, - secretsLimit: 60, + secretsLimit: 40, authRateLimit: 60, inviteUserRateLimit: 30, mfaRateLimit: 20, From 0f23b7e1d3181f6100a5f4337d8548c36ca51a56 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 3 Aug 2024 02:10:47 +0800 Subject: [PATCH 24/41] misc: added check for undefined orgId --- backend/src/server/plugins/inject-rate-limits.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/server/plugins/inject-rate-limits.ts b/backend/src/server/plugins/inject-rate-limits.ts index 487fdf1a6..4bd3729b3 100644 --- a/backend/src/server/plugins/inject-rate-limits.ts +++ b/backend/src/server/plugins/inject-rate-limits.ts @@ -9,7 +9,7 @@ export const injectRateLimits = fp(async (server) => { const appCfg = getConfig(); const instanceRateLimiterConfig = getRateLimiterConfig(); - if (!req.auth) { + if (!req.auth?.orgId) { // for public endpoints, we always use the instance-wide default rate limits req.rateLimits = instanceRateLimiterConfig; return; From b4a2a477d34190cba3b3c9eae0a5eb6e1ceda67f Mon Sep 17 00:00:00 2001 From: = Date: Sat, 3 Aug 2024 14:55:30 +0530 Subject: [PATCH 25/41] feat: brought back workspace permission and made requested changes --- .../ee/services/permission/org-permission.ts | 12 +- backend/src/server/routes/v1/index.ts | 2 +- .../src/server/routes/v1/org-admin-router.ts | 4 +- .../services/org-admin/org-admin-service.ts | 33 +- .../src/context/OrgPermissionContext/types.ts | 10 +- frontend/src/hooks/api/orgAdmin/mutation.tsx | 4 +- frontend/src/hooks/api/orgAdmin/queries.tsx | 2 +- frontend/src/layouts/AppLayout/AppLayout.tsx | 20 +- .../src/pages/org/[id]/overview/index.tsx | 1215 ++++++++--------- .../WorkspacePermission.tsx | 133 -- .../components/OrgRoleModifySection.utils.ts | 10 +- .../OrgPermissionAdminConsoleRow.tsx | 135 ++ .../OrgRoleWorkspaceRow.tsx | 129 ++ .../RolePermissionRow.tsx | 5 +- .../RolePermissionsSection.tsx | 12 + .../OrgAdminProjects/OrgAdminProjects.tsx | 254 ++-- 16 files changed, 1074 insertions(+), 906 deletions(-) delete mode 100644 frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/WorkspacePermission.tsx create mode 100644 frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx create mode 100644 frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index 77eaacd3b..c07107912 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -9,6 +9,10 @@ export enum OrgPermissionActions { Delete = "delete" } +export enum OrgPermissionAdminConsoleAction { + GrantAccessProjects = "grant-access-projects" +} + export enum OrgPermissionSubjects { Workspace = "workspace", Role = "role", @@ -22,7 +26,8 @@ export enum OrgPermissionSubjects { Billing = "billing", SecretScanning = "secret-scanning", Identity = "identity", - Kms = "kms" + Kms = "kms", + AdminConsole = "admin-console" } export type OrgPermissionSet = @@ -39,7 +44,8 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Identity] - | [OrgPermissionActions, OrgPermissionSubjects.Kms]; + | [OrgPermissionActions, OrgPermissionSubjects.Kms] + | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]; const buildAdminPermission = () => { const { can, build } = new AbilityBuilder>(createMongoAbility); @@ -107,6 +113,8 @@ const buildAdminPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms); + can(OrgPermissionAdminConsoleAction.GrantAccessProjects, OrgPermissionSubjects.AdminConsole); + return build({ conditionsMatcher }); }; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index bb6b9b57b..6c988d995 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -51,7 +51,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerOrgRouter, { prefix: "/organization" }); await server.register(registerAdminRouter, { prefix: "/admin" }); - await server.register(registerOrgAdminRouter, { prefix: "/org-admin" }); + await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" }); await server.register(registerUserRouter, { prefix: "/user" }); await server.register(registerInviteOrgRouter, { prefix: "/invite-org" }); await server.register(registerUserActionRouter, { prefix: "/user-action" }); diff --git a/backend/src/server/routes/v1/org-admin-router.ts b/backend/src/server/routes/v1/org-admin-router.ts index 5339d91b5..2d28b09bd 100644 --- a/backend/src/server/routes/v1/org-admin-router.ts +++ b/backend/src/server/routes/v1/org-admin-router.ts @@ -45,7 +45,7 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/projects/:projectId/access", + url: "/projects/:projectId/grant-admin-access", config: { rateLimit: readLimit }, @@ -61,7 +61,7 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { membership } = await server.services.orgAdmin.accessProject({ + const { membership } = await server.services.orgAdmin.grantProjectAdminAccess({ actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index aef1342b4..cfdda663d 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -1,7 +1,10 @@ -import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas"; +import { ForbiddenError } from "@casl/ability"; + +import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas"; +import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; -import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError } from "@app/lib/errors"; import { TProjectDALFactory } from "../project/project-dal"; import { assignWorkspaceKeysToMembers } from "../project/project-fns"; @@ -42,15 +45,17 @@ export const orgAdminServiceFactory = ({ actorOrgId, actorAuthMethod }: TListOrgProjectsDTO) => { - const { membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, actorAuthMethod, actorOrgId ); - const isAdmin = membership.role === OrgMembershipRole.Admin; - if (!isAdmin) throw new UnauthorizedError({ message: "Admin only operation" }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAdminConsoleAction.GrantAccessProjects, + OrgPermissionSubjects.AdminConsole + ); const projects = await projectDAL.find( { orgId: actorOrgId, @@ -65,16 +70,24 @@ export const orgAdminServiceFactory = ({ return { projects, count }; }; - const accessProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TAccessProjectDTO) => { - const { membership } = await permissionService.getOrgPermission( + const grantProjectAdminAccess = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + projectId + }: TAccessProjectDTO) => { + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, actorAuthMethod, actorOrgId ); - const isAdmin = membership.role === OrgMembershipRole.Admin; - if (!isAdmin) throw new UnauthorizedError({ message: "Admin only operation" }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAdminConsoleAction.GrantAccessProjects, + OrgPermissionSubjects.AdminConsole + ); const project = await projectDAL.findById(projectId); if (!project) throw new BadRequestError({ message: "Project not found" }); @@ -174,5 +187,5 @@ export const orgAdminServiceFactory = ({ return { isExistingMember: false, membership: updatedMembership }; }; - return { listOrgProjects, accessProject }; + return { listOrgProjects, grantProjectAdminAccess }; }; diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index 36206873d..a36aaac69 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -20,7 +20,12 @@ export enum OrgPermissionSubjects { Billing = "billing", SecretScanning = "secret-scanning", Identity = "identity", - Kms = "kms" + Kms = "kms", + AdminConsole = "admin-console" +} + +export enum OrgPermissionAdminConsoleAction { + GrantAccessProjects = "grant-access-projects" } export type OrgPermissionSet = @@ -37,6 +42,7 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Identity] - | [OrgPermissionActions, OrgPermissionSubjects.Kms]; + | [OrgPermissionActions, OrgPermissionSubjects.Kms] + | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]; export type TOrgPermission = MongoAbility; diff --git a/frontend/src/hooks/api/orgAdmin/mutation.tsx b/frontend/src/hooks/api/orgAdmin/mutation.tsx index 3b2c69efa..9fa93722e 100644 --- a/frontend/src/hooks/api/orgAdmin/mutation.tsx +++ b/frontend/src/hooks/api/orgAdmin/mutation.tsx @@ -7,7 +7,9 @@ import { TOrgAdminAccessProjectDTO } from "./types"; export const useOrgAdminAccessProject = () => useMutation({ mutationFn: async ({ projectId }: TOrgAdminAccessProjectDTO) => { - const { data } = await apiRequest.post(`/api/v1/org-admin/projects/${projectId}/access`); + const { data } = await apiRequest.post( + `/api/v1/organization-admin/projects/${projectId}/grant-admin-access` + ); return data; } }); diff --git a/frontend/src/hooks/api/orgAdmin/queries.tsx b/frontend/src/hooks/api/orgAdmin/queries.tsx index 23826a9a4..2856de0a2 100644 --- a/frontend/src/hooks/api/orgAdmin/queries.tsx +++ b/frontend/src/hooks/api/orgAdmin/queries.tsx @@ -14,7 +14,7 @@ export const useOrgAdminGetProjects = ({ search, offset, limit = 50 }: TOrgAdmin queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }), queryFn: async () => { const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>( - "/api/v1/org-admin/projects", + "/api/v1/organization-admin/projects", { params: { limit, diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 8a794f901..0568a5821 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -59,7 +59,6 @@ import { OrgPermissionActions, OrgPermissionSubjects, useOrganization, - useOrgPermission, useSubscription, useUser, useWorkspace @@ -132,8 +131,6 @@ export const AppLayout = ({ children }: LayoutProps) => { const { workspaces, currentWorkspace } = useWorkspace(); const { orgs, currentOrg } = useOrganization(); - const { membership } = useOrgPermission(); - const isOrgAdmin = membership?.role === "admin"; const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!); const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); @@ -483,6 +480,11 @@ export const AppLayout = ({ children }: LayoutProps) => { )} + + + Admin Panel + +
+
+ ); + + const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events +
{ + router.push(`/project/${workspace.id}/secrets/overview`); + localStorage.setItem("projectData.id", workspace.id); + }} + key={workspace.id} + className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${ + index === 0 && "rounded-t-md" + } ${index === filteredWorkspaces.length - 1 && "rounded-b-md border-b"}`} + > +
+ +
{workspace.name}
+
+
+
{workspace.environments?.length || 0} environments
- -
- ); - - const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -
{ - router.push(`/project/${workspace.id}/secrets/overview`); - localStorage.setItem("projectData.id", workspace.id); - }} - key={workspace.id} - className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${ - index === 0 && "rounded-t-md" - } ${index === filteredWorkspaces.length - 1 && "rounded-b-md border-b"}`} - > -
- -
{workspace.name}
-
-
-
- {workspace.environments?.length || 0} environments -
- {isFavorite ? ( - { - e.stopPropagation(); - removeProjectFromFavorites(workspace.id); - }} - /> - ) : ( - { - e.stopPropagation(); - addProjectToFavorites(workspace.id); - }} - /> - )} -
-
- ); - - const projectsGridView = ( - <> - {favoriteWorkspaces.length > 0 && ( - <> -

Favorites

-
0 && "border-b border-mineshaft-600" - } py-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`} - > - {favoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, true))} -
- + {isFavorite ? ( + { + e.stopPropagation(); + removeProjectFromFavorites(workspace.id); + }} + /> + ) : ( + { + e.stopPropagation(); + addProjectToFavorites(workspace.id); + }} + /> )} -
- {isProjectViewLoading && - Array.apply(0, Array(3)).map((_x, i) => ( -
-
- -
-
- -
-
- -
-
- ))} - {!isProjectViewLoading && - nonFavoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, false))} -
- - ); +
+
+ ); - const projectsListView = ( -
+ const projectsGridView = ( + <> + {favoriteWorkspaces.length > 0 && ( + <> +

Favorites

+
0 && "border-b border-mineshaft-600" + } py-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`} + > + {favoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, true))} +
+ + )} +
{isProjectViewLoading && Array.apply(0, Array(3)).map((_x, i) => (
- +
+ +
+
+ +
+
+ +
))} {!isProjectViewLoading && - workspacesWithFaveProp.map((workspace, ind) => - renderProjectListItem(workspace, workspace.isFavorite, ind) - )} + nonFavoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, false))}
- ); + + ); - return ( -
- - {t("common.head-title", { title: t("settings.members.title") })} - - - {!serverDetails?.redisConfigured && ( -
-

Announcements

-
- - Attention: Updated versions of Infisical now require Redis for full functionality. - Learn how to configure it - + {isProjectViewLoading && + Array.apply(0, Array(3)).map((_x, i) => ( +
+ +
+ ))} + {!isProjectViewLoading && + workspacesWithFaveProp.map((workspace, ind) => + renderProjectListItem(workspace, workspace.isFavorite, ind) + )} +
+ ); + + return ( +
+ + {t("common.head-title", { title: t("settings.members.title") })} + + + {!serverDetails?.redisConfigured && ( +
+

Announcements

+
+ + Attention: Updated versions of Infisical now require Redis for full functionality. Learn + how to configure it + + + here + + + . +
+
+ )} +
+
+

Projects

+
+
+ setSearchFilter(e.target.value)} + leftIcon={} + /> +
+ { + localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID); + setProjectsViewMode(ProjectsViewMode.GRID); + }} + ariaLabel="grid" + size="xs" + className={`${ + projectsViewMode === ProjectsViewMode.GRID ? "bg-mineshaft-500" : "bg-transparent" + } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} + > + + + { + localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST); + setProjectsViewMode(ProjectsViewMode.LIST); + }} + ariaLabel="list" + size="xs" + className={`${ + projectsViewMode === ProjectsViewMode.LIST ? "bg-mineshaft-500" : "bg-transparent" + } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} + > + + +
+ + {(isAllowed) => ( + + )} + +
+ {projectsViewMode === ProjectsViewMode.LIST ? projectsListView : projectsGridView} + {isWorkspaceEmpty && ( +
+ +
+ You are not part of any projects in this organization yet. When you are, they will + appear here. +
+
+ Create a new project, or ask other organization members to give you necessary + permissions.
)} -
-
-

Projects

-
-
- setSearchFilter(e.target.value)} - leftIcon={} - /> -
- { - localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID); - setProjectsViewMode(ProjectsViewMode.GRID); - }} - ariaLabel="grid" - size="xs" - className={`${ - projectsViewMode === ProjectsViewMode.GRID ? "bg-mineshaft-500" : "bg-transparent" - } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} - > - - - { - localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST); - setProjectsViewMode(ProjectsViewMode.LIST); - }} - ariaLabel="list" - size="xs" - className={`${ - projectsViewMode === ProjectsViewMode.LIST ? "bg-mineshaft-500" : "bg-transparent" - } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} - > - - -
- - {(isAllowed) => ( -
+
+

Explore Infisical

+
+ {features.map((feature) => ( + - - {!( - new Date().getTime() - new Date(user?.createdAt).getTime() < - 30 * 24 * 60 * 60 * 1000 - ) && ( -
-

Onboarding Guide

-
- - {orgWorkspaces.length !== 0 && ( - <> - - - - )} -
- -
-
+
+ {!(new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000) && ( +
+

Onboarding Guide

+
+ {orgWorkspaces.length !== 0 && ( -
-
-
- - {false && ( -
- -
- )} -
-
Inject secrets locally
-
- Replace .env files with a more secure and efficient alternative. -
+ <> + + + + )} +
+ +
+
+ {orgWorkspaces.length !== 0 && ( +
+
+
+ + {false && ( +
+ +
+ )} +
+
Inject secrets locally
+
+ Replace .env files with a more secure and efficient alternative.
-
- About 2 min -
- - {false &&
} +
+ About 2 min +
- )} - {orgWorkspaces.length !== 0 && ( - - )} -
- )} - { - handlePopUpToggle("addNewWs", isModalOpen); - reset(); - }} + + {false &&
} +
+ )} + {orgWorkspaces.length !== 0 && ( + + )} +
+ )} + { + handlePopUpToggle("addNewWs", isModalOpen); + reset(); + }} + > + - -
+ + ( + + + + )} + /> +
( - - - + name="addMembers" + defaultValue={false} + render={({ field: { onBlur, value, onChange } }) => ( + + {(isAllowed) => ( +
+ + Add all members of my organization to this project + +
+ )} +
)} /> -
- ( - - {(isAllowed) => ( -
- +
+ + + +
Advanced Settings
+
+ + ( + + { - onChange(e); - }} - className="mb-12 w-full bg-mineshaft-600" - > - - Default Infisical KMS + + Default Infisical KMS + + {externalKmsList?.map((kms) => ( + + {kms.slug} - {externalKmsList?.map((kms) => ( - - {kms.slug} - - ))} - - - )} - control={control} - name="kmsKeyId" - /> - -
-
-
- - -
+ ))} + + + )} + control={control} + name="kmsKeyId" + /> + + + +
+ +
- - - - handlePopUpToggle("upgradePlan", isOpen)} - text="You have exceeded the number of projects allowed on the free plan." - /> - {/* */} -
- ); - }, - { - action: OrgPermissionActions.Read, - subject: OrgPermissionSubjects.Workspace - } -); +
+ + + + handlePopUpToggle("upgradePlan", isOpen)} + text="You have exceeded the number of projects allowed on the free plan." + /> + {/* */} +
+ ); +}; Object.assign(OrganizationPage, { requireAuth: true }); diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/WorkspacePermission.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/WorkspacePermission.tsx deleted file mode 100644 index 465029f8b..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/WorkspacePermission.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { useEffect, useMemo } from "react"; -import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; -import { faMoneyBill } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { motion } from "framer-motion"; -import { twMerge } from "tailwind-merge"; - -import { Checkbox, Select, SelectItem } from "@app/components/v2"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../../../../RolePage/components/OrgRoleModifySection.utils"; - -type Props = { - isNonEditable?: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - ReadOnly = "read-only", - FullAccess = "full-acess", - Custom = "custom" -} - -const PERMISSIONS = [ - { action: "read", label: "View projects" }, - { action: "create", label: "Create new projects" } -] as const; - -export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props) => { - const rule = useWatch({ - control, - name: "permissions.workspace" - }); - const [isCustom, setIsCustom] = useToggle(); - - const selectedPermissionCategory = useMemo(() => { - const actions = Object.keys(rule || {}) as Array; - const totalActions = PERMISSIONS.length; - const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number); - - if (isCustom) return Permission.Custom; - if (score === 0) return Permission.NoAccess; - if (score === totalActions) return Permission.FullAccess; - if (score === 1 && rule?.read) return Permission.ReadOnly; - - return Permission.Custom; - }, [rule, isCustom]); - - useEffect(() => { - if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); - else setIsCustom.off(); - }, [selectedPermissionCategory]); - - const handlePermissionChange = (val: Permission) => { - if (val === Permission.Custom) setIsCustom.on(); - else setIsCustom.off(); - - switch (val) { - case Permission.NoAccess: - setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true }); - break; - case Permission.FullAccess: - setValue("permissions.workspace", { read: true, create: true }, { shouldDirty: true }); - break; - case Permission.ReadOnly: - setValue("permissions.workspace", { read: true, create: false }, { shouldDirty: true }); - break; - default: - setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true }); - break; - } - }; - - return ( -
-
-
- -
-
-
Project
-
- View and create new projects in this organization -
-
-
- -
-
- - {isCustom && - PERMISSIONS.map(({ action, label }) => ( - ( - - {label} - - )} - /> - ))} - -
- ); -}; diff --git a/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts b/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts index e85e62d0c..ba5308cd9 100644 --- a/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts +++ b/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts @@ -12,6 +12,12 @@ const generalPermissionSchema = z }) .optional(); +const adminConsolePermissionSchmea = z + .object({ + "grant-access-projects": z.boolean().optional() + }) + .optional(); + export const formSchema = z.object({ name: z.string().trim(), description: z.string().trim().optional(), @@ -23,7 +29,6 @@ export const formSchema = z.object({ .object({ workspace: z .object({ - read: z.boolean().optional(), create: z.boolean().optional() }) .optional(), @@ -38,7 +43,8 @@ export const formSchema = z.object({ scim: generalPermissionSchema, ldap: generalPermissionSchema, billing: generalPermissionSchema, - identity: generalPermissionSchema + identity: generalPermissionSchema, + "admin-console": adminConsolePermissionSchmea }) .optional() }); diff --git a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx new file mode 100644 index 000000000..21d58aabb --- /dev/null +++ b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx @@ -0,0 +1,135 @@ +import { useEffect, useMemo } from "react"; +import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; +import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils"; + +type Props = { + isEditable: boolean; + setValue: UseFormSetValue; + control: Control; +}; + +enum Permission { + NoAccess = "no-access", + Custom = "custom" +} + +const PERMISSION_ACTIONS = [ + { action: "grant-access-projects", label: "Grant access projects" } +] as const; + +export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: Props) => { + const [isRowExpanded, setIsRowExpanded] = useToggle(); + const [isCustom, setIsCustom] = useToggle(); + + const rule = useWatch({ + control, + name: "permissions.admin-console" + }); + + const selectedPermissionCategory = useMemo(() => { + if (rule?.["grant-access-projects"]) { + return Permission.Custom; + } + return Permission.NoAccess; + }, [rule, isCustom]); + + useEffect(() => { + if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); + else setIsCustom.off(); + }, [selectedPermissionCategory]); + + useEffect(() => { + const isRowCustom = selectedPermissionCategory === Permission.Custom; + if (isRowCustom) { + setIsRowExpanded.on(); + } + }, []); + + const handlePermissionChange = (val: Permission) => { + if (!val) return; + if (val === Permission.Custom) { + setIsRowExpanded.on(); + setIsCustom.on(); + return; + } + setIsCustom.off(); + + if (val === Permission.NoAccess) { + setValue( + "permissions.admin-console", + { "grant-access-projects": false }, + { shouldDirty: true } + ); + } + }; + + return ( + <> + setIsRowExpanded.toggle()} + > + + + + Admin Console + + + + + {isRowExpanded && ( + + +
+ {PERMISSION_ACTIONS.map(({ action, label }) => { + return ( + ( + { + if (!isEditable) { + createNotification({ + type: "error", + text: "Failed to update default role" + }); + return; + } + field.onChange(e); + }} + id={`permissions.admin-console.${action}`} + > + {label} + + )} + /> + ); + })} +
+ + + )} + + ); +}; diff --git a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx new file mode 100644 index 000000000..ea7f368c5 --- /dev/null +++ b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx @@ -0,0 +1,129 @@ +import { useEffect, useMemo } from "react"; +import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; +import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils"; + +type Props = { + isEditable: boolean; + setValue: UseFormSetValue; + control: Control; +}; + +enum Permission { + NoAccess = "no-access", + Custom = "custom" +} + +const PERMISSION_ACTIONS = [{ action: "create", label: "Create projects" }] as const; + +export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) => { + const [isRowExpanded, setIsRowExpanded] = useToggle(); + const [isCustom, setIsCustom] = useToggle(); + + const rule = useWatch({ + control, + name: "permissions.workspace" + }); + + const selectedPermissionCategory = useMemo(() => { + if (rule?.create) { + return Permission.Custom; + } + return Permission.NoAccess; + }, [rule, isCustom]); + + useEffect(() => { + if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); + else setIsCustom.off(); + }, [selectedPermissionCategory]); + + useEffect(() => { + const isRowCustom = selectedPermissionCategory === Permission.Custom; + if (isRowCustom) { + setIsRowExpanded.on(); + } + }, []); + + const handlePermissionChange = (val: Permission) => { + if (!val) return; + if (val === Permission.Custom) { + setIsRowExpanded.on(); + setIsCustom.on(); + return; + } + setIsCustom.off(); + + if (val === Permission.NoAccess) { + setValue("permissions.workspace", { create: false }, { shouldDirty: true }); + } + }; + + return ( + <> + setIsRowExpanded.toggle()} + > + + + + Project + + + + + {isRowExpanded && ( + + +
+ {PERMISSION_ACTIONS.map(({ action, label }) => { + return ( + ( + { + if (!isEditable) { + createNotification({ + type: "error", + text: "Failed to update default role" + }); + return; + } + field.onChange(e); + }} + id={`permissions.admin-console.${action}`} + > + {label} + + )} + /> + ); + })} +
+ + + )} + + ); +}; diff --git a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx index 6f4cc7c88..7f6c3953e 100644 --- a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx +++ b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx @@ -61,7 +61,10 @@ const getPermissionList = (option: string) => { type Props = { isEditable: boolean; title: string; - formName: keyof Omit, "workspace">; + formName: keyof Omit< + Exclude, + "workspace" | "admin-console" + >; setValue: UseFormSetValue; control: Control; }; diff --git a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx index fe19620f5..f4b237cfe 100644 --- a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -12,6 +12,8 @@ import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils"; +import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow"; +import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow"; import { RolePermissionRow } from "./RolePermissionRow"; const SIMPLE_PERMISSION_OPTIONS = [ @@ -153,6 +155,16 @@ export const RolePermissionsSection = ({ roleId }: Props) => { /> ); })} + + diff --git a/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx index d2b66ad18..8461cc893 100644 --- a/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx +++ b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx @@ -23,133 +23,145 @@ import { Td, Th, THead, - Tr} from "@app/components/v2"; + Tr +} from "@app/components/v2"; +import { + OrgPermissionAdminConsoleAction, + OrgPermissionSubjects +} from "@app/context/OrgPermissionContext/types"; +import { withPermission } from "@app/hoc"; import { useDebounce } from "@app/hooks"; import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api"; -export const OrgAdminProjects = () => { - const [page, setPage] = useState(1); - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search); - const [perPage, setPerPage] = useState(25); - const router = useRouter(); - const orgAdminAccessProject = useOrgAdminAccessProject(); +export const OrgAdminProjects = withPermission( + () => { + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search); + const [perPage, setPerPage] = useState(25); + const router = useRouter(); + const orgAdminAccessProject = useOrgAdminAccessProject(); - const { data, isLoading: isProjectsLoading } = useOrgAdminGetProjects({ - offset: (page - 1) * perPage, - limit: perPage, - search: debouncedSearch || undefined - }); + const { data, isLoading: isProjectsLoading } = useOrgAdminGetProjects({ + offset: (page - 1) * perPage, + limit: perPage, + search: debouncedSearch || undefined + }); - const projects = data?.projects || []; - const projectCount = data?.count || 0; - const isEmpty = !isProjectsLoading && projects.length === 0; + const projects = data?.projects || []; + const projectCount = data?.count || 0; + const isEmpty = !isProjectsLoading && projects.length === 0; - const handleAccessProject = async (projectId: string) => { - try { - await orgAdminAccessProject.mutateAsync({ - projectId - }); - await router.push({ - pathname: "/project/[projectId]/secrets/overview", - query: { + const handleAccessProject = async (projectId: string) => { + try { + await orgAdminAccessProject.mutateAsync({ projectId - } - }); - } catch { - createNotification({ - text: "Failed to access project", - type: "error" - }); - } - }; + }); + await router.push({ + pathname: "/project/[projectId]/secrets/overview", + query: { + projectId + } + }); + } catch { + createNotification({ + text: "Failed to access project", + type: "error" + }); + } + }; - return ( - -
-
-

Projects

+ return ( + +
+
+

Projects

+
+
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search by project name" + /> + + + + + + + + + + + {isProjectsLoading && } + {!isProjectsLoading && + projects?.map(({ name, slug, createdAt, id }) => ( + + + + + + + ))} + +
NameSlugCreated At +
{name}{slug}{format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa")} +
+ + + + + + { + e.stopPropagation(); + e.preventDefault(); + handleAccessProject(id); + }} + icon={} + disabled={ + orgAdminAccessProject.variables?.projectId === id && + orgAdminAccessProject.isLoading + } + > + Access{" "} + {orgAdminAccessProject.variables?.projectId === id && + orgAdminAccessProject.isLoading && } + + + +
+
+ {!isProjectsLoading && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {isEmpty && } +
+
-
- setSearch(e.target.value)} - leftIcon={} - placeholder="Search by project name" - /> - - - - - - - - - - - {isProjectsLoading && } - {!isProjectsLoading && - projects?.map(({ name, slug, createdAt, id }) => ( - - - - - - - ))} - -
NameSlugCreated At -
{name}{slug}{format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa")} -
- - - - - - { - e.stopPropagation(); - e.preventDefault(); - handleAccessProject(id); - }} - icon={} - disabled={ - orgAdminAccessProject.variables?.projectId === id && - orgAdminAccessProject.isLoading - } - > - Access{" "} - {orgAdminAccessProject.variables?.projectId === id && - orgAdminAccessProject.isLoading && } - - - -
-
- {!isProjectsLoading && ( - setPage(newPage)} - onChangePerPage={(newPerPage) => setPerPage(newPerPage)} - /> - )} - {isEmpty && } -
-
-
- - ); -}; + + ); + }, + { + action: OrgPermissionAdminConsoleAction.GrantAccessProjects, + subject: OrgPermissionSubjects.AdminConsole + } +); From 711a4179ce23ee8abd104e923cf1dcbfe7a266a7 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 3 Aug 2024 07:52:35 -0400 Subject: [PATCH 26/41] rename admin panel --- frontend/src/layouts/AppLayout/AppLayout.tsx | 2 +- frontend/src/views/OrgAdminPage/OrgAdminPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 0568a5821..6a65e32e8 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -482,7 +482,7 @@ export const AppLayout = ({ children }: LayoutProps) => { )} - Admin Panel + Organization Admin Console
diff --git a/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx b/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx index eadd732d3..406b65c69 100644 --- a/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx +++ b/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx @@ -14,7 +14,7 @@ export const OrgAdminPage = () => {
-

Admin Panel

+

Organization Admin Console

setActiveTab(el as TabSections)}> From cd06e4e7f30b10de7166985e47ec9e4dcc661081 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 3 Aug 2024 19:05:01 -0400 Subject: [PATCH 27/41] hot patch --- .../src/services/secret-v2-bridge/secret-v2-bridge-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 34e568ca7..2cb03cf6d 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -490,10 +490,10 @@ export const secretV2BridgeServiceFactory = ({ ...secret, value: secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() - : undefined, + : "", comment: secret.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : undefined + : "" }) ); const expandSecretReferences = expandSecretReferencesFactory({ From ad4393fdef0546942b9c318e70021d60b07ace34 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 4 Aug 2024 11:46:58 -0400 Subject: [PATCH 28/41] update broken image in ksm docs --- docs/documentation/platform/kms/aws-kms.mdx | 2 +- docs/mint.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/documentation/platform/kms/aws-kms.mdx b/docs/documentation/platform/kms/aws-kms.mdx index 14769f301..f9fa54b4d 100644 --- a/docs/documentation/platform/kms/aws-kms.mdx +++ b/docs/documentation/platform/kms/aws-kms.mdx @@ -16,7 +16,7 @@ Before you begin, you'll first need to choose a method of authentication with AW 1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console. - ![IAM Role Creation](../../images/integrations/aws/integration-aws-iam-assume-role.png) + ![IAM Role Creation](/images/integrations/aws/integration-aws-iam-assume-role.png) 2. Select **AWS Account** as the **Trusted Entity Type**. 3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted. diff --git a/docs/mint.json b/docs/mint.json index 537ea55b5..ed824284b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -155,7 +155,7 @@ ] }, { - "group": "Key Management", + "group": "Key Management (KMS)", "pages": [ "documentation/platform/kms/overview", "documentation/platform/kms/aws-kms", From 9721d7a15ec187b3be9ad032373460afa20a61cf Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Sun, 4 Aug 2024 14:04:09 -0700 Subject: [PATCH 29/41] add meetings article to handbook --- company/handbook/meetings.mdx | 15 +++++++++++++++ company/mint.json | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 company/handbook/meetings.mdx diff --git a/company/handbook/meetings.mdx b/company/handbook/meetings.mdx new file mode 100644 index 000000000..af6a3b54e --- /dev/null +++ b/company/handbook/meetings.mdx @@ -0,0 +1,15 @@ +--- +title: "Meetings" +sidebarTitle: "Meetings" +description: "The guide to meetings at Infisical." +--- + +## "Let's schedule a meeting about this" + +Being a remote-first company, we try to be as async as possible. When an issue arises, it's best to create a public Slack thread and tag all the necessary team members. Otherwise, if you were to "put a meeting on a calendar", the decision making process will inevitable slow down by at least a day (e.g., trying to find the right time for folks in different time zones is not always straightforward). + +In other words, we have almost no (recurring) meetings and prefer written communication or quick Slack huddles. + +## Weekly All-hands + +All-hands is the single recurring meeting that we run every Monday at 8:30am PT. Typically, we would discuss everything important that happened during the previous week and plan out the week ahead. This is also an opportunity to bring up any important topics in front of the whole company (but feel free to post those in Slack too). diff --git a/company/mint.json b/company/mint.json index e6ef851cc..d5f6395e5 100644 --- a/company/mint.json +++ b/company/mint.json @@ -59,7 +59,8 @@ "handbook/onboarding", "handbook/spending-money", "handbook/time-off", - "handbook/hiring" + "handbook/hiring", + "handbook/meetings" ] } ], From e4a21379911395e85f5739ed0d8243c2a4d06b60 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 5 Aug 2024 10:01:15 -0400 Subject: [PATCH 30/41] update permission action name for org admin console --- backend/src/ee/services/permission/org-permission.ts | 4 ++-- backend/src/services/org-admin/org-admin-service.ts | 4 ++-- frontend/src/context/OrgPermissionContext/types.ts | 2 +- .../Org/RolePage/components/OrgRoleModifySection.utils.ts | 2 +- .../OrgPermissionAdminConsoleRow.tsx | 8 ++++---- .../components/OrgAdminProjects/OrgAdminProjects.tsx | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index c07107912..9a5afe024 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -10,7 +10,7 @@ export enum OrgPermissionActions { } export enum OrgPermissionAdminConsoleAction { - GrantAccessProjects = "grant-access-projects" + AccessAllProjects = "access-all-projects" } export enum OrgPermissionSubjects { @@ -113,7 +113,7 @@ const buildAdminPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms); - can(OrgPermissionAdminConsoleAction.GrantAccessProjects, OrgPermissionSubjects.AdminConsole); + can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); return build({ conditionsMatcher }); }; diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index cfdda663d..4759db309 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -53,7 +53,7 @@ export const orgAdminServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionAdminConsoleAction.GrantAccessProjects, + OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole ); const projects = await projectDAL.find( @@ -85,7 +85,7 @@ export const orgAdminServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionAdminConsoleAction.GrantAccessProjects, + OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole ); diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index a36aaac69..9d846a065 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -25,7 +25,7 @@ export enum OrgPermissionSubjects { } export enum OrgPermissionAdminConsoleAction { - GrantAccessProjects = "grant-access-projects" + AccessAllProjects = "access-all-projects" } export type OrgPermissionSet = diff --git a/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts b/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts index ba5308cd9..2d83bf705 100644 --- a/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts +++ b/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts @@ -14,7 +14,7 @@ const generalPermissionSchema = z const adminConsolePermissionSchmea = z .object({ - "grant-access-projects": z.boolean().optional() + "access-all-projects": z.boolean().optional() }) .optional(); diff --git a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx index 21d58aabb..c657aa8a4 100644 --- a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx +++ b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx @@ -20,7 +20,7 @@ enum Permission { } const PERMISSION_ACTIONS = [ - { action: "grant-access-projects", label: "Grant access projects" } + { action: "access-all-projects", label: "Access all organization projects" } ] as const; export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: Props) => { @@ -33,7 +33,7 @@ export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: }); const selectedPermissionCategory = useMemo(() => { - if (rule?.["grant-access-projects"]) { + if (rule?.["access-all-projects"]) { return Permission.Custom; } return Permission.NoAccess; @@ -63,7 +63,7 @@ export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: if (val === Permission.NoAccess) { setValue( "permissions.admin-console", - { "grant-access-projects": false }, + { "access-all-projects": false }, { shouldDirty: true } ); } @@ -78,7 +78,7 @@ export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: - Admin Console + Organization Admin Console field.onChange(Number(e.target.value))} - /> - - )} - /> Date: Tue, 6 Aug 2024 10:07:41 -0400 Subject: [PATCH 40/41] update text when secrets deleted after integ delete --- .../components/IntegrationsSection/IntegrationsSection.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx index 19f21eb1a..0be10f129 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -311,7 +311,7 @@ export const IntegrationsSection = ({ checkIndicatorBg="text-white" onCheckedChange={() => setShouldDeleteSecrets.toggle()} > - Delete secrets in destination + Delete previously synced secrets from the destination
)} @@ -321,7 +321,7 @@ export const IntegrationsSection = ({ title={`Are you sure you also want to delete secrets on ${ (popUp?.deleteConfirmation.data as TIntegration)?.integration }?`} - subTitle="By confirming, all secrets managed by this integration will be deleted in the destination. This action is irreversible." + subTitle="By confirming, you acknowledge that all secrets managed by this integration will be removed from the destination. This action is irreversible." onChange={(isOpen) => handlePopUpToggle("deleteSecretsConfirmation", isOpen)} deleteKey="confirm" onDeleteApproved={async () => { From d1c142e5b121d1cbacfe43a785d65235ee0ad579 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 6 Aug 2024 12:39:06 -0400 Subject: [PATCH 41/41] patch findProjectUserWorkspaceKey --- backend/src/services/project-bot/project-bot-dal.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/services/project-bot/project-bot-dal.ts b/backend/src/services/project-bot/project-bot-dal.ts index 81c177d21..ecb23f78b 100644 --- a/backend/src/services/project-bot/project-bot-dal.ts +++ b/backend/src/services/project-bot/project-bot-dal.ts @@ -46,6 +46,7 @@ export const projectBotDALFactory = (db: TDbClient) => { const doc = await db .replicaNode()(TableName.ProjectMembership) .where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId) + .where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId) .where(`${TableName.Users}.isGhost` as "isGhost", false) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`)