Skip to content

Commit

Permalink
Merge pull request #15 from duplocloud/release/0.5.2
Browse files Browse the repository at this point in the history
Release v0.5.2
  • Loading branch information
joek-duplo authored Mar 3, 2023
2 parents 0c59c75 + cf919e4 commit aab1e23
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 51 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION=0.5.1
VERSION=0.5.2

default: all

Expand Down
26 changes: 14 additions & 12 deletions internal/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ func cacheWriteMustMarshal(file string, source interface{}) []byte {
return json
}

func cacheRemoveEntry(cacheKey, cacheType string) {
cacheRemoveFile(cacheKey, fmt.Sprintf("%s,%s-creds.json", cacheKey, cacheType))
}

func cacheRemoveFile(cacheKey, file string) {
err := os.Remove(filepath.Join(cacheDir, file))
if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("warning: %s: unable to remove from credentials cache", cacheKey)
}
}

// CacheGetAwsConfigOutput tries to read prior AWS creds from the cache.
func CacheGetAwsConfigOutput(cacheKey string) (creds *AwsConfigOutput) {
var file string
Expand Down Expand Up @@ -103,10 +114,7 @@ func CacheGetAwsConfigOutput(cacheKey string) (creds *AwsConfigOutput) {

// Clear the cache if the creds expired.
if creds == nil {
err := os.Remove(filepath.Join(cacheDir, file))
if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("warning: %s: unable to remove from credentials cache", cacheKey)
}
cacheRemoveFile(cacheKey, file)
}
}

Expand Down Expand Up @@ -146,10 +154,7 @@ func CacheGetDuploOutput(cacheKey string, host string) (creds *DuploCredsOutput)

// Clear the cache if the creds expired.
if creds == nil {
err := os.Remove(filepath.Join(cacheDir, file))
if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("warning: %s: unable to remove from credentials cache", cacheKey)
}
cacheRemoveFile(cacheKey, file)
}
}

Expand Down Expand Up @@ -181,10 +186,7 @@ func CacheGetK8sConfigOutput(cacheKey string) (creds *clientauthv1beta1.ExecCred

// Clear the cache if the creds expired.
if creds == nil {
err := os.Remove(filepath.Join(cacheDir, file))
if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("warning: %s: unable to remove from credentials cache", cacheKey)
}
cacheRemoveFile(cacheKey, file)
}
}

Expand Down
117 changes: 79 additions & 38 deletions internal/duplo.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,61 +16,102 @@ type DuploCredsOutput struct {
NeedOTP bool `json:"NeedOTP"`
}

func MustDuploClient(host, token string, interactive, admin bool) (client *duplocloud.Client, creds *DuploCredsOutput) {
var err error
otp := ""

// Possibly get a token from an interactive process.
if token == "" {
if !interactive {
log.Fatalf("%s: --token not specified and --interactive mode is disabled", os.Args[0])
}
// duploClientAndOtpFlag returns a duplo client if and only if the token is valid and OTP is not needed.
func duploClientAndOtpFlag(host, token, otp string, admin bool) (*duplocloud.Client, bool) {
client, err := duplocloud.NewClientWithOtp(host, token, otp)
DieIf(err, "invalid arguments")
features, err := client.FeaturesSystem()

// Is the token invalid?
if err != nil {
return nil, false
}

// Try to find credentials from the cache.
cacheKey := strings.TrimPrefix(host, "https://")
creds = CacheGetDuploOutput(cacheKey, host)
// Do we need to retrieve a OTP?
if admin && features.IsOtpNeeded && otp == "" {
return nil, true
}

// If we have valid credentials, and we do not need OTP, return the new client.
if creds != nil && (!admin || !creds.NeedOTP) {
client, err = duplocloud.NewClient(host, creds.DuploToken)
DieIf(err, "invalid arguments")
// Otherwise, the client is usable.
return client, false
}

// Otherwise, get the token the interactive way.
} else {
tokenResult := MustTokenInteractive(host, admin, "duplo-jit")
token = tokenResult.Token
otp = tokenResult.OTP
// MustDuploClient retrieves a duplo client (and credentials) or panics.
func MustDuploClient(host, token string, interactive, admin bool) (client *duplocloud.Client, creds *DuploCredsOutput) {
needsOtp := false
cacheKey := strings.TrimPrefix(host, "https://")

// Try non-interactive auth first.
if token != "" {
cacheRemoveEntry(cacheKey, "duplo") // never cache explicitly passed creds
client, needsOtp = duploClientAndOtpFlag(host, token, "", admin)

// If OTP is needed, we can only continue if interactive auth is allowed.
if needsOtp {
if !interactive {
log.Fatalf("%s: server requires MFA but --interactive mode is disabled", os.Args[0])
}

// Write the creds to the cache.
cacheFile := fmt.Sprintf("%s,duplo-creds.json", cacheKey)
// The client is usable, so we can return our result.
} else if client != nil {
creds = &DuploCredsOutput{
Version: 1,
DuploToken: token,
NeedOTP: otp != "",
NeedOTP: needsOtp,
}
cacheWriteMustMarshal(cacheFile, creds)
return

// The client is not usable, so we have an error.
} else {
log.Fatalf("%s: authentication failure: failed to collect system features", os.Args[0])
}
}

// Create the client.
if client == nil {
client, err = duplocloud.NewClientWithOtp(host, token, otp)
DieIf(err, "invalid arguments")
// Non-interactive auth was not available or not sufficient.
if !interactive {
log.Fatalf("%s: --token not specified and --interactive mode is disabled", os.Args[0])
}

// Ensure we have a representation off credentials
if creds == nil {
// Next, we load and validate Duplo credentials from the cache.
if token == "" {
creds = CacheGetDuploOutput(cacheKey, host)
if creds != nil {
client, _ = duploClientAndOtpFlag(host, creds.DuploToken, "", admin)
}
}

// Cached credentials were not available or not sufficient.
// So, finally, we try to retrieve and validate Duplo credentials interactively.
if client == nil {

// Clear invalid cached credentials.
if token == "" {
cacheRemoveEntry(cacheKey, "duplo")
}

// Get the token, or fail.
tokenResult := MustTokenInteractive(host, admin, "duplo-jit")
if tokenResult.Token == "" {
log.Fatalf("%s: authentication failure: failed to get token interactively", os.Args[0])
}

// Get the client, or fail.
client, _ = duploClientAndOtpFlag(host, tokenResult.Token, tokenResult.OTP, admin)
if client == nil {
log.Fatalf("%s: authentication failure: failed to collect system features", os.Args[0])
}

// Build credentials.
creds = &DuploCredsOutput{
Version: 1,
DuploToken: token,
NeedOTP: client.OTP != "",
DuploToken: tokenResult.Token,
NeedOTP: tokenResult.OTP != "",
}

// Ensure we are aware of any OTP requirements
if admin && !creds.NeedOTP {
features, err := client.FeaturesSystem()
DieIf(err, "failed to collect system features from Duplo")
creds.NeedOTP = features.IsOtpNeeded
// Write the creds to the cache, unless we started out with a non-interactive token
if token == "" {
cacheFile := fmt.Sprintf("%s,duplo-creds.json", cacheKey)
cacheWriteMustMarshal(cacheFile, creds)
}
}

Expand Down

0 comments on commit aab1e23

Please sign in to comment.