diff --git a/packages/api/model.go b/packages/api/model.go index cdd063e7..26124c03 100644 --- a/packages/api/model.go +++ b/packages/api/model.go @@ -788,7 +788,8 @@ type RegisterGatewayResponse struct { type PAMAccessRequest struct { Duration string `json:"duration,omitempty"` - AccountPath string `json:"accountPath,omitempty"` + ResourceName string `json:"resourceName,omitempty"` + AccountName string `json:"accountName,omitempty"` ProjectId string `json:"projectId,omitempty"` MfaSessionId string `json:"mfaSessionId,omitempty"` } @@ -807,7 +808,8 @@ type PAMAccessResponse struct { } type PAMAccessApprovalRequestPayloadRequestData struct { - AccountPath string `json:"accountPath"` + ResourceName string `json:"resourceName,omitempty"` + AccountName string `json:"accountName,omitempty"` AccessDuration string `json:"accessDuration"` } diff --git a/packages/cmd/pam.go b/packages/cmd/pam.go index deb9505b..7a300837 100644 --- a/packages/cmd/pam.go +++ b/packages/cmd/pam.go @@ -17,6 +17,8 @@ var pamCmd = &cobra.Command{ Args: cobra.NoArgs, } +// ==================== Database Commands ==================== + var pamDbCmd = &cobra.Command{ Use: "db", Short: "Database-related PAM commands", @@ -25,17 +27,22 @@ var pamDbCmd = &cobra.Command{ Args: cobra.NoArgs, } -var pamDbAccessAccountCmd = &cobra.Command{ - Use: "access-account ", +var pamDbAccessCmd = &cobra.Command{ + Use: "access", Short: "Access PAM database accounts", Long: "Access PAM database accounts for Infisical. This starts a local database proxy server that you can use to connect to databases directly.", - Example: "infisical pam db access-account prod/db/my-postgres-account --duration 4h --port 5432 --project-id 1234567890", + Example: "infisical pam db access --resource infisical-shared-cloud-instances --account infisical --project-id b38bef10-2685-43c4-9a2c-635206d60bec --duration 4h", DisableFlagsInUseLine: true, - Args: cobra.ExactArgs(1), + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { util.RequireLogin() - accountPath := args[0] + resourceName, _ := cmd.Flags().GetString("resource") + accountName, _ := cmd.Flags().GetString("account") + + if resourceName == "" || accountName == "" { + util.PrintErrorMessageAndExit("Both --resource and --account flags are required") + } projectID, err := cmd.Flags().GetString("project-id") if err != nil { @@ -55,7 +62,6 @@ var pamDbAccessAccountCmd = &cobra.Command{ util.HandleError(err, "Unable to parse duration flag") } - // Parse duration _, err = time.ParseDuration(durationStr) if err != nil { util.HandleError(err, "Invalid duration format. Use formats like '1h', '30m', '2h30m'") @@ -83,10 +89,15 @@ var pamDbAccessAccountCmd = &cobra.Command{ loggedInUserDetails = util.EstablishUserLoginSession() } - pam.StartDatabaseLocalProxy(loggedInUserDetails.UserCredentials.JTWToken, accountPath, projectID, durationStr, port) + pam.StartDatabaseLocalProxy(loggedInUserDetails.UserCredentials.JTWToken, pam.PAMAccessParams{ + ResourceName: resourceName, + AccountName: accountName, + }, projectID, durationStr, port) }, } +// ==================== SSH Commands ==================== + var pamSshCmd = &cobra.Command{ Use: "ssh", Short: "SSH-related PAM commands", @@ -95,24 +106,28 @@ var pamSshCmd = &cobra.Command{ Args: cobra.NoArgs, } -var pamSshAccessAccountCmd = &cobra.Command{ - Use: "access-account ", +var pamSshAccessCmd = &cobra.Command{ + Use: "access", Short: "Start SSH session to PAM account", Long: "Start an SSH session to a PAM-managed SSH account. This command automatically launches an SSH client connected through the Infisical Gateway.", - Example: "infisical pam ssh access-account prod/ssh/my-ssh-account --duration 2h --project-id 1234567890", + Example: "infisical pam ssh access --resource prod-servers --account root --project-id b38bef10-2685-43c4-9a2c-635206d60bec --duration 1h", DisableFlagsInUseLine: true, - Args: cobra.ExactArgs(1), + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { util.RequireLogin() - accountPath := args[0] + resourceName, _ := cmd.Flags().GetString("resource") + accountName, _ := cmd.Flags().GetString("account") + + if resourceName == "" || accountName == "" { + util.PrintErrorMessageAndExit("Both --resource and --account flags are required") + } durationStr, err := cmd.Flags().GetString("duration") if err != nil { util.HandleError(err, "Unable to parse duration flag") } - // Parse duration _, err = time.ParseDuration(durationStr) if err != nil { util.HandleError(err, "Invalid duration format. Use formats like '1h', '30m', '2h30m'") @@ -148,9 +163,15 @@ var pamSshAccessAccountCmd = &cobra.Command{ loggedInUserDetails = util.EstablishUserLoginSession() } - pam.StartSSHLocalProxy(loggedInUserDetails.UserCredentials.JTWToken, accountPath, projectID, durationStr) + pam.StartSSHLocalProxy(loggedInUserDetails.UserCredentials.JTWToken, pam.PAMAccessParams{ + ResourceName: resourceName, + AccountName: accountName, + }, projectID, durationStr) }, } + +// ==================== Kubernetes Commands ==================== + var pamKubernetesCmd = &cobra.Command{ Use: "kubernetes", Aliases: []string{"k8s"}, @@ -160,24 +181,28 @@ var pamKubernetesCmd = &cobra.Command{ Args: cobra.NoArgs, } -var pamKubernetesAccessAccountCmd = &cobra.Command{ - Use: "access-account ", +var pamKubernetesAccessCmd = &cobra.Command{ + Use: "access", Short: "Access Kubernetes PAM account", Long: "Access Kubernetes via a PAM-managed Kubernetes account. This command automatically launches a proxy connected to your Kubernetes cluster through the Infisical Gateway.", - Example: "infisical pam kubernetes access-account prod/ssh/my-k8s-account --duration 2h --project-id ", + Example: "infisical pam kubernetes access --resource prod-cluster --account developer --project-id b38bef10-2685-43c4-9a2c-635206d60bec --duration 4h", DisableFlagsInUseLine: true, - Args: cobra.ExactArgs(1), + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { util.RequireLogin() - accountPath := args[0] + resourceName, _ := cmd.Flags().GetString("resource") + accountName, _ := cmd.Flags().GetString("account") + + if resourceName == "" || accountName == "" { + util.PrintErrorMessageAndExit("Both --resource and --account flags are required") + } durationStr, err := cmd.Flags().GetString("duration") if err != nil { util.HandleError(err, "Unable to parse duration flag") } - // Parse duration _, err = time.ParseDuration(durationStr) if err != nil { util.HandleError(err, "Invalid duration format. Use formats like '1h', '30m', '2h30m'") @@ -218,10 +243,15 @@ var pamKubernetesAccessAccountCmd = &cobra.Command{ loggedInUserDetails = util.EstablishUserLoginSession() } - pam.StartKubernetesLocalProxy(loggedInUserDetails.UserCredentials.JTWToken, accountPath, projectID, durationStr, port) + pam.StartKubernetesLocalProxy(loggedInUserDetails.UserCredentials.JTWToken, pam.PAMAccessParams{ + ResourceName: resourceName, + AccountName: accountName, + }, projectID, durationStr, port) }, } +// ==================== Redis Commands ==================== + var pamRedisCmd = &cobra.Command{ Use: "redis", Short: "Redis-related PAM commands", @@ -230,17 +260,22 @@ var pamRedisCmd = &cobra.Command{ Args: cobra.NoArgs, } -var pamRedisAccessAccountCmd = &cobra.Command{ - Use: "access-account ", - Short: "Access Redis PAM account", - Long: "Access Redis via a PAM-managed Redis account. This starts a local Redis proxy server that you can use to connect to Redis directly.", - Example: "infisical pam redis access-account prod/redis/my-redis-account --duration 4h --port 6379 --project-id ", +var pamRedisAccessCmd = &cobra.Command{ + Use: "access", + Short: "Access PAM Redis accounts", + Long: "Access PAM Redis accounts for Infisical. This starts a local Redis proxy server that you can use to connect to Redis directly.", + Example: "infisical pam redis access --resource my-redis-resource --account redis-admin --duration 4h --port 6379 --project-id ", DisableFlagsInUseLine: true, - Args: cobra.ExactArgs(1), + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { util.RequireLogin() - accountPath := args[0] + resourceName, _ := cmd.Flags().GetString("resource") + accountName, _ := cmd.Flags().GetString("account") + + if resourceName == "" || accountName == "" { + util.PrintErrorMessageAndExit("Both --resource and --account flags are required") + } projectID, err := cmd.Flags().GetString("project-id") if err != nil { @@ -260,7 +295,6 @@ var pamRedisAccessAccountCmd = &cobra.Command{ util.HandleError(err, "Unable to parse duration flag") } - // Parse duration _, err = time.ParseDuration(durationStr) if err != nil { util.HandleError(err, "Invalid duration format. Use formats like '1h', '30m', '2h30m'") @@ -288,29 +322,52 @@ var pamRedisAccessAccountCmd = &cobra.Command{ loggedInUserDetails = util.EstablishUserLoginSession() } - pam.StartRedisLocalProxy(loggedInUserDetails.UserCredentials.JTWToken, accountPath, projectID, durationStr, port) + pam.StartRedisLocalProxy(loggedInUserDetails.UserCredentials.JTWToken, pam.PAMAccessParams{ + ResourceName: resourceName, + AccountName: accountName, + }, projectID, durationStr, port) }, } func init() { - pamDbCmd.AddCommand(pamDbAccessAccountCmd) - pamDbAccessAccountCmd.Flags().String("duration", "1h", "Duration for database access session (e.g., '1h', '30m', '2h30m')") - pamDbAccessAccountCmd.Flags().Int("port", 0, "Port for the local database proxy server (0 for auto-assign)") - pamDbAccessAccountCmd.Flags().String("project-id", "", "Project ID of the account to access") - - pamSshCmd.AddCommand(pamSshAccessAccountCmd) - pamSshAccessAccountCmd.Flags().String("duration", "1h", "Duration for SSH access session (e.g., '1h', '30m', '2h30m')") - pamSshAccessAccountCmd.Flags().String("project-id", "", "Project ID of the account to access") - - pamKubernetesCmd.AddCommand(pamKubernetesAccessAccountCmd) - pamKubernetesAccessAccountCmd.Flags().String("duration", "1h", "Duration for kubernetes access session (e.g., '1h', '30m', '2h30m')") - pamKubernetesAccessAccountCmd.Flags().Int("port", 0, "Port for the local kubernetes proxy server (0 for auto-assign)") - pamKubernetesAccessAccountCmd.Flags().String("project-id", "", "Project ID of the account to access") - - pamRedisCmd.AddCommand(pamRedisAccessAccountCmd) - pamRedisAccessAccountCmd.Flags().String("duration", "1h", "Duration for Redis access session (e.g., '1h', '30m', '2h30m')") - pamRedisAccessAccountCmd.Flags().Int("port", 0, "Port for the local Redis proxy server (0 for auto-assign)") - pamRedisAccessAccountCmd.Flags().String("project-id", "", "Project ID of the account to access") + // Database commands + pamDbCmd.AddCommand(pamDbAccessCmd) + pamDbAccessCmd.Flags().String("resource", "", "Name of the PAM resource to access") + pamDbAccessCmd.Flags().String("account", "", "Name of the account within the resource") + pamDbAccessCmd.Flags().String("duration", "1h", "Duration for database access session (e.g., '1h', '30m', '2h30m')") + pamDbAccessCmd.Flags().Int("port", 0, "Port for the local database proxy server (0 for auto-assign)") + pamDbAccessCmd.Flags().String("project-id", "", "Project ID of the account to access") + pamDbAccessCmd.MarkFlagRequired("resource") + pamDbAccessCmd.MarkFlagRequired("account") + + // SSH commands + pamSshCmd.AddCommand(pamSshAccessCmd) + pamSshAccessCmd.Flags().String("resource", "", "Name of the PAM resource to access") + pamSshAccessCmd.Flags().String("account", "", "Name of the account within the resource") + pamSshAccessCmd.Flags().String("duration", "1h", "Duration for SSH access session (e.g., '1h', '30m', '2h30m')") + pamSshAccessCmd.Flags().String("project-id", "", "Project ID of the account to access") + pamSshAccessCmd.MarkFlagRequired("resource") + pamSshAccessCmd.MarkFlagRequired("account") + + // Kubernetes commands + pamKubernetesCmd.AddCommand(pamKubernetesAccessCmd) + pamKubernetesAccessCmd.Flags().String("resource", "", "Name of the PAM resource to access") + pamKubernetesAccessCmd.Flags().String("account", "", "Name of the account within the resource") + pamKubernetesAccessCmd.Flags().String("duration", "1h", "Duration for kubernetes access session (e.g., '1h', '30m', '2h30m')") + pamKubernetesAccessCmd.Flags().Int("port", 0, "Port for the local kubernetes proxy server (0 for auto-assign)") + pamKubernetesAccessCmd.Flags().String("project-id", "", "Project ID of the account to access") + pamKubernetesAccessCmd.MarkFlagRequired("resource") + pamKubernetesAccessCmd.MarkFlagRequired("account") + + // Redis commands + pamRedisCmd.AddCommand(pamRedisAccessCmd) + pamRedisAccessCmd.Flags().String("resource", "", "Name of the PAM resource to access") + pamRedisAccessCmd.Flags().String("account", "", "Name of the account within the resource") + pamRedisAccessCmd.Flags().String("duration", "1h", "Duration for Redis access session (e.g., '1h', '30m', '2h30m')") + pamRedisAccessCmd.Flags().Int("port", 0, "Port for the local Redis proxy server (0 for auto-assign)") + pamRedisAccessCmd.Flags().String("project-id", "", "Project ID of the account to access") + pamRedisAccessCmd.MarkFlagRequired("resource") + pamRedisAccessCmd.MarkFlagRequired("account") pamCmd.AddCommand(pamDbCmd) pamCmd.AddCommand(pamSshCmd) diff --git a/packages/pam/local/base-proxy.go b/packages/pam/local/base-proxy.go index 06e705bf..b886f7dd 100644 --- a/packages/pam/local/base-proxy.go +++ b/packages/pam/local/base-proxy.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/json" + "errors" "fmt" "io" "net" @@ -19,9 +20,39 @@ import ( "github.com/Infisical/infisical-merge/packages/pam" "github.com/Infisical/infisical-merge/packages/util" "github.com/go-resty/resty/v2" + "github.com/manifoldco/promptui" "github.com/rs/zerolog/log" ) +type PAMAccessParams struct { + ResourceName string + AccountName string +} + +// GetDisplayName returns a user-friendly display name for the access params +func (p PAMAccessParams) GetDisplayName() string { + return fmt.Sprintf("%s/%s", p.ResourceName, p.AccountName) +} + +// ToAPIRequest converts PAMAccessParams to an api.PAMAccessRequest +func (p PAMAccessParams) ToAPIRequest(projectID, duration string) api.PAMAccessRequest { + return api.PAMAccessRequest{ + Duration: duration, + ResourceName: p.ResourceName, + AccountName: p.AccountName, + ProjectId: projectID, + } +} + +// ToApprovalRequestData converts PAMAccessParams to api.PAMAccessApprovalRequestPayloadRequestData +func (p PAMAccessParams) ToApprovalRequestData(duration string) api.PAMAccessApprovalRequestPayloadRequestData { + return api.PAMAccessApprovalRequestPayloadRequestData{ + ResourceName: p.ResourceName, + AccountName: p.AccountName, + AccessDuration: duration, + } +} + // BaseProxyServer contains common functionality for all local proxy types type BaseProxyServer struct { httpClient *resty.Client @@ -248,7 +279,7 @@ func (b *BaseProxyServer) WaitForConnectionsWithTimeout(timeout time.Duration) { } // CallPAMAccessWithMFA attempts to access a PAM account and handles MFA if required -// This is a shared function used by both database and SSH proxies +// This is a shared function used by all PAM proxies func CallPAMAccessWithMFA(httpClient *resty.Client, pamRequest api.PAMAccessRequest) (api.PAMAccessResponse, error) { // Initial request pamResponse, err := api.CallPAMAccess(httpClient, pamRequest) @@ -287,3 +318,69 @@ func CallPAMAccessWithMFA(httpClient *resty.Client, pamRequest api.PAMAccessRequ return pamResponse, nil } + +// HandleApprovalWorkflow checks if an error is due to an approval policy and handles the approval request flow. +// Returns true if the error was handled (either approval request created or user declined), false otherwise. +func HandleApprovalWorkflow(httpClient *resty.Client, err error, projectID string, accessParams PAMAccessParams, durationStr string) bool { + var apiErr *api.APIError + if !errors.As(err, &apiErr) || apiErr.ErrorMessage != "A policy is in place for this resource" { + return false + } + + details, ok := apiErr.Details.(map[string]any) + if !ok { + return false + } + + log.Info().Msgf("Account is protected by approval policy: %s", details["policyName"]) + + shouldSendRequest, promptErr := askForApprovalRequestTrigger() + if promptErr != nil { + if errors.Is(promptErr, promptui.ErrAbort) { + log.Info().Msgf("Approval request was not created.") + } else { + util.HandleError(promptErr, "Failed to send PAM account request") + } + return true + } + + if !shouldSendRequest { + log.Info().Msgf("Approval request was not created.") + return true + } + + approvalReq, reqErr := api.CallPAMAccessApprovalRequest(httpClient, api.PAMAccessApprovalRequest{ + ProjectId: projectID, + RequestData: accessParams.ToApprovalRequestData(durationStr), + }) + if reqErr != nil { + util.HandleError(reqErr, "Failed to send PAM account request") + return true + } + + url := fmt.Sprintf("%s/organizations/%s/projects/pam/%s/approval-requests/%s", + strings.TrimSuffix(config.INFISICAL_URL, "/api"), + approvalReq.Request.OrgId, + approvalReq.Request.ProjectId, + approvalReq.Request.ID) + + if browserErr := util.OpenBrowser(url); browserErr != nil { + log.Error().Msgf("Failed to do browser redirect: %v", browserErr) + } + + log.Info().Msgf("Approval request created.") + log.Info().Msgf("View details at: %s", url) + return true +} + +func askForApprovalRequestTrigger() (bool, error) { + prompt := promptui.Prompt{ + Label: "This action requires approval. You may create an approval request now. Continue?", + IsConfirm: true, + } + result, err := prompt.Run() + if err != nil { + return false, err + } + return strings.ToLower(result) == "y", nil +} diff --git a/packages/pam/local/database-proxy.go b/packages/pam/local/database-proxy.go index 02ac9eb1..604311a9 100644 --- a/packages/pam/local/database-proxy.go +++ b/packages/pam/local/database-proxy.go @@ -2,22 +2,17 @@ package pam import ( "context" - "errors" "fmt" "io" "net" "os" "os/signal" - "strings" "syscall" "time" - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/pam/session" "github.com/Infisical/infisical-merge/packages/util" "github.com/go-resty/resty/v2" - "github.com/manifoldco/promptui" "github.com/rs/zerolog/log" ) @@ -35,76 +30,21 @@ const ( ALPNInfisicalPAMCapabilities ALPN = "infisical-pam-capabilities" ) -func askForApprovalRequestTrigger() (bool, error) { - prompt := promptui.Prompt{ - Label: "This action requires approval. You may create an approval request now. Continue?", - IsConfirm: true, - } - result, err := prompt.Run() - if err != nil { - return false, err - } - return strings.ToLower(result) == "y", nil -} - -func StartDatabaseLocalProxy(accessToken string, accountPath string, projectID string, durationStr string, port int) { - log.Info().Msgf("Starting database proxy for account: %s", accountPath) +func StartDatabaseLocalProxy(accessToken string, accessParams PAMAccessParams, projectID string, durationStr string, port int) { + log.Info().Msgf("Starting database proxy for account: %s", accessParams.GetDisplayName()) log.Info().Msgf("Session duration: %s", durationStr) httpClient := resty.New() httpClient.SetAuthToken(accessToken) httpClient.SetHeader("User-Agent", "infisical-cli") - pamRequest := api.PAMAccessRequest{ - Duration: durationStr, - AccountPath: accountPath, - ProjectId: projectID, - } + pamRequest := accessParams.ToAPIRequest(projectID, durationStr) pamResponse, err := CallPAMAccessWithMFA(httpClient, pamRequest) if err != nil { - var apiErr *api.APIError - if errors.As(err, &apiErr) && apiErr.ErrorMessage == "A policy is in place for this resource" { - if v, ok := apiErr.Details.(map[string]any); ok { - log.Info().Msgf("Account is protected by approval policy: %s", v["policyName"]) - - shouldSendRequest, err := askForApprovalRequestTrigger() - if err != nil { - if errors.Is(err, promptui.ErrAbort) { - log.Info().Msgf("Approval request was not created.") - } else { - util.HandleError(err, "Failed to send PAM account request") - } - return - } - - if !shouldSendRequest { - log.Info().Msgf("Approval request was not created.") - return - } - - approvalReq, err := api.CallPAMAccessApprovalRequest(httpClient, api.PAMAccessApprovalRequest{ - ProjectId: projectID, - RequestData: api.PAMAccessApprovalRequestPayloadRequestData{ - AccountPath: accountPath, - AccessDuration: durationStr, - }, - }) - if err != nil { - util.HandleError(err, "Failed to send PAM account request") - return - } - - url := fmt.Sprintf("%s/organizations/%s/projects/pam/%s/approval-requests/%s", strings.TrimSuffix(config.INFISICAL_URL, "/api"), approvalReq.Request.OrgId, approvalReq.Request.ProjectId, approvalReq.Request.ID) - if err := util.OpenBrowser(url); err != nil { - log.Error().Msgf("Failed to do browser redirect: %v", err) - } - log.Info().Msgf("Approval request created.") - log.Info().Msgf("View details at: %s", url) - return - } + if HandleApprovalWorkflow(httpClient, err, projectID, accessParams, durationStr) { + return } - util.HandleError(err, "Failed to access PAM account") return } @@ -150,9 +90,9 @@ func StartDatabaseLocalProxy(accessToken string, accountPath string, projectID s } if port == 0 { - util.PrintfStderr("Database proxy started for account %s with duration %s on port %d (auto-assigned)\n", accountPath, duration.String(), proxy.port) + fmt.Printf("Database proxy started for account %s with duration %s on port %d (auto-assigned)\n", accessParams.GetDisplayName(), duration.String(), proxy.port) } else { - util.PrintfStderr("Database proxy started for account %s with duration %s on port %d\n", accountPath, duration.String(), proxy.port) + fmt.Printf("Database proxy started for account %s with duration %s on port %d\n", accessParams.GetDisplayName(), duration.String(), proxy.port) } username, ok := pamResponse.Metadata["username"] @@ -165,25 +105,16 @@ func StartDatabaseLocalProxy(accessToken string, accountPath string, projectID s util.HandleError(fmt.Errorf("PAM response metadata is missing 'database'"), "Failed to start proxy server") return } - accountName, ok := pamResponse.Metadata["accountName"] - if !ok { - util.HandleError(fmt.Errorf("PAM response metadata is missing 'accountName'"), "Failed to start proxy server") - return - } - accountPathMetadata, ok := pamResponse.Metadata["accountPath"] - if !ok { - util.HandleError(fmt.Errorf("PAM response metadata is missing 'accountPath'"), "Failed to start proxy server") - return - } log.Info().Msgf("Database proxy server listening on port %d", proxy.port) - util.PrintfStderr("\n") - util.PrintfStderr("**********************************************************************\n") - util.PrintfStderr(" Database Proxy Session Started! \n") - util.PrintfStderr("----------------------------------------------------------------------\n") - util.PrintfStderr("Accessing account %s at folder path %s\n", accountName, accountPathMetadata) - util.PrintfStderr("\n") - util.PrintfStderr("You can now connect to your database using this connection string:\n") + fmt.Printf("\n") + fmt.Printf("**********************************************************************\n") + fmt.Printf(" Database Proxy Session Started! \n") + fmt.Printf("----------------------------------------------------------------------\n") + fmt.Printf("Resource: %s\n", accessParams.ResourceName) + fmt.Printf("Account: %s\n", accessParams.AccountName) + fmt.Printf("\n") + fmt.Printf("You can now connect to your database using this connection string:\n") switch pamResponse.ResourceType { case session.ResourceTypePostgres: diff --git a/packages/pam/local/kubernetes-proxy.go b/packages/pam/local/kubernetes-proxy.go index 5993bc89..4a3e2497 100644 --- a/packages/pam/local/kubernetes-proxy.go +++ b/packages/pam/local/kubernetes-proxy.go @@ -10,7 +10,6 @@ import ( "syscall" "time" - "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/util" "github.com/go-resty/resty/v2" "github.com/rs/zerolog/log" @@ -27,10 +26,10 @@ type KubernetesProxyServer struct { kubeConfigOriginalContext string } -func StartKubernetesLocalProxy(accessToken string, accountPath string, projectId, durationStr string, port int) { +func StartKubernetesLocalProxy(accessToken string, accessParams PAMAccessParams, projectId, durationStr string, port int) { log.Info(). Str("projectId", projectId). - Str("accountPath", accountPath). + Str("account", accessParams.GetDisplayName()). Str("duration", durationStr). Msg("Starting kubernetes proxy") @@ -38,14 +37,13 @@ func StartKubernetesLocalProxy(accessToken string, accountPath string, projectId httpClient.SetAuthToken(accessToken) httpClient.SetHeader("User-Agent", "infisical-cli") - pamRequest := api.PAMAccessRequest{ - Duration: durationStr, - AccountPath: accountPath, - ProjectId: projectId, - } + pamRequest := accessParams.ToAPIRequest(projectId, durationStr) - pamResponse, err := api.CallPAMAccess(httpClient, pamRequest) + pamResponse, err := CallPAMAccessWithMFA(httpClient, pamRequest) if err != nil { + if HandleApprovalWorkflow(httpClient, err, projectId, accessParams, durationStr) { + return + } util.HandleError(err, "Failed to access PAM account") return } @@ -96,20 +94,9 @@ func StartKubernetesLocalProxy(accessToken string, accountPath string, projectId } if port == 0 { - util.PrintfStderr("Kubernetes proxy started for account %s with duration %s on port %d (auto-assigned)\n", accountPath, duration.String(), proxy.port) + fmt.Printf("Kubernetes proxy started for account %s with duration %s on port %d (auto-assigned)\n", accessParams.GetDisplayName(), duration.String(), proxy.port) } else { - util.PrintfStderr("Kubernetes proxy started for account %s with duration %s on port %d\n", accountPath, duration.String(), proxy.port) - } - - accountName, ok := pamResponse.Metadata["accountName"] - if !ok { - util.HandleError(fmt.Errorf("PAM response metadata is missing 'accountName'"), "Failed to start proxy server") - return - } - actualAccountPath, ok := pamResponse.Metadata["accountPath"] - if !ok { - util.HandleError(fmt.Errorf("PAM response metadata is missing 'accountPath'"), "Failed to start proxy server") - return + fmt.Printf("Kubernetes proxy started for account %s with duration %s on port %d\n", accessParams.GetDisplayName(), duration.String(), proxy.port) } // TODO: we should let the user decide whether if they want to update kubeconfig or not @@ -121,7 +108,9 @@ func StartKubernetesLocalProxy(accessToken string, accountPath string, projectId log.Fatal().Err(err).Msg("Failed to load kubernetes config") return } - clusterName := fmt.Sprintf("infisical-k8s-pam/%s", actualAccountPath) + + // Build cluster name for kubeconfig context + clusterName := fmt.Sprintf("infisical-k8s-pam/%s/%s", accessParams.ResourceName, accessParams.AccountName) config.Clusters[clusterName] = &k8sapi.Cluster{ Server: fmt.Sprintf("http://localhost:%d", proxy.port), @@ -143,13 +132,15 @@ func StartKubernetesLocalProxy(accessToken string, accountPath string, projectId proxy.kubeConfigPath = kubeconfig log.Info().Msgf("Kubernetes proxy server listening on port %d", proxy.port) - util.PrintfStderr("\n") - util.PrintfStderr("**********************************************************************\n") - util.PrintfStderr(" Kubernetes Proxy Session Started! \n") - util.PrintfStderr("----------------------------------------------------------------------\n") - util.PrintfStderr("Accessing account %s at folder path %s\n", accountName, accountPath) - util.PrintfStderr("Your current kubectl context has been switched to %s, you can start using kubectl command to access your Kubernetes right now\n", clusterName) - util.PrintfStderr("\n") + fmt.Printf("\n") + fmt.Printf("**********************************************************************\n") + fmt.Printf(" Kubernetes Proxy Session Started! \n") + fmt.Printf("----------------------------------------------------------------------\n") + fmt.Printf("Resource: %s\n", accessParams.ResourceName) + fmt.Printf("Account: %s\n", accessParams.AccountName) + fmt.Printf("\nYour kubectl context has been switched to: %s\n", clusterName) + fmt.Printf("You can now use kubectl commands to access your Kubernetes cluster.\n") + fmt.Printf("\n") // TODO: write kubectl config sigChan := make(chan os.Signal, 1) diff --git a/packages/pam/local/redis-proxy.go b/packages/pam/local/redis-proxy.go index f0dd52ad..3967f39b 100644 --- a/packages/pam/local/redis-proxy.go +++ b/packages/pam/local/redis-proxy.go @@ -2,22 +2,17 @@ package pam import ( "context" - "errors" "fmt" "io" "net" "os" "os/signal" - "strings" "syscall" "time" - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/pam/session" "github.com/Infisical/infisical-merge/packages/util" "github.com/go-resty/resty/v2" - "github.com/manifoldco/promptui" "github.com/rs/zerolog/log" ) @@ -27,64 +22,21 @@ type RedisProxyServer struct { port int } -func StartRedisLocalProxy(accessToken string, accountPath string, projectID string, durationStr string, port int) { - log.Info().Msgf("Starting Redis proxy for account: %s", accountPath) +func StartRedisLocalProxy(accessToken string, accessParams PAMAccessParams, projectID string, durationStr string, port int) { + log.Info().Msgf("Starting Redis proxy for account: %s", accessParams.GetDisplayName()) log.Info().Msgf("Session duration: %s", durationStr) httpClient := resty.New() httpClient.SetAuthToken(accessToken) httpClient.SetHeader("User-Agent", "infisical-cli") - pamRequest := api.PAMAccessRequest{ - Duration: durationStr, - AccountPath: accountPath, - ProjectId: projectID, - } + pamRequest := accessParams.ToAPIRequest(projectID, durationStr) - pamResponse, err := api.CallPAMAccess(httpClient, pamRequest) + pamResponse, err := CallPAMAccessWithMFA(httpClient, pamRequest) if err != nil { - var apiErr *api.APIError - if errors.As(err, &apiErr) && apiErr.ErrorMessage == "A policy is in place for this resource" { - if v, ok := apiErr.Details.(map[string]any); ok { - log.Info().Msgf("Account is protected by approval policy: %s", v["policyName"]) - - shouldSendRequest, err := askForApprovalRequestTrigger() - if err != nil { - if errors.Is(err, promptui.ErrAbort) { - log.Info().Msgf("Approval request was not created.") - } else { - util.HandleError(err, "Failed to send PAM account request") - } - return - } - - if !shouldSendRequest { - log.Info().Msgf("Approval request was not created.") - return - } - - approvalReq, err := api.CallPAMAccessApprovalRequest(httpClient, api.PAMAccessApprovalRequest{ - ProjectId: projectID, - RequestData: api.PAMAccessApprovalRequestPayloadRequestData{ - AccountPath: accountPath, - AccessDuration: durationStr, - }, - }) - if err != nil { - util.HandleError(err, "Failed to send PAM account request") - return - } - - url := fmt.Sprintf("%s/organizations/%s/projects/pam/%s/approval-requests/%s", strings.TrimSuffix(config.INFISICAL_URL, "/api"), approvalReq.Request.OrgId, approvalReq.Request.ProjectId, approvalReq.Request.ID) - if err := util.OpenBrowser(url); err != nil { - log.Error().Msgf("Failed to do browser redirect: %v", err) - } - log.Info().Msgf("Approval request created.") - log.Info().Msgf("View details at: %s", url) - return - } + if HandleApprovalWorkflow(httpClient, err, projectID, accessParams, durationStr) { + return } - util.HandleError(err, "Failed to access PAM account") return } @@ -136,32 +88,22 @@ func StartRedisLocalProxy(accessToken string, accountPath string, projectID stri } if port == 0 { - util.PrintfStderr("Redis proxy started for account %s with duration %s on port %d (auto-assigned)\n", accountPath, duration.String(), proxy.port) + util.PrintfStderr("Redis proxy started for account %s with duration %s on port %d (auto-assigned)\n", accessParams.GetDisplayName(), duration.String(), proxy.port) } else { - util.PrintfStderr("Redis proxy started for account %s with duration %s on port %d\n", accountPath, duration.String(), proxy.port) + util.PrintfStderr("Redis proxy started for account %s with duration %s on port %d\n", accessParams.GetDisplayName(), duration.String(), proxy.port) } username, ok := pamResponse.Metadata["username"] if !ok { username = "" // Redis may not always have username } - accountName, ok := pamResponse.Metadata["accountName"] - if !ok { - util.HandleError(fmt.Errorf("PAM response metadata is missing 'accountName'"), "Failed to start proxy server") - return - } - accountPathMetadata, ok := pamResponse.Metadata["accountPath"] - if !ok { - util.HandleError(fmt.Errorf("PAM response metadata is missing 'accountPath'"), "Failed to start proxy server") - return - } log.Info().Msgf("Redis proxy server listening on port %d", proxy.port) util.PrintfStderr("\n") util.PrintfStderr("**********************************************************************\n") util.PrintfStderr(" Redis Proxy Session Started! \n") util.PrintfStderr("----------------------------------------------------------------------\n") - util.PrintfStderr("Accessing account %s at folder path %s\n", accountName, accountPathMetadata) + util.PrintfStderr("Resource: %s, Account: %s\n", accessParams.ResourceName, accessParams.AccountName) util.PrintfStderr("\n") util.PrintfStderr("You can now connect to your Redis instance using:\n") if username != "" { diff --git a/packages/pam/local/ssh-proxy.go b/packages/pam/local/ssh-proxy.go index 2092c598..57dfdf54 100644 --- a/packages/pam/local/ssh-proxy.go +++ b/packages/pam/local/ssh-proxy.go @@ -13,7 +13,6 @@ import ( "syscall" "time" - "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/pam/session" "github.com/Infisical/infisical-merge/packages/util" "github.com/go-resty/resty/v2" @@ -27,19 +26,18 @@ type SSHProxyServer struct { sshProcess *exec.Cmd } -func StartSSHLocalProxy(accessToken string, accountPath string, projectID string, durationStr string) { +func StartSSHLocalProxy(accessToken string, accessParams PAMAccessParams, projectID string, durationStr string) { httpClient := resty.New() httpClient.SetAuthToken(accessToken) httpClient.SetHeader("User-Agent", "infisical-cli") - pamRequest := api.PAMAccessRequest{ - Duration: durationStr, - AccountPath: accountPath, - ProjectId: projectID, - } + pamRequest := accessParams.ToAPIRequest(projectID, durationStr) pamResponse, err := CallPAMAccessWithMFA(httpClient, pamRequest) if err != nil { + if HandleApprovalWorkflow(httpClient, err, projectID, accessParams, durationStr) { + return + } util.HandleError(err, "Failed to access PAM account") return }