Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sso auto run #847

Closed
wants to merge 3 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
74 changes: 71 additions & 3 deletions aws/service.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package aws

import (
"bytes"
"context"
"errors"
"fmt"
"math"
"math/rand"
"os/exec"
"path"
"strings"
"time"
Expand Down Expand Up @@ -1718,7 +1720,7 @@ func getSessionWithMaxRetries(ctx context.Context, d *plugin.QueryData, region s
}

// If seesion was not in cache - create a session and save to cache

var isSSO bool = false
// get aws config info
awsConfig := GetConfig(d.Connection)

Expand All @@ -1737,6 +1739,10 @@ func getSessionWithMaxRetries(ctx context.Context, d *plugin.QueryData, region s

if awsConfig.Profile != nil {
sessionOptions.Profile = *awsConfig.Profile
// When profile is set but not access or secret key, logically this means it will be SSO login
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dbmurphy Is this always true? If I have a connection using my default profile (or any other named profile):

connection "aws" {
    plugin = "aws"
    regions = ["us-east-1"]
}

Then have my ~/.aws/credentials file setup so my default profile uses an AWS access key pair:

[default]
aws_access_key_id = AKIA...
aws_secret_access_key = cD+J...

The awsConfig.AccessKey and awsConfig.SecretKey values will still be nil, as they're not explicitly defined in the config file. Similarly, there are a few other ways you can authenticate with the AWS plugin that do not require an access or secret key explicitly set in the Steampipe AWS config file, like using AssumeRole creds, AWS Vault creds, and EC2 instance profile creds. There are a few examples of each sample connection configuration from https://hub.steampipe.io/plugins/turbot/aws.

Have you tried this code while using any of the above authentication methods?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dbmurphy Is this always true? If I have a connection using my default profile (or any other named profile):

connection "aws" {
    plugin = "aws"
    regions = ["us-east-1"]
}

Then have my ~/.aws/credentials file setup so my default profile uses an AWS access key pair:

[default]
aws_access_key_id = AKIA...
aws_secret_access_key = cD+J...

The awsConfig.AccessKey and awsConfig.SecretKey values will still be nil, as they're not explicitly defined in the config file. Similarly, there are a few other ways you can authenticate with the AWS plugin that do not require an access or secret key explicitly set in the Steampipe AWS config file, like using AssumeRole creds, AWS Vault creds, and EC2 instance profile creds. There are a few examples of each sample connection configuration from https://hub.steampipe.io/plugins/turbot/aws.

Have you tried this code while using any of the above authentication methods?

I have tried some but not all, this is rather complicated, and why I made mention that the auth system really needs some TLC, so we can do things like

if isSSO
if isKey
...

However this means you would need to refactor the Env, SPC, and .aws layer to get a materialized view of what the SDK itself would see. In the current form of this code, it's possible if you had a key type auth and it tried this is would call sts and sso butt hey would fail as no SSO URL is defined in the .aws/config. In this case, it would simply continue and the SDK would still use the key/secret you have provided.

As such, I feel this is a solution that works but could be made cleaner, but such clean is a major refactor of the auth code. Similar to how it should only call SSO login/sts checks once but as steampipe does not have a hierarchical order and all copies of the AWS plugin as started at the same time it calls N open browser or log prints for authorizing in your SSO system.

if awsConfig.AccessKey == nil || awsConfig.SecretKey == nil {
isSSO = true
}
}

if awsConfig.AccessKey != nil && awsConfig.SecretKey == nil {
Expand All @@ -1754,7 +1760,14 @@ func getSessionWithMaxRetries(ctx context.Context, d *plugin.QueryData, region s
)
}
}

// If we are using SSO, check if credintals are valid, if not trigger aws sso login
// *awsConfig.Profile is a risk of being nil, however isSSO will be false in that case, so we are protected
if isSSO == true {
validAwsCredActive := checkAWSCallerIdent(ctx, *awsConfig.Profile)
if !validAwsCredActive && awsConfig.SessionToken == nil && (awsConfig.AccessKey == nil || awsConfig.SecretKey == nil) {
runAWSCLISSOLogin(ctx, *awsConfig.Profile)
}
}
sess, err := session.NewSessionWithOptions(sessionOptions)
if err != nil {
plugin.Logger(ctx).Error("getSessionWithMaxRetries", "new_session_with_options", err)
Expand All @@ -1767,6 +1780,61 @@ func getSessionWithMaxRetries(ctx context.Context, d *plugin.QueryData, region s
return sess, nil
}

// checkAWSCallerIdent returns boolean if currently logged in to AWS CLI
func checkAWSCallerIdent(ctx context.Context, profile string) bool {
plugin.Logger(ctx).Trace("getSessionWithMaxRetries", "checkAWSCallerIdent", "Starting for "+profile)
commandInput := strings.Fields("aws sts get-caller-identity --profile " + profile)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user doesn't have the proper STS permissions to run this particular command, do you know if aws sso login would still work as expected? I'm concerned that if a user has set themselves up to use aws sso login but not any STS commands, this command would fail.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't have St's permission the command will false return false and the code would assume you logger out as it can't verify it. Given STS is the AWS best practice way to check if your logged in. I think you trying to code for a bad practice that is going against aws standards when you ask if STS permissions are missing.

plugin.Logger(ctx).Trace("getSessionWithMaxRetries", "checkAWSCallerIdent", "CommandInput was for "+strings.Join(commandInput, " "))
cmd := exec.Command(commandInput[0], commandInput[1:]...)
var buf bytes.Buffer
cmd.Stdout = &buf

if err := cmd.Run(); err != nil {
plugin.Logger(ctx).Trace("getSessionWithMaxRetries", "checkAWSCallerIdent", err)
return false
}
plugin.Logger(ctx).Trace("getSessionWithMaxRetries", "checkAWSCallerIdent", buf.String())
return true
}

// runAWSCLISSOLogin returns boolean if currently logged in to AWS CLI
func runAWSCLISSOLogin(ctx context.Context, profile string) bool {
commandInput := strings.Fields("aws sso login --profile " + profile)
cmd := exec.Command(commandInput[0], commandInput[1:]...)
var buf bytes.Buffer
cmd.Stdout = &buf
if err := cmd.Start(); err != nil {
plugin.Logger(ctx).Error("getSessionWithMaxRetries", "runAWSCLISSOLogin", err)
return false
}
done := make(chan error)
go func() { done <- cmd.Wait() }()
// Start a timer
timeout := time.After(30 * time.Second)
// The select statement allows us to execute based on which channel
// we get a message from first.
select {
case <-timeout:
// Timeout happened first, kill the process and print a message.
err := cmd.Process.Kill()
plugin.Logger(ctx).Error("getSessionWithMaxRetries", "runAWSCLISSOLogin", "Killed due to timeout!")
if err != nil {
return false
}
return false
case err := <-done:
// Command completed before timeout. Print output and error if it exists.
plugin.Logger(ctx).Trace("getSessionWithMaxRetries", "runAWSCLISSOLogin", buf.String())

if err != nil {
plugin.Logger(ctx).Error("getSessionWithMaxRetries", "runAWSCLISSOLogin", err)
return false
}
return true
}

}

// GetDefaultAwsRegion returns the default region for AWS partiton
// if not set by Env variable or in aws profile
func GetDefaultAwsRegion(d *plugin.QueryData) string {
Expand Down Expand Up @@ -1836,7 +1904,7 @@ func GetDefaultAwsRegion(d *plugin.QueryData) string {

// Function from https://github.com/panther-labs/panther/blob/v1.16.0/pkg/awsretry/connection_retryer.go
func NewConnectionErrRetryer(maxRetries int, ctx context.Context) *ConnectionErrRetryer {
var minRetryDelay time.Duration = 25 * time.Millisecond
var minRetryDelay = 25 * time.Millisecond
rand.Seed(time.Now().UnixNano()) // reseting state of rand to generate different random values
return &ConnectionErrRetryer{
ctx: ctx,
Expand Down