Skip to content

Commit

Permalink
Merge c497f8b into b69be41
Browse files Browse the repository at this point in the history
  • Loading branch information
spbsoluble authored Nov 18, 2024
2 parents b69be41 + c497f8b commit bd76b9b
Show file tree
Hide file tree
Showing 7 changed files with 414 additions and 4 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,19 @@ servers:
client_id: client-id
client_secret: client-secret
api_path: KeyfactorAPI
```
```
## Configuration File Providers
Below are a list of configuration file providers that can be used to load configuration from a file if loading from disk
is not desired.
### Azure Key Vault
To use Azure Key Vault as a configuration file provider, the code must either be running in an Azure environment or the
environment configured with `az login`. The following environment variables will be used when they are not directly set.

| Name | Description | Default |
|---------------------|---------------------------------------|---------|
| AZURE_KEYVAULT_NAME | The name of the Azure KeyVault | |
| AZURE_SECRET_NAME | The name of the Azure KeyVault secret | |
4 changes: 2 additions & 2 deletions auth_providers/auth_basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@ func (a *CommandAuthConfigBasic) Authenticate() error {
return cErr
}

// create oauth Client
authy, err := NewBasicAuthAuthenticatorBuilder().
// create Basic Client
authy, err := a.
WithUsername(a.Username).
WithPassword(a.Password).
WithDomain(a.Domain).
Expand Down
2 changes: 2 additions & 0 deletions auth_providers/auth_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,8 @@ func (c *CommandAuthConfig) GetServerConfig() *Server {
return &server
}

type contextKey string

// Example usage of CommandAuthConfig
//
// This example demonstrates how to use CommandAuthConfig to authenticate to the Keyfactor Command API.
Expand Down
171 changes: 171 additions & 0 deletions auth_providers/azure_keyvault.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package auth_providers

import (
"context"
"encoding/json"
"fmt"
"os"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets"
)

const (
EnvAzureVaultName = "AZURE_KEYVAULT_NAME"
EnvAzureSecretName = "AZURE_SECRET_NAME"
)

// ConfigProviderAzureKeyVault is an Authenticator that uses Azure Key Vault for authentication.
type ConfigProviderAzureKeyVault struct {
SecretName string `json:"secret_name;omitempty" yaml:"secret_name;omitempty"`
VaultName string `json:"vault_name" yaml:"vault_name"`
DefaultCredential *azidentity.DefaultAzureCredential
CommandConfig *Config
//TenantID string `json:"tenant_id;omitempty"`
//SubscriptionID string `json:"subscription_id;omitempty"`
//ResourceGroup string `json:"resource_group;omitempty"`
}

// NewConfigProviderAzureKeyVault creates a new instance of ConfigProviderAzureKeyVault.
func NewConfigProviderAzureKeyVault() *ConfigProviderAzureKeyVault {
return &ConfigProviderAzureKeyVault{}
}

// String returns a string representation of the ConfigProviderAzureKeyVault.
func (a *ConfigProviderAzureKeyVault) String() string {
return fmt.Sprintf("SecretName: %s, AzureVaultName: %s", a.SecretName, a.VaultName)
}

// WithSecretName sets the secret name for authentication.
func (a *ConfigProviderAzureKeyVault) WithSecretName(secretName string) *ConfigProviderAzureKeyVault {
a.SecretName = secretName
return a
}

// WithVaultName sets the vault name for authentication.
func (a *ConfigProviderAzureKeyVault) WithVaultName(vaultName string) *ConfigProviderAzureKeyVault {
a.VaultName = vaultName
return a
}

// Authenticate authenticates to Azure.
func (a *ConfigProviderAzureKeyVault) Authenticate() error {

vErr := a.Validate()
if vErr != nil {
return vErr
}

// Create a context with a timeout
ctx, cancel := context.WithTimeout(context.Background(), DefaultClientTimeout*time.Second)
defer cancel()

// Add custom metadata to context
ctx = context.WithValue(ctx, contextKey("operation"), "AzureAuthenticate")

// Try to authenticate using DefaultAzureCredential
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return fmt.Errorf("failed to obtain a credential: %w", err)
}
a.DefaultCredential = cred
return nil
}

