Skip to content
This repository has been archived by the owner on Jul 31, 2024. It is now read-only.

Add open browser option (-w, --web) #258

Merged
merged 1 commit into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ options:
Configuration Mode
-p, --profile string
AWS profile name (default: "default")
-w, --web
Open the AWS Console URL in your default browser (*1)
```

Please be careful that assam overrides default profile in `.aws/credentials` by default.
Expand All @@ -40,6 +42,14 @@ $ brew install cybozu/assam/assam

Download a binary file from [Release](https://github.com/cybozu/assam/releases) and save it to the desired location.

## Notes

### (*1) Command to open the default browser

- Windows: `start`
- macOS : `open`
- Linux: `xdg-open`

## Contribution

1. Fork ([https://github.com/cybozu/assam](https://github.com/cybozu/assam))
Expand Down
142 changes: 142 additions & 0 deletions aws/console.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package aws

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
)

// AWSClient is an interface for AWS operations
type awsClientInterface interface {
GetConsoleURL() (string, error)
}

// awsClient is the implementation of AWSClient interface
type awsClient struct {
session *session.Session
}

// NewAWSClient creates a new AWSClient instance
//
// By default NewSession will only load credentials from the shared credentials file (~/.aws/credentials).
func NewAWSClient() awsClientInterface {
// Create session
sess := session.Must(session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
}))

return &awsClient{
session: sess,
}
}

// GetConsoleURL returns the AWS Management Console URL
// ref: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html
func (c *awsClient) GetConsoleURL() (string, error) {
amazonDomain := c.getConsoleDomain(*c.session.Config.Region)

// Create get signin token URL
creds, err := c.session.Config.Credentials.Get()
if err != nil {
return "", errors.New("failed to get aws credential: please authenticate with `assam`")
}

token, err := c.getSigninToken(creds, amazonDomain)
if err != nil {
return "", err
}

targetURL := fmt.Sprintf("https://console.%s/console/home", amazonDomain)
params := url.Values{
"Action": []string{"login"},
"Destination": []string{targetURL},
"SigninToken": []string{token},
}

return fmt.Sprintf("https://signin.%s/federation?%s", amazonDomain, params.Encode()), nil
}

// getConsoleDomain returns the console domain based on the region
func (c *awsClient) getConsoleDomain(region string) string {
var amazonDomain string

if strings.HasPrefix(region, "us-gov-") {
amazonDomain = "amazonaws-us-gov.com"
} else if strings.HasPrefix(region, "cn-") {
amazonDomain = "amazonaws.cn"
} else {
amazonDomain = "aws.amazon.com"
}
return amazonDomain
}

// getSinginToken retrieves the signin token
func (c *awsClient) getSigninToken(creds credentials.Value, amazonDomain string) (string, error) {
urlCreds := map[string]string{
"sessionId": creds.AccessKeyID,
"sessionKey": creds.SecretAccessKey,
"sessionToken": creds.SessionToken,
}

bytes, err := json.Marshal(urlCreds)
if err != nil {
return "", err
}

params := url.Values{
"Action": []string{"getSigninToken"},
"DurationSeconds": []string{"900"}, // DurationSeconds minimum value
"Session": []string{string(bytes)},
}
tokenRequest := fmt.Sprintf("https://signin.%s/federation?%s", amazonDomain, params.Encode())

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Construct a request to the federation URL.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenRequest, nil)
if err != nil {
return "", err
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("request failed: %s", resp.Status)
}

// Extract a signin token from the response body.
token, err := c.getToken(resp.Body)
if err != nil {
return "", err
}

return token, nil
}

// getToken extracts the signin token from the response body
func (c *awsClient) getToken(reader io.Reader) (string, error) {
type response struct {
SigninToken string
}

var resp response
if err := json.NewDecoder(reader).Decode(&resp); err != nil {
return "", err
}

return resp.SigninToken, nil
}
42 changes: 42 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ package cmd
import (
"context"
"fmt"
"runtime"
"strings"

"github.com/cybozu/assam/aws"
"github.com/cybozu/assam/config"
"github.com/cybozu/assam/defaults"
"github.com/cybozu/assam/idp"
"github.com/cybozu/assam/prompt"
"github.com/pkg/errors"
"github.com/spf13/cobra"

"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
Expand All @@ -37,6 +42,7 @@ func newRootCmd() *cobra.Command {
var configure bool
var roleName string
var profile string
var web bool
var showVersion bool

cmd := &cobra.Command{
Expand All @@ -58,6 +64,10 @@ func newRootCmd() *cobra.Command {
return nil
}

if web {
return openBrowser()
}

cfg, err := config.NewConfig(profile)
if err != nil {
return errors.Wrap(err, "please run `assam --configure` at the first time")
Expand Down Expand Up @@ -105,6 +115,7 @@ func newRootCmd() *cobra.Command {
cmd.PersistentFlags().BoolVarP(&configure, "configure", "c", false, "configure initial settings")
cmd.PersistentFlags().StringVarP(&profile, "profile", "p", "default", "AWS profile")
cmd.PersistentFlags().StringVarP(&roleName, "role", "r", "", "AWS IAM role name")
cmd.PersistentFlags().BoolVarP(&web, "web", "w", false, "open AWS management console in a browser")
cmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "Show version")

return cmd
Expand Down Expand Up @@ -175,6 +186,37 @@ func configureSettings(profile string) error {
return config.Save(cfg, profile)
}

func openBrowser() error {
url, err := aws.NewAWSClient().GetConsoleURL()
if err != nil {
return err
}

var cmd string
var args []string
switch runtime.GOOS {
case "darwin":
cmd = "open"
args = []string{url}
case "windows":
cmd = "cmd"
args = []string{"/c", "start", strings.ReplaceAll(url, "&", "^&")} // for Windows: "&! <>^|" etc. must be escaped, but since only "&" is used, the corresponding
case "linux":
cmd = "xdg-open"
args = []string{url}
}

if len(cmd) != 0 {
err = exec.Command(cmd, args...).Run()
if err != nil {
return err
}
} else {
return errors.New("OS does not support -web command")
}
return nil
}

func handleSignal(cancel context.CancelFunc) {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
Expand Down