Skip to content

Commit fef1ef6

Browse files
committed
Add UrlAction: printurl and exec
Add additional url actions to print only the url or execute an arbitrary command to make it easier to itegrate with other tools/browsers Fixes: #303
1 parent 9c64bb3 commit fef1ef6

11 files changed

+315
-73
lines changed

cmd/console_cmd.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,8 @@ func openConsoleAccessKey(ctx *RunContext, creds *storage.RoleCredentials, durat
291291
}
292292
url := login.GetUrl()
293293

294-
return utils.HandleUrl(ctx.Settings.UrlAction, ctx.Settings.Browser, url,
294+
urlOpener := utils.NewHandleUrl(ctx.Settings.UrlAction, ctx.Settings.Browser, ctx.Settings.UrlExecCommand)
295+
return urlOpener.Open(url,
295296
"Please open the following URL in your browser:\n\n", "\n\n")
296297
}
297298

cmd/main.go

+2-13
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ var DEFAULT_CONFIG map[string]interface{} = map[string]interface{}{
7979
"ListFields": []string{"AccountId", "AccountAlias", "RoleName", "ExpiresStr"},
8080
"ConsoleDuration": 60,
8181
"UrlAction": "open",
82+
"UrlExecCommand": "",
8283
"LogLevel": "warn",
8384
"DefaultSSO": "Default",
8485
}
@@ -89,7 +90,7 @@ type CLI struct {
8990
ConfigFile string `kong:"name='config',default='${CONFIG_FILE}',help='Config file',env='AWS_SSO_CONFIG'"`
9091
Lines bool `kong:"help='Print line number in logs'"`
9192
LogLevel string `kong:"short='L',name='level',help='Logging level [error|warn|info|debug|trace] (default: warn)'"`
92-
UrlAction string `kong:"short='u',help='How to handle URLs [open|print|clip] (default: open)'"`
93+
UrlAction string `kong:"short='u',help='How to handle URLs [clip|exec|open|print|printurl] (default: open)'"`
9394
SSO string `kong:"short='S',help='Override default AWS SSO Instance',env='AWS_SSO',predictor='sso'"`
9495
STSRefresh bool `kong:"help='Force refresh of STS Token Credentials'"`
9596

@@ -115,10 +116,6 @@ func main() {
115116
ctx, override := parseArgs(&cli)
116117
var err error
117118

118-
if err := urlActionValidate(cli.UrlAction); err != nil {
119-
log.Fatalf("%s", err.Error())
120-
}
121-
122119
if err := logLevelValidate(cli.LogLevel); err != nil {
123120
log.Fatalf("%s", err.Error())
124121
}
@@ -325,11 +322,3 @@ func logLevelValidate(level string) error {
325322
}
326323
return fmt.Errorf("Invalid value for --level: %s", level)
327324
}
328-
329-
func urlActionValidate(action string) error {
330-
switch action {
331-
case "open", "print", "clip", "":
332-
return nil
333-
}
334-
return fmt.Errorf("Invalid value for --url-action: %s", action)
335-
}

cmd/setup_cmd.go

+3-9
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/manifoldco/promptui"
2828
log "github.com/sirupsen/logrus"
2929
"github.com/synfinatic/aws-sso-cli/sso"
30+
"github.com/synfinatic/aws-sso-cli/utils"
3031
)
3132