// Validate validates the ConfigProviderAzureKeyVault.
func (a *ConfigProviderAzureKeyVault) Validate() error {
if a.SecretName == "" {
// Check if the secret name is set in the environment
if secretName := os.Getenv(EnvAzureSecretName); secretName != "" {
a.SecretName = secretName
} else {
return fmt.Errorf("Azure KeyVault `SecretName` is required")
}
}
if a.VaultName == "" {
// Check if the vault name is set in the environment
if vaultName := os.Getenv(EnvAzureVaultName); vaultName != "" {
a.VaultName = vaultName
} else {
return fmt.Errorf("Azure KeyVault `VaultName` is required")
}
}
return nil
}

// LoadConfigFromAzureKeyVault loads a Config type from Azure Key Vault.
func (a *ConfigProviderAzureKeyVault) LoadConfigFromAzureKeyVault() (*Config, error) {
if a.DefaultCredential == nil {
aErr := a.Authenticate()
if aErr != nil {
return nil, aErr
}
}

// Create a context with a timeout
ctx, cancel := context.WithTimeout(context.Background(), DefaultClientTimeout*time.Second)
defer cancel()

// Add custom metadata to context
ctx = context.WithValue(ctx, contextKey("operation"), "LoadConfigFromAzureKeyVault")
ctx = context.WithValue(ctx, contextKey("vaultName"), a.VaultName)
ctx = context.WithValue(ctx, contextKey("secretName"), a.SecretName)
// Create a client to access the Azure Key Vault
vaultURL := fmt.Sprintf("https://%s.vault.azure.net/", a.VaultName)
client, cErr := azsecrets.NewClient(vaultURL, a.DefaultCredential, nil)
if cErr != nil {
return nil, cErr
}

// Retrieve the secret from the Azure Key Vault
secretResp, sErr := client.GetSecret(ctx, a.SecretName, "", nil)
if sErr != nil {
return nil, sErr
}

// Check if the secret value is nil to avoid dereferencing a nil pointer
if secretResp.Value == nil {
return nil, fmt.Errorf("secret value for '%s' in vault '%s' is nil", a.SecretName, a.VaultName)
}

// Parse the secret value into a Config type
var config Config
if jErr := json.Unmarshal([]byte(*secretResp.Value), &config); jErr != nil {
//attempt to unmarshal as a single server config
var singleServerConfig Server
if sjErr := json.Unmarshal([]byte(*secretResp.Value), &singleServerConfig); sjErr == nil {
config.Servers = make(map[string]Server)
config.Servers[DefaultConfigProfile] = singleServerConfig
} else {
return nil, jErr
}
}

a.CommandConfig = &config
return &config, nil
}

// Example usage of ConfigProviderAzureKeyVault
//
// This example demonstrates how to use ConfigProviderAzureKeyVault to load a configuration from Azure Key Vault.
//
// func ExampleConfigProviderAzureKeyVault() {
// provider := NewConfigProviderAzureKeyVault().
// WithSecretName("my-secret").
// WithVaultName("my-vault")
//
// err := provider.Authenticate()
// if err != nil {
// fmt.Println("Failed to authenticate:", err)
// return
// }
//
// config, err := provider.LoadConfigFromAzureKeyVault()
// if err != nil {
// fmt.Println("Failed to load config from Azure Key Vault:", err)
// return
// }
//
// fmt.Println("Loaded config from Azure Key Vault:", config)
// }
145 changes: 145 additions & 0 deletions auth_providers/azure_keyvault_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package auth_providers_test

import (
"encoding/json"
"fmt"
"net/http"
"os"
"testing"
"time"

"github.com/Keyfactor/keyfactor-auth-client-go/auth_providers"
"github.com/stretchr/testify/assert"
)

func TestConfigProviderAzureKeyVault_Authenticate(t *testing.T) {
if isRunningInGithubAction() {
t.Skip("Testing via Github Actions not supported, skipping test")
}
provider := auth_providers.NewConfigProviderAzureKeyVault()
err := provider.Authenticate()
assert.NoError(t, err, "expected no error during authentication")
}

