From 058ea6cc58e86528e08e5b1c847ca4357545eeec Mon Sep 17 00:00:00 2001
From: meyerjrr <33018450+meyerjrr@users.noreply.github.com>
Date: Tue, 24 Sep 2024 16:44:16 +1000
Subject: [PATCH] implement custom templated launch for sso browser option
 (#750)

* implement custom templated launch for sso browser option

* fix typo

* update custom browser logic to handle custom cases for google chrome
---
 pkg/assume/entrypoint.go |  1 +
 pkg/config/config.go     |  7 ++++++-
 pkg/granted/console.go   |  2 +-
 pkg/idclogin/run.go      | 44 +++++++++++++++++++++++++++++++++++++++-
 pkg/launcher/custom.go   | 24 +++++++++++++++++++++-
 5 files changed, 74 insertions(+), 4 deletions(-)

diff --git a/pkg/assume/entrypoint.go b/pkg/assume/entrypoint.go
index 32f86660..c85c2377 100644
--- a/pkg/assume/entrypoint.go
+++ b/pkg/assume/entrypoint.go
@@ -116,6 +116,7 @@ func GetCliApp() *cli.App {
 			if err != nil {
 				return err
 			}
+
 			if !hasSetup {
 				browserName, err := browser.HandleBrowserWizard(c)
 				if err != nil {
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 7c4f8ac0..f1933dbd 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -52,6 +52,11 @@ type Config struct {
 	// and CustomBrowserPath fields.
 	AWSConsoleBrowserLaunchTemplate *BrowserLaunchTemplate `toml:",omitempty"`
 
+	// SSOBrowserLaunchTemplate is an optional launch template to use
+	// for opening the SSO auth flow. If specified it overrides the DefaultBrowser
+	// and CustomSSOBrowserPath fields.
+	SSOBrowserLaunchTemplate *BrowserLaunchTemplate `toml:",omitempty"`
+
 	Keyring                *KeyringConfig `toml:",omitempty"`
 	Ordering               string
 	ExportCredentialSuffix string
@@ -290,7 +295,7 @@ func Load() (*Config, error) {
 	_, err = toml.NewDecoder(file).Decode(&c)
 	if err != nil {
 		// if there is an error just reset the file
-		return &c, nil
+		return nil, err
 	}
 	return &c, nil
 }
diff --git a/pkg/granted/console.go b/pkg/granted/console.go
index 60ab88c7..9fb89953 100644
--- a/pkg/granted/console.go
+++ b/pkg/granted/console.go
@@ -128,7 +128,7 @@ var ConsoleCommand = cli.Command{
 		}
 
 		if startErr != nil {
-			return clierr.New(fmt.Sprintf("Granted was unable to open a browser session automatically due to the following error: %s", err.Error()),
+			return clierr.New(fmt.Sprintf("Granted was unable to open a browser session automatically due to the following error: %s", startErr.Error()),
 				// allow them to try open the url manually
 				clierr.Info("You can open the browser session manually using the following url:"),
 				clierr.Info(consoleURL),
diff --git a/pkg/idclogin/run.go b/pkg/idclogin/run.go
index 0e2e92f0..eedf6b8f 100644
--- a/pkg/idclogin/run.go
+++ b/pkg/idclogin/run.go
@@ -2,13 +2,18 @@ package idclogin
 
 import (
 	"context"
+	"errors"
+	"fmt"
 	"os/exec"
 	"time"
 
 	"github.com/aws/aws-sdk-go-v2/aws"
 	"github.com/aws/aws-sdk-go-v2/service/ssooidc"
 	"github.com/common-fate/clio"
+	"github.com/common-fate/clio/clierr"
 	grantedConfig "github.com/common-fate/granted/pkg/config"
+	"github.com/common-fate/granted/pkg/forkprocess"
+	"github.com/common-fate/granted/pkg/launcher"
 	"github.com/common-fate/granted/pkg/securestorage"
 	"github.com/pkg/browser"
 )
@@ -54,7 +59,44 @@ func Login(ctx context.Context, cfg aws.Config, startUrl string, scopes []string
 		return nil, err
 	}
 
-	if config.CustomSSOBrowserPath != "" {
+	if config.SSOBrowserLaunchTemplate != nil {
+		l, err := launcher.CustomFromLaunchTemplate(config.SSOBrowserLaunchTemplate, []string{})
+		if err == launcher.ErrLaunchTemplateNotConfigured {
+			return nil, errors.New("error configuring custom browser, ensure that [SSOBrowserLaunchTemplate] is specified in your Granted config file")
+		}
+		if err != nil {
+			return nil, err
+		}
+
+		// now build the actual command to run - e.g. 'firefox --new-tab <URL>'
+		args, err := l.LaunchCommand(url, "")
+		if err != nil {
+			return nil, fmt.Errorf("error building browser launch command: %w", err)
+		}
+
+		var startErr error
+		if l.UseForkProcess() {
+			clio.Debugf("running command using forkprocess: %s", args)
+			cmd, err := forkprocess.New(args...)
+			if err != nil {
+				return nil, err
+			}
+			startErr = cmd.Start()
+		} else {
+			clio.Debugf("running command without forkprocess: %s", args)
+			cmd := exec.Command(args[0], args[1:]...)
+			startErr = cmd.Start()
+		}
+
+		if startErr != nil {
+			return nil, clierr.New(fmt.Sprintf("Granted was unable to open a browser session automatically due to the following error: %s", startErr.Error()),
+				// allow them to try open the url manually
+				clierr.Info("You can open the browser session manually using the following url:"),
+				clierr.Info(url),
+			)
+		}
+
+	} else if config.CustomSSOBrowserPath != "" {
 		cmd := exec.Command(config.CustomSSOBrowserPath, url)
 		err = cmd.Start()
 		if err != nil {
diff --git a/pkg/launcher/custom.go b/pkg/launcher/custom.go
index 18c30ccb..67c2bbf6 100644
--- a/pkg/launcher/custom.go
+++ b/pkg/launcher/custom.go
@@ -3,6 +3,7 @@ package launcher
 import (
 	"errors"
 	"fmt"
+	"regexp"
 	"strings"
 	"text/template"
 
@@ -52,10 +53,31 @@ func (l Custom) LaunchCommand(url string, profile string) ([]string, error) {
 		return nil, fmt.Errorf("executing command template (check that your browser launch template is valid in your Granted config): %w", err)
 	}
 
-	commandParts := strings.Fields(renderedCommand.String())
+	commandParts := splitCommand(renderedCommand.String())
 	return commandParts, nil
 }
 
+// splits each component of the command. Anything within quotes will be handled as one component of the command
+// eg open -a "Google Chrome" <URL> returns ["open", "-a", "Google Chrome", "<URL>"]
+func splitCommand(command string) []string {
+
+	re := regexp.MustCompile(`"([^"]+)"|(\S+)`)
+	matches := re.FindAllStringSubmatch(command, -1)
+
+	var result []string
+	for _, match := range matches {
+
+		if match[1] != "" {
+			result = append(result, match[1])
+		} else {
+
+			result = append(result, match[2])
+		}
+	}
+
+	return result
+}
+
 func (l Custom) UseForkProcess() bool { return l.ForkProcess }
 
 var ErrLaunchTemplateNotConfigured = errors.New("launch template is not configured")