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

Commit

Permalink
Merge pull request #258 from cybozu/add-open-browser-option
Browse files Browse the repository at this point in the history
Add open browser option (-w, --web)
  • Loading branch information
naotama2002 authored Oct 10, 2023
2 parents 8f65038 + 54210b5 commit b3e2fcf
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 0 deletions.
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

0 comments on commit b3e2fcf

Please sign in to comment.