func TestConfigProviderAzureKeyVault_LoadConfigFromAzureKeyVault(t *testing.T) {
if isRunningInGithubAction() {
t.Skip("Testing via Github Actions not supported, skipping test")
}
vaultName := os.Getenv(auth_providers.EnvAzureVaultName)
secretName := os.Getenv(auth_providers.EnvAzureSecretName)

t.Logf("vaultName: %s, secretName: %s", vaultName, secretName)

// Test with environment variables set
t.Logf("Testing with only environment variables set")
envConf := auth_providers.NewConfigProviderAzureKeyVault()
envConfig, cErr := envConf.LoadConfigFromAzureKeyVault()
assert.NoError(t, cErr, "expected no error during config load")
assert.NotNil(t, envConfig, "expected config to be loaded")

// Test with mixed environment variables and parameters set
t.Logf("Testing with mixed environment variables and parameters set")
os.Unsetenv(auth_providers.EnvAzureSecretName)
envParamsSecretName := auth_providers.NewConfigProviderAzureKeyVault().
WithSecretName(secretName)
envParamsSecretNameConfig, envParamsSecretNameErr := envParamsSecretName.LoadConfigFromAzureKeyVault()
assert.NoError(t, envParamsSecretNameErr, "expected no error during config load")
assert.NotNil(t, envParamsSecretNameConfig, "expected config to be loaded")
os.Setenv(auth_providers.EnvAzureSecretName, secretName)

// Test with mixed environment variables and parameters set
t.Logf("Testing with mixed environment variables and parameters set")
os.Unsetenv(auth_providers.EnvAzureVaultName)
envParamsVaultName := auth_providers.NewConfigProviderAzureKeyVault().
WithVaultName(vaultName)
envParamsVaultNameConfig, envParamsVaultNameErr := envParamsVaultName.LoadConfigFromAzureKeyVault()
assert.NoError(t, envParamsVaultNameErr, "expected no error during config load")
assert.NotNil(t, envParamsVaultNameConfig, "expected config to be loaded")
os.Setenv(auth_providers.EnvAzureVaultName, vaultName)

// Test with no environment variables set
t.Logf("Testing with no environment variables set")
unsetAkvEnvVars()
fullParams := auth_providers.NewConfigProviderAzureKeyVault().
WithSecretName(secretName).
WithVaultName(vaultName)
fullParamsConfig, fullParamsErr := fullParams.LoadConfigFromAzureKeyVault()
assert.NoError(t, fullParamsErr, "expected no error during config load")
assert.NotNil(t, fullParamsConfig, "expected config to be loaded")
}

func TestConfigProviderAzureKeyVault_Validate(t *testing.T) {
provider := auth_providers.NewConfigProviderAzureKeyVault().
WithSecretName("my-secret").
WithVaultName("my-vault")

err := provider.Validate()
assert.NoError(t, err, "expected no error during validation")
}

func unsetAkvEnvVars() {
os.Unsetenv(auth_providers.EnvAzureSecretName)
os.Unsetenv(auth_providers.EnvAzureVaultName)
}

func isRunningOnAzureByEnvVar() bool {
_, exists := os.LookupEnv("AZURE_REGION")
return exists
}

func isRunningOnAzureByIMDS() bool {
client := &http.Client{Timeout: 2 * time.Second}
req, err := http.NewRequest("GET", "http://169.254.169.254/metadata/instance?api-version=2021-02-01", nil)
if err != nil {
return false
}
req.Header.Set("Metadata", "true")

resp, err := client.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()

return resp.StatusCode == http.StatusOK
}

func isRunningOnAzureByAgent() bool {
_, err := os.Stat("/usr/sbin/waagent")
return err == nil
}

func hasManagedIdentity() (bool, error) {
client := &http.Client{Timeout: 2 * time.Second}
req, err := http.NewRequest("GET", "http://169.254.169.254/metadata/identity?api-version=2021-02-01", nil)
if err != nil {
return false, fmt.Errorf("error creating request: %w", err)
}
req.Header.Set("Metadata", "true")

resp, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("error making request to IMDS: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return false, nil // No managed identity attached
}

var identityMetadata map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&identityMetadata); err != nil {
return false, fmt.Errorf("error decoding identity metadata: %w", err)
}

// Check for the presence of identity-related fields
if _, exists := identityMetadata["compute"]; exists {
return true, nil
}
return false, nil
}

func isRunningInGithubAction() bool {
_, exists := os.LookupEnv("GITHUB_RUN_ID")
return exists
}
21 changes: 21 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,27 @@ module github.com/Keyfactor/keyfactor-auth-client-go
go 1.22

require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0
github.com/stretchr/testify v1.9.0
golang.org/x/oauth2 v0.24.0
gopkg.in/yaml.v2 v2.4.0
)

require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit bd76b9b

Please sign in to comment.