-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
414 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
// } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.