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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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 683e3dd7be8b20810e1f34c2aea9c34fb19e6636 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 31 Jul 2024 13:57:47 -0700 Subject: [PATCH 18/39] Add sign certificate endpoint --- .../ee/services/audit-log/audit-log-types.ts | 11 + backend/src/lib/api-docs/constants.ts | 17 +- .../routes/v1/certificate-authority-router.ts | 81 ++++++- .../certificate-authority-fns.ts | 37 ++++ .../certificate-authority-service.ts | 208 +++++++++++++++++- .../certificate-authority-types.ts | 11 + .../certificate-authorities/sign-cert.mdx | 4 + .../platform/pki/certificates.mdx | 28 ++- docs/mint.json | 1 + 9 files changed, 391 insertions(+), 7 deletions(-) create mode 100644 docs/api-reference/endpoints/certificate-authorities/sign-cert.mdx 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 76bf73d8c..bfc1dbf92 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -136,6 +136,7 @@ export enum EventType { IMPORT_CA_CERT = "import-certificate-authority-cert", GET_CA_CRL = "get-certificate-authority-crl", ISSUE_CERT = "issue-cert", + SIGN_CERT = "sign-cert", GET_CERT = "get-cert", DELETE_CERT = "delete-cert", REVOKE_CERT = "revoke-cert", @@ -1143,6 +1144,15 @@ interface IssueCert { }; } +interface SignCert { + type: EventType.SIGN_CERT; + metadata: { + caId: string; + dn: string; + serialNumber: string; + }; +} + interface GetCert { type: EventType.GET_CERT; metadata: { @@ -1333,6 +1343,7 @@ export type Event = | ImportCaCert | GetCaCrl | IssueCert + | SignCert | GetCert | DeleteCert | RevokeCert diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index e151ffb85..aa7d2fc51 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1056,7 +1056,7 @@ export const CERTIFICATE_AUTHORITIES = { }, SIGN_INTERMEDIATE: { caId: "The ID of the CA to sign the intermediate certificate with", - csr: "The CSR to sign with the CA", + csr: "The pem-encoded CSR to sign with the CA", notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format", notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format", maxPathLength: @@ -1086,6 +1086,21 @@ export const CERTIFICATE_AUTHORITIES = { privateKey: "The private key of the issued certificate", serialNumber: "The serial number of the issued certificate" }, + SIGN_CERT: { + caId: "The ID of the CA to issue the certificate from", + csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance", + friendlyName: "A friendly name for the certificate", + commonName: "The common name (CN) for the certificate", + altNames: + "A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.", + ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...", + notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format", + notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format", + certificate: "The issued certificate", + issuingCaCertificate: "The certificate of the issuing CA", + certificateChain: "The certificate chain of the issued certificate", + serialNumber: "The serial number of the issued certificate" + }, GET_CRL: { caId: "The ID of the CA to get the certificate revocation list (CRL) for", crl: "The certificate revocation list (CRL) of the CA" diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 533f249f2..1a6e30302 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -337,7 +337,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId) }), body: z.object({ - csr: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr), + csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore), notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter), maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength) @@ -453,7 +453,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }), body: z .object({ - friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName), + friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName), commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName), altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames), ttl: z @@ -516,4 +516,81 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }; } }); + + server.route({ + method: "POST", + url: "/:caId/sign-certificate", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Sign certificate from CA", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId) + }), + body: z + .object({ + csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr), + friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName), + commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName), + altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames), + ttl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl), + notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore), + notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter) + }) + .refine( + (data) => { + const { ttl, notAfter } = data; + return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined); + }, + { + message: "Either ttl or notAfter must be present, but not both", + path: ["ttl", "notAfter"] + } + ), + response: { + 200: z.object({ + certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.certificate), + issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate), + certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain), + serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } = + await server.services.certificateAuthority.signCertFromCa({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.SIGN_CERT, + metadata: { + caId: ca.id, + dn: ca.dn, + serialNumber + } + } + }); + + return { + certificate, + certificateChain, + issuingCaCertificate, + serialNumber + }; + } + }); }; diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index 32ac4bfb1..e1edfd198 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -18,6 +18,43 @@ export const createDistinguishedName = (parts: TDNParts) => { return dnParts.join(", "); }; +export const parseDistinguishedName = (dn: string): TDNParts => { + const parts: TDNParts = {}; + const dnRegex = /(?:^|,\s*)([A-Z]+)=([^,]+)/g; + let match: RegExpExecArray | null; + + while (true) { + match = dnRegex.exec(dn); + if (match === null) break; + + const [, key, value] = match; + switch (key) { + case "C": + parts.country = value; + break; + case "O": + parts.organization = value; + break; + case "OU": + parts.ou = value; + break; + case "ST": + parts.province = value; + break; + case "CN": + parts.commonName = value; + break; + case "L": + parts.locality = value; + break; + default: + // Ignore unrecognized keys + break; + } + } + return parts; +}; + export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => { switch (keyAlgorithm) { case CertKeyAlgorithm.RSA_4096: diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index a1390b1f7..49bb209ef 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -22,7 +22,8 @@ import { createDistinguishedName, getCaCertChain, getCaCredentials, - keyAlgorithmToAlgCfg + keyAlgorithmToAlgCfg, + parseDistinguishedName } from "./certificate-authority-fns"; import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue"; import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; @@ -36,6 +37,7 @@ import { TGetCaDTO, TImportCertToCaDTO, TIssueCertFromCaDTO, + TSignCertFromCaDTO, TSignIntermediateDTO, TUpdateCaDTO } from "./certificate-authority-types"; @@ -651,7 +653,8 @@ export const certificateAuthorityServiceFactory = ({ }; /** - * Return new leaf certificate issued by CA with id [caId] + * Return new leaf certificate issued by CA with id [caId] and private key. + * Note: private key and CSR are generated within Infisical. */ const issueCertFromCa = async ({ caId, @@ -851,6 +854,204 @@ export const certificateAuthorityServiceFactory = ({ }; }; + /** + * Return new leaf certificate issued by CA with id [caId]. + * Note: CSR is generated externally and submitted to Infisical. + */ + const signCertFromCa = async ({ + caId, + csr, + friendlyName, + commonName, + altNames, + ttl, + notBefore, + notAfter, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TSignCertFromCaDTO) => { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) throw new BadRequestError({ message: "CA not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + ca.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); + + if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + + const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); + if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + + const decryptedCaCert = await kmsDecryptor({ + cipherTextBlob: caCert.encryptedCertificate + }); + + const caCertObj = new x509.X509Certificate(decryptedCaCert); + + const notBeforeDate = notBefore ? new Date(notBefore) : new Date(); + + let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1)); + if (notAfter) { + notAfterDate = new Date(notAfter); + } else if (ttl) { + notAfterDate = new Date(new Date().getTime() + ms(ttl)); + } + + const caCertNotBeforeDate = new Date(caCertObj.notBefore); + const caCertNotAfterDate = new Date(caCertObj.notAfter); + + // check not before constraint + if (notBeforeDate < caCertNotBeforeDate) { + throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" }); + } + + if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" }); + + // check not after constraint + if (notAfterDate > caCertNotAfterDate) { + throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); + } + + const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + + const csrObj = new x509.Pkcs10CertificateRequest(csr); + + const dn = parseDistinguishedName(csrObj.subject); + const cn = commonName || dn.commonName; + + if (!cn) + throw new BadRequestError({ + message: "A common name (CN) is required in the CSR or as a parameter to this endpoint" + }); + + const { caPrivateKey } = await getCaCredentials({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + projectDAL, + kmsService + }); + + const extensions: x509.Extension[] = [ + new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true), + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) + ]; + + if (altNames) { + const altNamesArray: { + type: "email" | "dns"; + value: string; + }[] = altNames + .split(",") + .map((name) => name.trim()) + .map((altName) => { + // check if the altName is a valid email + if (z.string().email().safeParse(altName).success) { + return { + type: "email", + value: altName + }; + } + + // check if the altName is a valid hostname + if (hostnameRegex.test(altName)) { + return { + type: "dns", + value: altName + }; + } + + // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly + throw new Error(`Invalid altName: ${altName}`); + }); + + const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); + extensions.push(altNamesExtension); + } + + const serialNumber = crypto.randomBytes(32).toString("hex"); + const leafCert = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: csrObj.subject, + issuer: caCertObj.subject, + notBefore: notBeforeDate, + notAfter: notAfterDate, + signingKey: caPrivateKey, + publicKey: csrObj.publicKey, + signingAlgorithm: alg, + extensions + }); + + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ + plainText: Buffer.from(new Uint8Array(leafCert.rawData)) + }); + + await certificateDAL.transaction(async (tx) => { + const cert = await certificateDAL.create( + { + caId: ca.id, + status: CertStatus.ACTIVE, + friendlyName: friendlyName || csrObj.subject, + commonName: cn, + altNames, + serialNumber, + notBefore: notBeforeDate, + notAfter: notAfterDate + }, + tx + ); + + await certificateBodyDAL.create( + { + certId: cert.id, + encryptedCertificate + }, + tx + ); + + return cert; + }); + + const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + return { + certificate: leafCert.toString("pem"), + certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), + issuingCaCertificate, + serialNumber, + ca + }; + }; + return { createCa, getCaById, @@ -860,6 +1061,7 @@ export const certificateAuthorityServiceFactory = ({ getCaCert, signIntermediate, importCertToCa, - issueCertFromCa + issueCertFromCa, + signCertFromCa }; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 7818c3a3e..d4c2682d2 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -81,6 +81,17 @@ export type TIssueCertFromCaDTO = { notAfter?: string; } & Omit; +export type TSignCertFromCaDTO = { + caId: string; + csr: string; + friendlyName?: string; + commonName?: string; + altNames: string; + ttl: string; + notBefore?: string; + notAfter?: string; +} & Omit; + export type TDNParts = { commonName?: string; organization?: string; diff --git a/docs/api-reference/endpoints/certificate-authorities/sign-cert.mdx b/docs/api-reference/endpoints/certificate-authorities/sign-cert.mdx new file mode 100644 index 000000000..95c8d8c65 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-authorities/sign-cert.mdx @@ -0,0 +1,4 @@ +--- +title: "Sign certificate" +openapi: "POST /api/v1/pki/ca/{caId}/sign-certificate" +--- diff --git a/docs/documentation/platform/pki/certificates.mdx b/docs/documentation/platform/pki/certificates.mdx index 7dc3bca88..1efb0c44e 100644 --- a/docs/documentation/platform/pki/certificates.mdx +++ b/docs/documentation/platform/pki/certificates.mdx @@ -74,7 +74,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. - To create a certificate, make an API request to the [Create Certificate](/api-reference/endpoints/certificate-authorities/sign-intermediate) API endpoint, + To create a certificate, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint, specifying the issuing CA. ### Sample request @@ -84,6 +84,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. --header 'Content-Type: application/json' \ --data-raw '{ "commonName": "My Certificate", + "ttl": "1y", }' ``` @@ -103,6 +104,31 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. Make sure to store the `privateKey` as it is only returned once here at the time of certificate issuance. The `certificate` and `certificateChain` will remain accessible and can be retrieved at any time. + If you have an external private key, you can also create a certificate by making an API request containing a pem-encoded CSR (Certificate Signing Request) to the [Sign Certificate](/api-reference/endpoints/certificates/sign-cert) API endpoint, specifying the issuing CA. + + ### Sample request + + ```bash Request + curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca//issue-certificate' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "csr": "...", + "ttl": "1y", + }' + ``` + + ### Sample response + + ```bash Response + { + certificate: "...", + certificateChain: "...", + issuingCaCertificate: "...", + privateKey: "...", + serialNumber: "..." + } + ``` + diff --git a/docs/mint.json b/docs/mint.json index 5c7012745..97e4bbf13 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -671,6 +671,7 @@ "api-reference/endpoints/certificate-authorities/sign-intermediate", "api-reference/endpoints/certificate-authorities/import-cert", "api-reference/endpoints/certificate-authorities/issue-cert", + "api-reference/endpoints/certificate-authorities/sign-cert", "api-reference/endpoints/certificate-authorities/crl" ] }, From 9f0d7c6d11ecfcdd206521fdc5235f2edf22c2f5 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 31 Jul 2024 14:04:52 -0700 Subject: [PATCH 19/39] Correct sign-certificate endpoint ref in docs --- docs/documentation/platform/pki/certificates.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/platform/pki/certificates.mdx b/docs/documentation/platform/pki/certificates.mdx index 1efb0c44e..ab6f4df59 100644 --- a/docs/documentation/platform/pki/certificates.mdx +++ b/docs/documentation/platform/pki/certificates.mdx @@ -109,7 +109,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca//issue-certificate' \ + curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca//sign-certificate' \ --header 'Content-Type: application/json' \ --data-raw '{ "csr": "...", From afba6368508e48f37341317fc107bf3d78623acd Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Thu, 1 Aug 2024 14:22:22 +0300 Subject: [PATCH 20/39] feat: parse full env secrets (key,value) when pasted from clipboard --- .../CreateSecretForm/CreateSecretForm.tsx | 17 +++++++++++++++++ .../CreateSecretForm/CreateSecretForm.tsx | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx index c1ce05b4c..fc625ed67 100644 --- a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -1,3 +1,4 @@ +import { ClipboardEvent } from 'react'; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -38,6 +39,7 @@ export const CreateSecretForm = ({ handleSubmit, control, reset, + setValue, formState: { errors, isSubmitting } } = useForm({ resolver: zodResolver(typeSchema) }); const { isOpen } = usePopUpState(PopUpNames.CreateSecretForm); @@ -73,6 +75,20 @@ export const CreateSecretForm = ({ } }; + const handlePaste = (e: ClipboardEvent) => { + e.preventDefault(); + const pastedContent = e.clipboardData.getData('text'); + const splitIndex = pastedContent.indexOf("="); + + if (splitIndex == -1) return + + const key = pastedContent.slice(0, splitIndex) + const value = pastedContent.slice(splitIndex + 1); + + setValue('key', key); + setValue('value', value); + } + return ( diff --git a/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx index b13293602..cfb2baecd 100644 --- a/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -1,3 +1,4 @@ +import { ClipboardEvent } from 'react'; import { Controller, useForm } from "react-hook-form"; import { faWarning } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -54,6 +55,7 @@ export const CreateSecretForm = ({ control, reset, watch, + setValue, formState: { isSubmitting, errors } } = useForm({ resolver: zodResolver(typeSchema) }); const newSecretKey = watch("key"); @@ -133,6 +135,21 @@ export const CreateSecretForm = ({ }); } }; + + const handlePaste = (e: ClipboardEvent) => { + e.preventDefault(); + const pastedContent = e.clipboardData.getData('text'); + const splitIndex = pastedContent.indexOf("="); + + if (splitIndex == -1) return + + const key = pastedContent.slice(0, splitIndex) + const value = pastedContent.slice(splitIndex + 1); + + setValue('key', key); + setValue('value', value); + } + return ( From 7467a05fc4fd9581c3083e3955e37b253ef7b937 Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Thu, 1 Aug 2024 14:42:15 +0300 Subject: [PATCH 21/39] fix(lint): fix triple equal strict check --- .../components/CreateSecretForm/CreateSecretForm.tsx | 2 +- .../components/CreateSecretForm/CreateSecretForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx index fc625ed67..d040bb859 100644 --- a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -80,7 +80,7 @@ export const CreateSecretForm = ({ const pastedContent = e.clipboardData.getData('text'); const splitIndex = pastedContent.indexOf("="); - if (splitIndex == -1) return + if (splitIndex === -1) return const key = pastedContent.slice(0, splitIndex) const value = pastedContent.slice(splitIndex + 1); diff --git a/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx index cfb2baecd..8c760bb6a 100644 --- a/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -141,7 +141,7 @@ export const CreateSecretForm = ({ const pastedContent = e.clipboardData.getData('text'); const splitIndex = pastedContent.indexOf("="); - if (splitIndex == -1) return + if (splitIndex === -1) return const key = pastedContent.slice(0, splitIndex) const value = pastedContent.slice(splitIndex + 1); From 76311a1b5fc1f49a0313c62a940b2d40f78f4e9d Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 2 Aug 2024 07:00:36 -0700 Subject: [PATCH 22/39] Update DN parsing fn --- .../certificate-authority-fns.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index e1edfd198..397aa4552 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -20,15 +20,11 @@ export const createDistinguishedName = (parts: TDNParts) => { export const parseDistinguishedName = (dn: string): TDNParts => { const parts: TDNParts = {}; - const dnRegex = /(?:^|,\s*)([A-Z]+)=([^,]+)/g; - let match: RegExpExecArray | null; + const dnParts = dn.split(/,\s*/); - while (true) { - match = dnRegex.exec(dn); - if (match === null) break; - - const [, key, value] = match; - switch (key) { + for (const part of dnParts) { + const [key, value] = part.split("="); + switch (key.toUpperCase()) { case "C": parts.country = value; break; @@ -52,6 +48,7 @@ export const parseDistinguishedName = (dn: string): TDNParts => { break; } } + return parts; }; From 995f0360fbf249c9fce2452fbf0e7850686ec90e Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Fri, 2 Aug 2024 10:22:21 -0400 Subject: [PATCH 23/39] update docker-compsoe to docker compose --- .github/workflows/check-api-for-breaking-changes.yml | 6 +++--- .github/workflows/run-backend-tests.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check-api-for-breaking-changes.yml b/.github/workflows/check-api-for-breaking-changes.yml index b6d698af4..c4cb6e451 100644 --- a/.github/workflows/check-api-for-breaking-changes.yml +++ b/.github/workflows/check-api-for-breaking-changes.yml @@ -22,14 +22,14 @@ jobs: # uncomment this when testing locally using nektos/act - uses: KengoTODA/actions-setup-docker-compose@v1 if: ${{ env.ACT }} - name: Install `docker-compose` for local simulations + name: Install `docker compose` for local simulations with: version: "2.14.2" - name: 📦Build the latest image run: docker build --tag infisical-api . working-directory: backend - name: Start postgres and redis - run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis + run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis - name: Start the server run: | echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env @@ -72,6 +72,6 @@ jobs: run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR - name: cleanup run: | - docker-compose -f "docker-compose.dev.yml" down + docker compose -f "docker-compose.dev.yml" down docker stop infisical-api docker remove infisical-api diff --git a/.github/workflows/run-backend-tests.yml b/.github/workflows/run-backend-tests.yml index edb58f9a6..1fc9deff6 100644 --- a/.github/workflows/run-backend-tests.yml +++ b/.github/workflows/run-backend-tests.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v3 - uses: KengoTODA/actions-setup-docker-compose@v1 if: ${{ env.ACT }} - name: Install `docker-compose` for local simulations + name: Install `docker compose` for local simulations with: version: "2.14.2" - name: 🔧 Setup Node 20 @@ -33,7 +33,7 @@ jobs: run: npm install working-directory: backend - name: Start postgres and redis - run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis + run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis - name: Start integration test run: npm run test:e2e working-directory: backend @@ -44,4 +44,4 @@ jobs: ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218 - name: cleanup run: | - docker-compose -f "docker-compose.dev.yml" down \ No newline at end of file + docker compose -f "docker-compose.dev.yml" down \ No newline at end of file From 13e0dd8e0feae856260f9a133a5fe9984f33ee93 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 2 Aug 2024 16:22:42 +0530 Subject: [PATCH 24/39] 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 25/39] 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 26/39] 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 7b6f4d810d7005c405db1a8486c00660ba2bc3e8 Mon Sep 17 00:00:00 2001 From: Vishv Date: Fri, 2 Aug 2024 20:51:08 +0530 Subject: [PATCH 27/39] Placeholder value is same as it's label --- .../src/components/v2/DeleteActionModal/DeleteActionModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx index 76804c137..8e4bcbe71 100644 --- a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx +++ b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx @@ -94,7 +94,7 @@ export const DeleteActionModal = ({ setInputData(e.target.value)} - placeholder="Type confirm..." + placeholder={`Type ${deleteKey} here`} /> From 43fded2350dfdc9b1bf833ea92891d39c4cc9759 Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Fri, 2 Aug 2024 20:41:47 +0300 Subject: [PATCH 28/39] refactor: take into account other delimiters --- frontend/src/helpers/parseEnvVar.ts | 26 +++++++++++++++++++ .../CreateSecretForm/CreateSecretForm.tsx | 15 +++++------ .../CreateSecretForm/CreateSecretForm.tsx | 15 +++++------ 3 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 frontend/src/helpers/parseEnvVar.ts diff --git a/frontend/src/helpers/parseEnvVar.ts b/frontend/src/helpers/parseEnvVar.ts new file mode 100644 index 000000000..20e09a16f --- /dev/null +++ b/frontend/src/helpers/parseEnvVar.ts @@ -0,0 +1,26 @@ +/** Extracts the key and value from a passed in env string based on the provided delimiters. */ +export const getKeyValue = (pastedContent: string, delimiters: string[]) => { + let splitIndex = -1 + let key = "" + let value = "" + + for (const delimiter of delimiters) { + const idx = pastedContent.indexOf(delimiter) + + if (idx !== -1) { + splitIndex = idx + break; + } + } + + // if only key is pasted + if (splitIndex === -1) { + key = pastedContent.trim() + return { key, value: "" } + } + + key = pastedContent.slice(0, splitIndex).trim() + value = pastedContent.slice(splitIndex + 1).trim() + + return { key, value } +}; diff --git a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx index d040bb859..17fad2b72 100644 --- a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -8,6 +8,7 @@ import { Button, FormControl, Input, Modal, ModalContent } from "@app/components import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { useCreateSecretV3 } from "@app/hooks/api"; import { SecretType } from "@app/hooks/api/types"; +import { getKeyValue } from '@app/helpers/parseEnvVar'; import { PopUpNames, usePopUpAction, usePopUpState } from "../../SecretMainPage.store"; @@ -77,16 +78,12 @@ export const CreateSecretForm = ({ const handlePaste = (e: ClipboardEvent) => { e.preventDefault(); - const pastedContent = e.clipboardData.getData('text'); - const splitIndex = pastedContent.indexOf("="); + const delimitters = [":", "="]; + const pastedContent = e.clipboardData.getData("text"); + const { key, value } = getKeyValue(pastedContent, delimitters); - if (splitIndex === -1) return - - const key = pastedContent.slice(0, splitIndex) - const value = pastedContent.slice(splitIndex + 1); - - setValue('key', key); - setValue('value', value); + setValue("key", key); + setValue("value", value); } return ( diff --git a/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx index 8c760bb6a..a38b3db91 100644 --- a/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/views/SecretOverviewPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -20,6 +20,7 @@ import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { useWorkspace } from "@app/context"; import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api"; import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/types"; +import { getKeyValue } from '@app/helpers/parseEnvVar'; const typeSchema = z .object({ @@ -138,16 +139,12 @@ export const CreateSecretForm = ({ const handlePaste = (e: ClipboardEvent) => { e.preventDefault(); - const pastedContent = e.clipboardData.getData('text'); - const splitIndex = pastedContent.indexOf("="); + const delimitters = [":", "="]; + const pastedContent = e.clipboardData.getData("text"); + const { key, value } = getKeyValue(pastedContent, delimitters); - if (splitIndex === -1) return - - const key = pastedContent.slice(0, splitIndex) - const value = pastedContent.slice(splitIndex + 1); - - setValue('key', key); - setValue('value', value); + setValue("key", key); + setValue("value", value); } return ( From 1aee50a7518af57aaac354b634e64cbdbdc4be46 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 3 Aug 2024 03:29:45 +0200 Subject: [PATCH 29/39] Fix: Parser improvements and lint fixes --- frontend/src/helpers/parseEnvVar.ts | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/frontend/src/helpers/parseEnvVar.ts b/frontend/src/helpers/parseEnvVar.ts index 20e09a16f..27640b515 100644 --- a/frontend/src/helpers/parseEnvVar.ts +++ b/frontend/src/helpers/parseEnvVar.ts @@ -1,26 +1,14 @@ /** Extracts the key and value from a passed in env string based on the provided delimiters. */ export const getKeyValue = (pastedContent: string, delimiters: string[]) => { - let splitIndex = -1 - let key = "" - let value = "" + const foundDelimiter = delimiters.find((delimiter) => pastedContent.includes(delimiter)); - for (const delimiter of delimiters) { - const idx = pastedContent.indexOf(delimiter) - - if (idx !== -1) { - splitIndex = idx - break; - } + if (!foundDelimiter) { + return { key: pastedContent.trim(), value: "" }; } - // if only key is pasted - if (splitIndex === -1) { - key = pastedContent.trim() - return { key, value: "" } - } - - key = pastedContent.slice(0, splitIndex).trim() - value = pastedContent.slice(splitIndex + 1).trim() - - return { key, value } + const [key, value] = pastedContent.split(foundDelimiter); + return { + key: key.trim(), + value: (value ?? "").trim() + }; }; From 71af463ad800097f19e4c486442d9e0514adf539 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 3 Aug 2024 03:49:47 +0200 Subject: [PATCH 30/39] fix format --- .../CreateSecretForm/CreateSecretForm.tsx | 13 +++++++++---- .../CreateSecretForm/CreateSecretForm.tsx | 15 ++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx index c115a9acb..c095ce47b 100644 --- a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -1,4 +1,4 @@ -import { ClipboardEvent } from 'react'; +import { ClipboardEvent } from "react"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -6,9 +6,9 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; +import { getKeyValue } from "@app/helpers/parseEnvVar"; import { useCreateSecretV3 } from "@app/hooks/api"; import { SecretType } from "@app/hooks/api/types"; -import { getKeyValue } from '@app/helpers/parseEnvVar'; import { PopUpNames, usePopUpAction, usePopUpState } from "../../SecretMainPage.store"; @@ -84,7 +84,7 @@ export const CreateSecretForm = ({ setValue("key", key); setValue("value", value); - } + }; return (
- + @@ -155,7 +155,12 @@ export const CreateSecretForm = ({ subTitle="Create & update a secret across many environments" > - + Date: Sat, 3 Aug 2024 14:55:30 +0530 Subject: [PATCH 31/39] 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 32/39] 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 33/39] 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 34/39] 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 35/39] 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 36/39] 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