diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index ed9a1d629..0a5bfee6d 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -528,6 +528,7 @@ type GetRawSecretsV3Request struct { WorkspaceId string `json:"workspaceId"` SecretPath string `json:"secretPath"` IncludeImport bool `json:"include_imports"` + Recursive bool `json:"recursive"` } type GetRawSecretsV3Response struct { diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go index 80f321396..7e8babb85 100644 --- a/cli/packages/cmd/agent.go +++ b/cli/packages/cmd/agent.go @@ -479,7 +479,7 @@ func (tm *AgentManager) GetToken() string { // Fetches a new access token using client credentials func (tm *AgentManager) FetchNewAccessToken() error { - clientID := os.Getenv("INFISICAL_UNIVERSAL_AUTH_CLIENT_ID") + clientID := os.Getenv(util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME) if clientID == "" { clientIDAsByte, err := ReadFile(tm.clientIdPath) if err != nil { @@ -509,7 +509,7 @@ func (tm *AgentManager) FetchNewAccessToken() error { // save as cache in memory tm.cachedClientSecret = clientSecret - err, loginResponse := universalAuthLogin(clientID, clientSecret) + loginResponse, err := util.UniversalAuthLogin(clientID, clientSecret) if err != nil { return err } @@ -725,20 +725,6 @@ func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, templateId } } -func universalAuthLogin(clientId string, clientSecret string) (error, api.UniversalAuthLoginResponse) { - httpClient := resty.New() - httpClient.SetRetryCount(10000). - SetRetryMaxWaitTime(20 * time.Second). - SetRetryWaitTime(5 * time.Second) - - tokenResponse, err := api.CallUniversalAuthLogin(httpClient, api.UniversalAuthLoginRequest{ClientId: clientId, ClientSecret: clientSecret}) - if err != nil { - return err, api.UniversalAuthLoginResponse{} - } - - return nil, tokenResponse -} - // runCmd represents the run command var agentCmd = &cobra.Command{ Example: ` diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go index 1a34219eb..4e08e5fa8 100644 --- a/cli/packages/cmd/export.go +++ b/cli/packages/cmd/export.go @@ -44,6 +44,11 @@ var exportCmd = &cobra.Command{ util.HandleError(err) } + includeImports, err := cmd.Flags().GetBool("include-imports") + if err != nil { + util.HandleError(err) + } + projectId, err := cmd.Flags().GetString("projectId") if err != nil { util.HandleError(err) @@ -59,8 +64,7 @@ var exportCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - infisicalToken, err := util.GetInfisicalServiceToken(cmd) - + token, err := util.GetInfisicalToken(cmd) if err != nil { util.HandleError(err, "Unable to parse flag") } @@ -75,7 +79,21 @@ var exportCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, WorkspaceId: projectId, SecretsPath: secretsPath}, "") + request := models.GetAllSecretsParameters{ + Environment: environmentName, + TagSlugs: tagSlugs, + WorkspaceId: projectId, + SecretsPath: secretsPath, + IncludeImport: includeImports, + } + + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { + request.InfisicalToken = token.Token + } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + request.UniversalAuthAccessToken = token.Token + } + + secrets, err := util.GetAllEnvironmentVariables(request, "") if err != nil { util.HandleError(err, "Unable to fetch secrets") } @@ -88,9 +106,16 @@ var exportCmd = &cobra.Command{ var output string if shouldExpandSecrets { - secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{ - InfisicalToken: infisicalToken, - }, "") + + authParams := models.ExpandSecretsAuthentication{} + + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { + authParams.InfisicalToken = token.Token + } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + authParams.UniversalAuthAccessToken = token.Token + } + + secrets = util.ExpandSecrets(secrets, authParams, "") } secrets = util.FilterSecretsByTag(secrets, tagSlugs) output, err = formatEnvs(secrets, format) @@ -110,6 +135,7 @@ func init() { exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)") exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") + exportCmd.Flags().Bool("include-imports", true, "Imported linked secrets") exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") exportCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs") exportCmd.Flags().String("projectId", "", "manually set the projectId to fetch secrets from") diff --git a/cli/packages/cmd/folder.go b/cli/packages/cmd/folder.go index 290f32c38..b306960f4 100644 --- a/cli/packages/cmd/folder.go +++ b/cli/packages/cmd/folder.go @@ -36,18 +36,33 @@ var getCmd = &cobra.Command{ } } - infisicalToken, err := util.GetInfisicalServiceToken(cmd) - + projectId, err := cmd.Flags().GetString("projectId") if err != nil { util.HandleError(err, "Unable to parse flag") } + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } foldersPath, err := cmd.Flags().GetString("path") if err != nil { util.HandleError(err, "Unable to parse flag") } - folders, err := util.GetAllFolders(models.GetAllFoldersParameters{Environment: environmentName, InfisicalToken: infisicalToken, FoldersPath: foldersPath}) + request := models.GetAllFoldersParameters{ + Environment: environmentName, + WorkspaceId: projectId, + FoldersPath: foldersPath, + } + + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { + request.InfisicalToken = token.Token + } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + request.UniversalAuthAccessToken = token.Token + } + + folders, err := util.GetAllFolders(request) if err != nil { util.HandleError(err, "Unable to get folders") } diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index f0337f658..bbb2c3a05 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -55,95 +55,157 @@ var loginCmd = &cobra.Command{ Short: "Login into your Infisical account", DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { - currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() - // if the key can't be found or there is an error getting current credentials from key ring, allow them to override - if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) { - log.Debug().Err(err) - } else if err != nil { + + loginMethod, err := cmd.Flags().GetString("method") + if err != nil { + util.HandleError(err) + } + plainOutput, err := cmd.Flags().GetBool("plain") + if err != nil { util.HandleError(err) } - if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired && len(currentLoggedInUserDetails.UserCredentials.PrivateKey) != 0 { - shouldOverride, err := userLoginMenu(currentLoggedInUserDetails.UserCredentials.Email) - if err != nil { + if loginMethod != "user" && loginMethod != "universal-auth" { + util.PrintErrorMessageAndExit("Invalid login method. Please use either 'user' or 'universal-auth'") + } + + if loginMethod == "user" { + + currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + // if the key can't be found or there is an error getting current credentials from key ring, allow them to override + if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) { + log.Debug().Err(err) + } else if err != nil { util.HandleError(err) } - if !shouldOverride { - return + if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired && len(currentLoggedInUserDetails.UserCredentials.PrivateKey) != 0 { + shouldOverride, err := userLoginMenu(currentLoggedInUserDetails.UserCredentials.Email) + if err != nil { + util.HandleError(err) + } + + if !shouldOverride { + return + } } - } - //override domain - domainQuery := true - if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && config.INFISICAL_URL_MANUAL_OVERRIDE != util.INFISICAL_DEFAULT_API_URL { - overrideDomain, err := DomainOverridePrompt() - if err != nil { - util.HandleError(err) + //override domain + domainQuery := true + if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && config.INFISICAL_URL_MANUAL_OVERRIDE != util.INFISICAL_DEFAULT_API_URL { + overrideDomain, err := DomainOverridePrompt() + if err != nil { + util.HandleError(err) + } + + //if not override set INFISICAL_URL to exported var + //set domainQuery to false + if !overrideDomain { + domainQuery = false + config.INFISICAL_URL = config.INFISICAL_URL_MANUAL_OVERRIDE + } + } - //if not override set INFISICAL_URL to exported var - //set domainQuery to false - if !overrideDomain { - domainQuery = false - config.INFISICAL_URL = config.INFISICAL_URL_MANUAL_OVERRIDE + //prompt user to select domain between Infisical cloud and self hosting + if domainQuery { + err = askForDomain() + if err != nil { + util.HandleError(err, "Unable to parse domain url") + } } + var userCredentialsToBeStored models.UserCredentials - } - - //prompt user to select domain between Infisical cloud and self hosting - if domainQuery { - err = askForDomain() - if err != nil { - util.HandleError(err, "Unable to parse domain url") - } - } - var userCredentialsToBeStored models.UserCredentials - - interactiveLogin := false - if cmd.Flags().Changed("interactive") { - interactiveLogin = true - cliDefaultLogin(&userCredentialsToBeStored) - } - - //call browser login function - if !interactiveLogin { - fmt.Println("Logging in via browser... To login via interactive mode run [infisical login -i]") - userCredentialsToBeStored, err = browserCliLogin() - if err != nil { - //default to cli login on error + interactiveLogin := false + if cmd.Flags().Changed("interactive") { + interactiveLogin = true cliDefaultLogin(&userCredentialsToBeStored) } + + //call browser login function + if !interactiveLogin { + fmt.Println("Logging in via browser... To login via interactive mode run [infisical login -i]") + userCredentialsToBeStored, err = browserCliLogin() + if err != nil { + //default to cli login on error + cliDefaultLogin(&userCredentialsToBeStored) + } + } + + err = util.StoreUserCredsInKeyRing(&userCredentialsToBeStored) + if err != nil { + log.Error().Msgf("Unable to store your credentials in system vault [%s]") + log.Error().Msgf("\nTo trouble shoot further, read https://infisical.com/docs/cli/faq") + log.Debug().Err(err) + //return here + util.HandleError(err) + } + + err = util.WriteInitalConfig(&userCredentialsToBeStored) + if err != nil { + util.HandleError(err, "Unable to write write to Infisical Config file. Please try again") + } + + // clear backed up secrets from prev account + util.DeleteBackupSecrets() + + whilte := color.New(color.FgGreen) + boldWhite := whilte.Add(color.Bold) + time.Sleep(time.Second * 1) + boldWhite.Printf(">>>> Welcome to Infisical!") + boldWhite.Printf(" You are now logged in as %v <<<< \n", userCredentialsToBeStored.Email) + + plainBold := color.New(color.Bold) + + plainBold.Println("\nQuick links") + fmt.Println("- Learn to inject secrets into your application at https://infisical.com/docs/cli/usage") + fmt.Println("- Stuck? Join our slack for quick support https://infisical.com/slack") + Telemetry.CaptureEvent("cli-command:login", posthog.NewProperties().Set("infisical-backend", config.INFISICAL_URL).Set("version", util.CLI_VERSION)) + } else if loginMethod == "universal-auth" { + + clientId, err := cmd.Flags().GetString("client-id") + if err != nil { + util.HandleError(err) + } + + clientSecret, err := cmd.Flags().GetString("client-secret") + if err != nil { + util.HandleError(err) + } + + if clientId == "" { + clientId = os.Getenv(util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME) + if clientId == "" { + util.PrintErrorMessageAndExit("Please provide client-id") + } + } + if clientSecret == "" { + clientSecret = os.Getenv(util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME) + if clientSecret == "" { + util.PrintErrorMessageAndExit("Please provide client-secret") + } + } + + res, err := util.UniversalAuthLogin(clientId, clientSecret) + + if err != nil { + util.HandleError(err) + } + + if plainOutput { + fmt.Println(res.AccessToken) + return + } + + boldGreen := color.New(color.FgGreen).Add(color.Bold) + boldPlain := color.New(color.Bold) + time.Sleep(time.Second * 1) + boldGreen.Printf(">>>> Successfully authenticated with Universal Auth!\n\n") + boldPlain.Printf("Universal Auth Access Token:\n%v", res.AccessToken) + + plainBold := color.New(color.Bold) + plainBold.Println("\n\nYou can use this access token to authenticate through other commands in the CLI.") + } - - err = util.StoreUserCredsInKeyRing(&userCredentialsToBeStored) - if err != nil { - log.Error().Msgf("Unable to store your credentials in system vault [%s]") - log.Error().Msgf("\nTo trouble shoot further, read https://infisical.com/docs/cli/faq") - log.Debug().Err(err) - //return here - util.HandleError(err) - } - - err = util.WriteInitalConfig(&userCredentialsToBeStored) - if err != nil { - util.HandleError(err, "Unable to write write to Infisical Config file. Please try again") - } - - // clear backed up secrets from prev account - util.DeleteBackupSecrets() - - whilte := color.New(color.FgGreen) - boldWhite := whilte.Add(color.Bold) - time.Sleep(time.Second * 1) - boldWhite.Printf(">>>> Welcome to Infisical!") - boldWhite.Printf(" You are now logged in as %v <<<< \n", userCredentialsToBeStored.Email) - - plainBold := color.New(color.Bold) - - plainBold.Println("\nQuick links") - fmt.Println("- Learn to inject secrets into your application at https://infisical.com/docs/cli/usage") - fmt.Println("- Stuck? Join our slack for quick support https://infisical.com/slack") - Telemetry.CaptureEvent("cli-command:login", posthog.NewProperties().Set("infisical-backend", config.INFISICAL_URL).Set("version", util.CLI_VERSION)) }, } @@ -313,6 +375,10 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) { func init() { rootCmd.AddCommand(loginCmd) loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line") + loginCmd.Flags().String("method", "user", "login method [user, universal-auth]") + loginCmd.Flags().String("client-id", "", "client id for universal auth") + loginCmd.Flags().Bool("plain", false, "only output the token without any formatting") + loginCmd.Flags().String("client-secret", "", "client secret for universal auth") } func DomainOverridePrompt() (bool, error) { diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index 9c7814ecc..06846260f 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -40,8 +40,14 @@ func init() { rootCmd.PersistentFlags().StringP("log-level", "l", "info", "log level (trace, debug, info, warn, error, fatal)") rootCmd.PersistentFlags().Bool("telemetry", true, "Infisical collects non-sensitive telemetry data to enhance features and improve user experience. Participation is voluntary") rootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", util.INFISICAL_DEFAULT_API_URL, "Point the CLI to your own backend [can also set via environment variable name: INFISICAL_API_URL]") + rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.") rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { - if !util.IsRunningInDocker() { + silent, err := cmd.Flags().GetBool("silent") + if err != nil { + util.HandleError(err) + } + + if !util.IsRunningInDocker() && !silent { util.CheckForUpdate() } } diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index cb44e3d7e..3fe151d0b 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -62,8 +62,7 @@ var runCmd = &cobra.Command{ } } - infisicalToken, err := util.GetInfisicalServiceToken(cmd) - + token, err := util.GetInfisicalToken(cmd) if err != nil { util.HandleError(err, "Unable to parse flag") } @@ -73,6 +72,11 @@ var runCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + secretOverriding, err := cmd.Flags().GetBool("secret-overriding") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -103,7 +107,22 @@ var runCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports, Recursive: recursive}, projectConfigDir) + request := models.GetAllSecretsParameters{ + Environment: environmentName, + WorkspaceId: projectId, + TagSlugs: tagSlugs, + SecretsPath: secretsPath, + IncludeImport: includeImports, + Recursive: recursive, + } + + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { + request.InfisicalToken = token.Token + } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + request.UniversalAuthAccessToken = token.Token + } + + secrets, err := util.GetAllEnvironmentVariables(request, projectConfigDir) if err != nil { util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") @@ -116,9 +135,16 @@ var runCmd = &cobra.Command{ } if shouldExpandSecrets { - secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{ - InfisicalToken: infisicalToken, - }, projectConfigDir) + + authParams := models.ExpandSecretsAuthentication{} + + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { + authParams.InfisicalToken = token.Token + } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + authParams.UniversalAuthAccessToken = token.Token + } + + secrets = util.ExpandSecrets(secrets, authParams, projectConfigDir) } secretsByKey := getSecretsByKeys(secrets) @@ -149,7 +175,15 @@ var runCmd = &cobra.Command{ log.Debug().Msgf("injecting the following environment variables into shell: %v", env) - Telemetry.CaptureEvent("cli-command:run", posthog.NewProperties().Set("secretsCount", len(secrets)).Set("environment", environmentName).Set("isUsingServiceToken", infisicalToken != "").Set("single-command", strings.Join(args, " ")).Set("multi-command", cmd.Flag("command").Value.String()).Set("version", util.CLI_VERSION)) + Telemetry.CaptureEvent("cli-command:run", + posthog.NewProperties(). + Set("secretsCount", len(secrets)). + Set("environment", environmentName). + Set("isUsingServiceToken", token.Type == util.SERVICE_TOKEN_IDENTIFIER). + Set("isUsingUniversalAuthToken", token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER). + Set("single-command", strings.Join(args, " ")). + Set("multi-command", cmd.Flag("command").Value.String()). + Set("version", util.CLI_VERSION)) if cmd.Flags().Changed("command") { command := cmd.Flag("command").Value.String() @@ -204,6 +238,7 @@ func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) { func init() { rootCmd.AddCommand(runCmd) runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") + runCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") runCmd.Flags().Bool("include-imports", true, "Import linked secrets ") diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 4e1b09c9e..7ba6e5411 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -38,12 +38,12 @@ var secretsCmd = &cobra.Command{ } } - infisicalToken, err := util.GetInfisicalServiceToken(cmd) - + token, err := util.GetInfisicalToken(cmd) if err != nil { util.HandleError(err, "Unable to parse flag") } + projectId, err := cmd.Flags().GetString("projectId") if err != nil { util.HandleError(err, "Unable to parse flag") } @@ -78,7 +78,22 @@ var secretsCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports, Recursive: recursive}, "") + request := models.GetAllSecretsParameters{ + Environment: environmentName, + WorkspaceId: projectId, + TagSlugs: tagSlugs, + SecretsPath: secretsPath, + IncludeImport: includeImports, + Recursive: recursive, + } + + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { + request.InfisicalToken = token.Token + } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + request.UniversalAuthAccessToken = token.Token + } + + secrets, err := util.GetAllEnvironmentVariables(request, "") if err != nil { util.HandleError(err) } @@ -90,9 +105,15 @@ var secretsCmd = &cobra.Command{ } if shouldExpandSecrets { - secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{ - InfisicalToken: infisicalToken, - }, "") + + authParams := models.ExpandSecretsAuthentication{} + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { + authParams.InfisicalToken = token.Token + } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + authParams.UniversalAuthAccessToken = token.Token + } + + secrets = util.ExpandSecrets(secrets, authParams, "") } visualize.PrintAllSecretDetails(secrets) @@ -402,8 +423,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { } } - infisicalToken, err := util.GetInfisicalServiceToken(cmd) + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + shouldExpand, err := cmd.Flags().GetBool("expand") if err != nil { util.HandleError(err, "Unable to parse flag") } @@ -413,6 +438,11 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse flag") } + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + secretsPath, err := cmd.Flags().GetString("path") if err != nil { util.HandleError(err, "Unable to parse path flag") @@ -428,11 +458,37 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse path flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true, Recursive: recursive}, "") + request := models.GetAllSecretsParameters{ + Environment: environmentName, + WorkspaceId: projectId, + TagSlugs: tagSlugs, + SecretsPath: secretsPath, + IncludeImport: true, + Recursive: recursive, + } + + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { + request.InfisicalToken = token.Token + } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + request.UniversalAuthAccessToken = token.Token + } + + secrets, err := util.GetAllEnvironmentVariables(request, "") if err != nil { util.HandleError(err, "To fetch all secrets") } + if shouldExpand { + authParams := models.ExpandSecretsAuthentication{} + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { + authParams.InfisicalToken = token.Token + } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + authParams.UniversalAuthAccessToken = token.Token + } + + secrets = util.ExpandSecrets(secrets, authParams, "") + } + requestedSecrets := []models.SingleEnvironmentVariable{} secretsMap := getSecretsByKeys(secrets) @@ -475,8 +531,12 @@ func generateExampleEnv(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse flag") } - infisicalToken, err := util.GetInfisicalServiceToken(cmd) + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + projectId, err := cmd.Flags().GetString("projectId") if err != nil { util.HandleError(err, "Unable to parse flag") } @@ -486,7 +546,21 @@ func generateExampleEnv(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true}, "") + request := models.GetAllSecretsParameters{ + Environment: environmentName, + WorkspaceId: projectId, + TagSlugs: tagSlugs, + SecretsPath: secretsPath, + IncludeImport: true, + } + + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { + request.InfisicalToken = token.Token + } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + request.UniversalAuthAccessToken = token.Token + } + + secrets, err := util.GetAllEnvironmentVariables(request, "") if err != nil { util.HandleError(err, "To fetch all secrets") } @@ -686,19 +760,23 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod func init() { secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") + secretsGenerateExampleEnvCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") secretsGenerateExampleEnvCmd.Flags().String("path", "/", "Fetch secrets from within a folder path") secretsCmd.AddCommand(secretsGenerateExampleEnvCmd) secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") - secretsCmd.AddCommand(secretsGetCmd) + secretsGetCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path") + secretsGetCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret") secretsGetCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") + secretsCmd.AddCommand(secretsGetCmd) secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") secretsCmd.AddCommand(secretsSetCmd) secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path") + // Only supports logged in users (JWT auth) secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { util.RequireLogin() util.RequireLocalWorkspaceFile() @@ -707,6 +785,8 @@ func init() { secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)") secretsDeleteCmd.Flags().String("path", "/", "get secrets within a folder path") secretsCmd.AddCommand(secretsDeleteCmd) + + // Only supports logged in users (JWT auth) secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { util.RequireLogin() util.RequireLocalWorkspaceFile() @@ -718,6 +798,7 @@ func init() { // Add getCmd, createCmd and deleteCmd flags here getCmd.Flags().StringP("path", "p", "/", "The path from where folders should be fetched from") getCmd.Flags().String("token", "", "Fetch folders using the infisical token") + getCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") folderCmd.AddCommand(getCmd) // Add createCmd flags here @@ -735,6 +816,7 @@ func init() { // ** End of folders sub command secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") + secretsCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ") diff --git a/cli/packages/cmd/token.go b/cli/packages/cmd/token.go new file mode 100644 index 000000000..3e5d42765 --- /dev/null +++ b/cli/packages/cmd/token.go @@ -0,0 +1,63 @@ +/* +Copyright (c) 2023 Infisical Inc. +*/ +package cmd + +import ( + "strings" + "time" + + "github.com/Infisical/infisical-merge/packages/util" + "github.com/fatih/color" + "github.com/spf13/cobra" +) + +var tokenCmd = &cobra.Command{ + Use: "token", + Short: "Manage your access tokens", + DisableFlagsInUseLine: true, + Example: "infisical token", + Args: cobra.ExactArgs(0), + PreRun: func(cmd *cobra.Command, args []string) { + util.RequireLogin() + }, + Run: func(cmd *cobra.Command, args []string) { + }, +} + +var tokenRenewCmd = &cobra.Command{ + Use: "renew [token]", + Short: "Used to renew your universal auth access token", + DisableFlagsInUseLine: true, + Example: "infisical token renew ", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + // args[0] will be the from your command call + token := args[0] + + if strings.HasPrefix(token, "st.") { + util.PrintErrorMessageAndExit("You are trying to renew a service token. You can only renew universal auth access tokens.") + } + + renewedAccessToken, err := util.RenewUniversalAuthAccessToken(token) + + if err != nil { + util.HandleError(err, "Unable to renew token") + } + + boldGreen := color.New(color.FgGreen).Add(color.Bold) + time.Sleep(time.Second * 1) + boldGreen.Printf(">>>> Successfully renewed token!\n\n") + boldGreen.Printf("Renewed Access Token:\n%v", renewedAccessToken) + + plainBold := color.New(color.Bold) + plainBold.Println("\n\nYou can use the new access token to authenticate through other commands in the CLI.") + + }, +} + +func init() { + tokenCmd.AddCommand(tokenRenewCmd) + + rootCmd.AddCommand(tokenCmd) +} diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index e197218e0..68527c469 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -59,6 +59,11 @@ type DynamicSecretLease struct { Data map[string]interface{} `json:"data"` } +type TokenDetails struct { + Type string + Token string +} + type SingleFolder struct { ID string `json:"_id"` Name string `json:"name"` @@ -97,10 +102,11 @@ type GetAllSecretsParameters struct { } type GetAllFoldersParameters struct { - WorkspaceId string - Environment string - FoldersPath string - InfisicalToken string + WorkspaceId string + Environment string + FoldersPath string + InfisicalToken string + UniversalAuthAccessToken string } type CreateFolderParameters struct { @@ -123,3 +129,8 @@ type ExpandSecretsAuthentication struct { InfisicalToken string UniversalAuthAccessToken string } + +type MachineIdentityCredentials struct { + ClientId string + ClientSecret string +} diff --git a/cli/packages/util/constants.go b/cli/packages/util/constants.go index ee2532ee8..311a4b0d9 100644 --- a/cli/packages/util/constants.go +++ b/cli/packages/util/constants.go @@ -1,17 +1,23 @@ package util const ( - CONFIG_FILE_NAME = "infisical-config.json" - CONFIG_FOLDER_NAME = ".infisical" - INFISICAL_DEFAULT_API_URL = "https://app.infisical.com/api" - INFISICAL_DEFAULT_URL = "https://app.infisical.com" - INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" - INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" - SECRET_TYPE_PERSONAL = "personal" - SECRET_TYPE_SHARED = "shared" - KEYRING_SERVICE_NAME = "infisical" - PERSONAL_SECRET_TYPE_NAME = "personal" - SHARED_SECRET_TYPE_NAME = "shared" + CONFIG_FILE_NAME = "infisical-config.json" + CONFIG_FOLDER_NAME = ".infisical" + INFISICAL_DEFAULT_API_URL = "https://app.infisical.com/api" + INFISICAL_DEFAULT_URL = "https://app.infisical.com" + INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" + INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" + INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID" + INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET" + INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN" + SECRET_TYPE_PERSONAL = "personal" + SECRET_TYPE_SHARED = "shared" + KEYRING_SERVICE_NAME = "infisical" + PERSONAL_SECRET_TYPE_NAME = "personal" + SHARED_SECRET_TYPE_NAME = "shared" + + SERVICE_TOKEN_IDENTIFIER = "service-token" + UNIVERSAL_AUTH_TOKEN_IDENTIFIER = "universal-auth-token" ) var ( diff --git a/cli/packages/util/folders.go b/cli/packages/util/folders.go index 0f837ee71..165b97534 100644 --- a/cli/packages/util/folders.go +++ b/cli/packages/util/folders.go @@ -19,7 +19,7 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder var foldersToReturn []models.SingleFolder var folderErr error - if params.InfisicalToken == "" { + if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" { log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details") @@ -44,11 +44,24 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder folders, err := GetFoldersViaJTW(loggedInUserDetails.UserCredentials.JTWToken, workspaceFile.WorkspaceId, params.Environment, params.FoldersPath) folderErr = err foldersToReturn = folders - } else { + } else if params.InfisicalToken != "" { + log.Debug().Msg("GetAllFolders: Trying to fetch folders using service token") + // get folders via service token folders, err := GetFoldersViaServiceToken(params.InfisicalToken, params.WorkspaceId, params.Environment, params.FoldersPath) folderErr = err foldersToReturn = folders + } else if params.UniversalAuthAccessToken != "" { + log.Debug().Msg("GetAllFolders: Trying to fetch folders using universal auth") + + if params.WorkspaceId == "" { + PrintErrorMessageAndExit("Project ID is required when using machine identity") + } + + // get folders via machine identity + folders, err := GetFoldersViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.FoldersPath) + folderErr = err + foldersToReturn = folders } return foldersToReturn, folderErr } @@ -132,6 +145,34 @@ func GetFoldersViaServiceToken(fullServiceToken string, workspaceId string, envi return folders, nil } +func GetFoldersViaMachineIdentity(accessToken string, workspaceId string, envSlug string, foldersPath string) ([]models.SingleFolder, error) { + httpClient := resty.New() + httpClient.SetAuthToken(accessToken). + SetHeader("Accept", "application/json") + + getFoldersRequest := api.GetFoldersV1Request{ + WorkspaceId: workspaceId, + Environment: envSlug, + FoldersPath: foldersPath, + } + + apiResponse, err := api.CallGetFoldersV1(httpClient, getFoldersRequest) + if err != nil { + return nil, err + } + + var folders []models.SingleFolder + + for _, folder := range apiResponse.Folders { + folders = append(folders, models.SingleFolder{ + Name: folder.Name, + ID: folder.ID, + }) + } + + return folders, nil +} + // CreateFolder creates a folder in Infisical func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, error) { loggedInUserDetails, err := GetCurrentLoggedInUserDetails() diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index 11b3396dc..e5b8ab6d9 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -9,8 +9,11 @@ import ( "os/exec" "path" "strings" + "time" + "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/models" + "github.com/go-resty/resty/v2" "github.com/spf13/cobra" ) @@ -64,18 +67,70 @@ func IsSecretTypeValid(s string) bool { return false } -func GetInfisicalServiceToken(cmd *cobra.Command) (serviceToken string, err error) { +func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err error) { infisicalToken, err := cmd.Flags().GetString("token") - if infisicalToken == "" { - infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME) + if err != nil { + return nil, err } + if infisicalToken == "" { // If no flag is passed, we first check for the universal auth access token env variable. + infisicalToken = os.Getenv(INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME) + + if infisicalToken == "" { // If it's still empty after the first env check, we check for the service token env variable. + infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME) + } + } + + if infisicalToken == "" { // If it's empty, we return nothing at all. + return nil, nil + } + + if strings.HasPrefix(infisicalToken, "st.") { + return &models.TokenDetails{ + Type: SERVICE_TOKEN_IDENTIFIER, + Token: infisicalToken, + }, nil + } + + return &models.TokenDetails{ + Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER, + Token: infisicalToken, + }, nil + +} + +func UniversalAuthLogin(clientId string, clientSecret string) (api.UniversalAuthLoginResponse, error) { + httpClient := resty.New() + httpClient.SetRetryCount(10000). + SetRetryMaxWaitTime(20 * time.Second). + SetRetryWaitTime(5 * time.Second) + + tokenResponse, err := api.CallUniversalAuthLogin(httpClient, api.UniversalAuthLoginRequest{ClientId: clientId, ClientSecret: clientSecret}) + if err != nil { + return api.UniversalAuthLoginResponse{}, err + } + + return tokenResponse, nil +} + +func RenewUniversalAuthAccessToken(accessToken string) (string, error) { + + httpClient := resty.New() + httpClient.SetRetryCount(10000). + SetRetryMaxWaitTime(20 * time.Second). + SetRetryWaitTime(5 * time.Second) + + request := api.UniversalAuthRefreshRequest{ + AccessToken: accessToken, + } + + tokenResponse, err := api.CallUniversalAuthRefreshAccessToken(httpClient, request) if err != nil { return "", err } - return infisicalToken, nil + return tokenResponse.AccessToken, nil } // Checks if the passed in email already exists in the users slice diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 23e95db64..59d80ea77 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -159,7 +159,7 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin httpClient.SetAuthToken(accessToken). SetHeader("Accept", "application/json") - getSecretsRequest := api.GetEncryptedSecretsV3Request{ + getSecretsRequest := api.GetRawSecretsV3Request{ WorkspaceId: workspaceId, Environment: environmentName, IncludeImport: includeImports, @@ -171,7 +171,8 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin getSecretsRequest.SecretPath = secretsPath } - rawSecrets, err := api.CallGetRawSecretsV3(httpClient, api.GetRawSecretsV3Request{WorkspaceId: workspaceId, SecretPath: secretsPath, Environment: environmentName}) + rawSecrets, err := api.CallGetRawSecretsV3(httpClient, getSecretsRequest) + if err != nil { return models.PlaintextSecretResult{}, err } @@ -182,7 +183,7 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin } for _, secret := range rawSecrets.Secrets { - plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, WorkspaceId: secret.Workspace}) + plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, Type: secret.Type, WorkspaceId: secret.Workspace}) } // if includeImports { @@ -355,6 +356,11 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo log.Debug().Msg("Trying to fetch secrets using service token") secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) } else if params.UniversalAuthAccessToken != "" { + + if params.WorkspaceId == "" { + PrintErrorMessageAndExit("Project ID is required when using machine identity") + } + log.Debug().Msg("Trying to fetch secrets using universal auth") res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) diff --git a/docs/cli/commands/login.mdx b/docs/cli/commands/login.mdx index 3028ec110..2758ced00 100644 --- a/docs/cli/commands/login.mdx +++ b/docs/cli/commands/login.mdx @@ -12,4 +12,53 @@ The CLI uses authentication to verify your identity. When you enter the correct To change where the login credentials are stored, visit the [vaults command](./vault). -If you have added multiple users, you can switch between the users by using the [user command](./user). \ No newline at end of file +If you have added multiple users, you can switch between the users by using the [user command](./user). + + +### Flags + + ```bash + infisical login --method= # Optional, will default to 'user'. + ``` + + #### Valid values for the `method` flag are: + - `user`: Login using email and password. + - `universal-auth`: Login using a universal auth client ID and client secret. + + + When `method` is set to `universal-auth`, the `client-id` and `client-secret` flags are required. Optionally you can set the `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` and `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` environment variables instead of using the flags. + + When you authenticate with universal auth, an access token will be printed to the console upon successful login. This token can be used to authenticate with the Infisical API and the CLI by passing it in the `--token` flag when applicable. + + Use flag `--plain` along with `--silent` to print only the token in plain text when using the `universal-auth` method. + + + + + + ```bash + infisical login --client-id= # Optional, required if --method=universal-auth. + ``` + + #### Description + The client ID of the universal auth client. This is required if the `--method` flag is set to `universal-auth`. + + + The `client-id` flag can be substituted with the `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` environment variable. + + + + ```bash + infisical login --client-secret= # Optional, required if --method=universal-auth. + ``` + #### Description + The client secret of the universal auth client. This is required if the `--method` flag is set to `universal-auth`. + + + The `client-secret` flag can be substituted with the `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` environment variable. + + + + + + \ No newline at end of file diff --git a/docs/cli/commands/token.mdx b/docs/cli/commands/token.mdx new file mode 100644 index 000000000..5b0d4ad5c --- /dev/null +++ b/docs/cli/commands/token.mdx @@ -0,0 +1,21 @@ +--- +title: "infisical token" +description: "Manage your Infisical identity access tokens" +--- + +```bash +infisical service-token renew +``` + +## Description +The Infisical `token` command allows you to manage your universal auth access tokens. +With this command, you can renew your access tokens. In the future more subcommands will be added to better help you manage your tokens through the CLI. + + + Use this command to renew your access token. This command will renew your access token and output a renewed access token to the console. + + ```bash + $ infisical token renew + ``` + + diff --git a/docs/mint.json b/docs/mint.json index 6103ceda2..61d5409f8 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -232,6 +232,7 @@ "cli/commands/run", "cli/commands/secrets", "cli/commands/export", + "cli/commands/token", "cli/commands/service-token", "cli/commands/vault", "cli/commands/user",