3233
// https://docs.aws.amazon.com/general/latest/gr/sso.html
@@ -78,9 +79,8 @@ func setupWizard(ctx *RunContext) error {
7879
var hLimit, hMinutes int64
7980

8081
if ctx.Cli.Setup.UrlAction != "" {
81-
if err := urlActionValidate(ctx.Cli.Setup.UrlAction); err != nil {
82-
log.Fatalf("Invalid value for --default-url-action %s", ctx.Cli.Setup.UrlAction)
83-
}
82+
utils.NewHandleUrl(ctx.Cli.Setup.UrlAction, "", "")
83+
urlAction = ctx.Cli.Setup.UrlAction
8484
}
8585

8686
if ctx.Cli.Setup.DefaultLevel != "" {
@@ -170,12 +170,6 @@ func setupWizard(ctx *RunContext) error {
170170
}
171171

172172
// UrlAction
173-
if len(ctx.Cli.Setup.UrlAction) > 0 {
174-
if err := urlActionValidate(ctx.Cli.Setup.UrlAction); err == nil {
175-
urlAction = ctx.Cli.Setup.UrlAction
176-
}
177-
}
178-
179173
if len(urlAction) == 0 {
180174
// How should we deal with URLs?
181175
label = "Default action to take with URLs (UrlAction)"

docs/config.md

+30-7
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ DefaultRegion: <AWS_DEFAULT_REGION>
3333
DefaultSSO: <name of AWS SSO>
3434

3535
Browser: <path to web browser>
36-
UrlAction: [print|open|clip]
36+
UrlAction: [clip|exec|print|printurl|open]
37+
UrlActionExec:
38+
- <command>
39+
- <arg 1>
40+
- <arg N>
41+
- "%s"
3742
ConsoleDuration: <minutes>
3843

3944
LogLevel: [error|warn|info|debug|trace]
@@ -160,16 +165,34 @@ If you only have a single AWS SSO instance, then it doesn't really matter what y
160165
but if you have two or more, than `Default` is automatically selected unless you manually
161166
specify it here, on the CLI (`--sso`), or via the `AWS_SSO` environment variable.
162167

163-
## Browser / UrlAction
168+
## Browser / UrlAction / UrlActionExec
164169

165170
`UrlAction` gives you control over how AWS SSO and AWS Console URLs are opened in a browser:
166171

167-
* `print` -- Prints the URL in your terminal
168-
* `open` -- Opens the URL in your default browser or the browser you specified via `--browser` or `Browser`
169172
* `clip` -- Copies the URL to your clipboard
173+
* `exec` -- Execute the command provided in `UrlActionExec`
174+
* `open` -- Opens the URL in your default browser or the browser you specified via `--browser` or `Browser`
175+
* `print` -- Prints the URL with a message in your terminal to stderr
176+
* `printurl` -- Prints only the URL in your terminal to stderr
177+
178+
If `Browser` is not set, then your default browser will be used and that
179+
your browser needs to support JavaScript for the AWS SSO user interface.
180+
181+
`UrlActionExec` allows you to execute arbitrary commands to handle the URL. The command and arguments
182+
should be specified as a list, with the URL to open specified as the format string `%s`. Only one instance
183+
of `%s` is allowed. Note that YAML requires quotes around strings which start with a [reserved indicator](
184+
https://yaml.org/spec/1.2-old/spec.html#id2774228) like `%`.
170185

171-
If `Browser` is not set, then your default browser will be used. Note that
172-
your browser needs to support Javascript for the AWS SSO user interface.
186+
Example:
187+
188+
```yaml
189+
UrlActionExec:
190+
- open
191+
- -a
192+
- /Applications/Brave Browser.app
193+
- --args
194+
- "%s"
195+
```
173196

174197
## LogLevel / LogLines
175198

@@ -352,6 +375,6 @@ using the `eval` or `exec` commands.
352375
**Note:** These environment variables are considered completely owned and
353376
controlled by `aws-sso` so any existing value will be overwritten.
354377

355-
**Note:** This feature is not compatible when using roles using the
378+
**Note:** This feature is not compatible when using roles using the
356379
`$AWS_PROFILE` via the `config` command.
357380

sso/awssso.go

+28-24
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,22 @@ type SsoApi interface {
5252
}
5353

5454
type AWSSSO struct {
55-
sso SsoApi
56-
ssooidc SsoOidcApi
57-
store storage.SecureStorage
58-
ClientName string `json:"ClientName"`
59-
ClientType string `json:"ClientType"`
60-
SsoRegion string `json:"ssoRegion"`
61-
StartUrl string `json:"startUrl"`
62-
ClientData storage.RegisterClientData `json:"RegisterClient"`
63-
DeviceAuth storage.StartDeviceAuthData `json:"StartDeviceAuth"`
64-
Token storage.CreateTokenResponse `json:"TokenResponse"`
65-
Accounts []AccountInfo `json:"Accounts"`
66-
Roles map[string][]RoleInfo `json:"Roles"`
67-
SSOConfig *SSOConfig `json:"SSOConfig"`
68-
urlAction string // cache for future calls
69-
browser string // cache for future calls
55+
sso SsoApi
56+
ssooidc SsoOidcApi
57+
store storage.SecureStorage
58+
ClientName string `json:"ClientName"`
59+
ClientType string `json:"ClientType"`
60+
SsoRegion string `json:"ssoRegion"`
61+
StartUrl string `json:"startUrl"`
62+
ClientData storage.RegisterClientData `json:"RegisterClient"`
63+
DeviceAuth storage.StartDeviceAuthData `json:"StartDeviceAuth"`
64+
Token storage.CreateTokenResponse `json:"TokenResponse"`
65+
Accounts []AccountInfo `json:"Accounts"`
66+
Roles map[string][]RoleInfo `json:"Roles"`
67+
SSOConfig *SSOConfig `json:"SSOConfig"`
68+
urlAction string // cache for future calls
69+
browser string // cache for future calls
70+
urlExecCommand interface{} // cache for future calls
7071
}
7172

7273
func NewAWSSSO(s *SSOConfig, store *storage.SecureStorage) *AWSSSO {
@@ -79,15 +80,18 @@ func NewAWSSSO(s *SSOConfig, store *storage.SecureStorage) *AWSSSO {
7980
})
8081

8182
as := AWSSSO{
82-
sso: ssoSession,
83-
ssooidc: oidcSession,
84-
store: *store,
85-
ClientName: awsSSOClientName,
86-
ClientType: awsSSOClientType,
87-
SsoRegion: s.SSORegion,
88-
StartUrl: s.StartUrl,
89-
Roles: map[string][]RoleInfo{},
90-
SSOConfig: s,
83+
sso: ssoSession,
84+
ssooidc: oidcSession,
85+
store: *store,
86+
ClientName: awsSSOClientName,
87+
ClientType: awsSSOClientType,
88+
SsoRegion: s.SSORegion,
89+
StartUrl: s.StartUrl,
90+
Roles: map[string][]RoleInfo{},
91+
SSOConfig: s,
92+
urlAction: s.settings.UrlAction,
93+
browser: s.settings.Browser,
94+
urlExecCommand: s.settings.UrlExecCommand,
9195
}
9296
return &as
9397
}

sso/awssso_auth.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ func (as *AWSSSO) reauthenticate() error {
9696
return fmt.Errorf("Unable to get device auth info from AWS SSO: %s", err.Error())
9797
}
9898

99-
err = utils.HandleUrl(as.urlAction, as.browser, auth.VerificationUriComplete,
99+
urlOpener := utils.NewHandleUrl(as.urlAction, as.browser, as.urlExecCommand)
100+
101+
err = urlOpener.Open(auth.VerificationUriComplete,
100102
"Please open the following URL in your browser:\n\n", "\n\n")
101103
if err != nil {
102104
return err

sso/awssso_auth_test.go

+93
Original file line numberDiff line numberDiff line change
@@ -343,3 +343,96 @@ func TestAuthenticateFailure(t *testing.T) {
343343
err = as.Authenticate("print", "fake-browser")
344344
assert.Contains(t, err.Error(), "some error")
345345
}
346+
347+
func TestReauthenticate(t *testing.T) {
348+
tfile, err := ioutil.TempFile("", "*storage.json")
349+
assert.NoError(t, err)
350+
351+
jstore, err := storage.OpenJsonStore(tfile.Name())
352+
assert.NoError(t, err)
353+
354+
defer os.Remove(tfile.Name())
355+
356+
as := &AWSSSO{
357+
SsoRegion: "us-west-1",
358+
StartUrl: "https://testing.awsapps.com/start",
359+
store: jstore,
360+
urlAction: "invalid",
361+
browser: "no-such-browser",
362+
urlExecCommand: []interface{}{"/dev/null"},
363+
}
364+
365+
secs, _ := time.ParseDuration("5s")
366+
expires := time.Now().Add(secs).Unix()
367+
368+
as.ssooidc = &mockSsoOidcApi{
369+
Results: []mockSsoOidcApiResults{
370+
{
371+
RegisterClient: &ssooidc.RegisterClientOutput{
372+
AuthorizationEndpoint: nil,
373+
ClientId: aws.String("this-is-my-client-id"),
374+
ClientSecret: aws.String("this-is-my-client-secret"),
375+
ClientIdIssuedAt: time.Now().Unix(),
376+
ClientSecretExpiresAt: int64(expires),
377+
TokenEndpoint: nil,
378+
},
379+
Error: nil,
380+
},
381+
{
382+
StartDeviceAuthorization: &ssooidc.StartDeviceAuthorizationOutput{
383+
DeviceCode: aws.String("device-code"),
384+
UserCode: aws.String("user-code"),
385+
VerificationUri: aws.String("verification-uri"),
386+
VerificationUriComplete: aws.String("verification-uri-complete"),
387+
ExpiresIn: int32(expires),
388+
Interval: 5,
389+
},
390+
Error: nil,
391+
},
392+
{
393+
CreateToken: &ssooidc.CreateTokenOutput{},
394+
Error: fmt.Errorf("some error"),
395+
},
396+
},
397+
}
398+
399+
// invalid urlAction
400+
assert.Panics(t, func() { as.reauthenticate() })
401+
402+
// valid urlAction, but command is invalid
403+
as.urlAction = "exec"
404+
as.ssooidc = &mockSsoOidcApi{
405+
Results: []mockSsoOidcApiResults{
406+
{
407+
RegisterClient: &ssooidc.RegisterClientOutput{
408+
AuthorizationEndpoint: nil,
409+
ClientId: aws.String("this-is-my-client-id"),
410+
ClientSecret: aws.String("this-is-my-client-secret"),
411+
ClientIdIssuedAt: time.Now().Unix(),
412+
ClientSecretExpiresAt: int64(expires),
413+
TokenEndpoint: nil,
414+
},
415+
Error: nil,
416+
},
417+
{
418+
StartDeviceAuthorization: &ssooidc.StartDeviceAuthorizationOutput{
419+
DeviceCode: aws.String("device-code"),
420+
UserCode: aws.String("user-code"),
421+
VerificationUri: aws.String("verification-uri"),
422+
VerificationUriComplete: aws.String("verification-uri-complete"),
423+
ExpiresIn: int32(expires),
424+
Interval: 5,
425+
},
426+
Error: nil,
427+
},
428+
{
429+
CreateToken: &ssooidc.CreateTokenOutput{},
430+
Error: fmt.Errorf("some error"),
431+
},
432+
},
433+
}
434+
435+
err = as.reauthenticate()
436+
assert.Contains(t, err.Error(), "Unable to exec")
437+
438+
}

sso/awssso_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ func TestNewAWSSSO(t *testing.T) {
8686
c := SSOConfig{
8787
StartUrl: "https://starturl.com/start",
8888
SSORegion: "us-east-1",
89+
settings: &Settings{},
8990
}
9091

9192
s := NewAWSSSO(&c, &jstore)
@@ -418,6 +419,7 @@ func TestGetAccounts(t *testing.T) {
418419
RefreshToken: "refresh-token",
419420
TokenType: "token-type",
420421
},
422+
urlAction: "print",
421423
}
422424

423425
as.sso = &mockSsoApi{

sso/settings.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import (
3434
"github.com/knadh/koanf/providers/file"
3535
log "github.com/sirupsen/logrus"
3636
"github.com/synfinatic/aws-sso-cli/utils"
37-
// goyaml "gopkg.in/yaml.v3"
3837
)
3938

4039
const (
@@ -54,6 +53,7 @@ type Settings struct {
5453
JsonStore string `koanf:"JsonStore" yaml:"JsonStore,omitempty"`
5554
UrlAction string `koanf:"UrlAction" yaml:"UrlAction,omitempty"`
5655
Browser string `koanf:"Browser" yaml:"Browser,omitempty"`
56+
UrlExecCommand interface{} `koanf:"UrlExecCommand" yaml:"UrlExecCommand,omitempty"` // string or list
5757
ProfileFormat string `koanf:"ProfileFormat" yaml:"ProfileFormat,omitempty"`
5858
AccountPrimaryTag []string `koanf:"AccountPrimaryTag" yaml:"AccountPrimaryTag,omitempty"`
5959
PromptColors PromptColors `koanf:"PromptColors" yaml:"PromptColors,omitempty"` // go-prompt colors

0 commit comments

Comments
 (0)