Allow Azure Auth order to be specified via `azureAuthMethods` component metadata (#3217)
Signed-off-by: Bernd Verst <github@bernd.dev> Signed-off-by: luigirende <luigirende@gmail.com> Signed-off-by: luiren <luigirende@gmail.com> Signed-off-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> Co-authored-by: luigirende <luigirende@gmail.com> Co-authored-by: luiren <luigi.rende@assistdigital.com>
This commit is contained in:
parent
8fe74b15aa
commit
3bcd0c7451
|
|
@ -105,59 +105,47 @@ func (s EnvironmentSettings) GetAzureEnvironment() (*cloud.Configuration, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTokenCredential returns an azcore.TokenCredential retrieved from, in order:
|
func (s EnvironmentSettings) addClientCredentialsProvider(creds *[]azcore.TokenCredential, errs *[]error) {
|
||||||
// 1. Client credentials
|
|
||||||
// 2. Client certificate
|
|
||||||
// 3. Workload identity
|
|
||||||
// 4. MSI (we use a timeout of 1 second when no compatible managed identity implementation is available)
|
|
||||||
// 5. Azure CLI
|
|
||||||
//
|
|
||||||
// This order and timeout (with the exception of the additional step 5) matches the DefaultAzureCredential.
|
|
||||||
func (s EnvironmentSettings) GetTokenCredential() (azcore.TokenCredential, error) {
|
|
||||||
// Create a chain
|
|
||||||
var creds []azcore.TokenCredential
|
|
||||||
errs := make([]error, 0, 3)
|
|
||||||
|
|
||||||
// 1. Client credentials
|
|
||||||
if c, e := s.GetClientCredentials(); e == nil {
|
if c, e := s.GetClientCredentials(); e == nil {
|
||||||
cred, err := c.GetTokenCredential()
|
cred, err := c.GetTokenCredential()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
creds = append(creds, cred)
|
*creds = append(*creds, cred)
|
||||||
} else {
|
} else {
|
||||||
errs = append(errs, err)
|
*errs = append(*errs, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Client certificate
|
func (s EnvironmentSettings) addClientCertificateProvider(creds *[]azcore.TokenCredential, errs *[]error) {
|
||||||
if c, e := s.GetClientCert(); e == nil {
|
if c, e := s.GetClientCert(); e == nil {
|
||||||
cred, err := c.GetTokenCredential()
|
cred, err := c.GetTokenCredential()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
creds = append(creds, cred)
|
*creds = append(*creds, cred)
|
||||||
} else {
|
} else {
|
||||||
errs = append(errs, err)
|
*errs = append(*errs, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Workload identity
|
func (s EnvironmentSettings) addWorkloadIdentityProvider(creds *[]azcore.TokenCredential, errs *[]error) {
|
||||||
// workload identity requires values for AZURE_AUTHORITY_HOST, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, AZURE_TENANT_ID
|
// workload identity requires values for AZURE_AUTHORITY_HOST, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, AZURE_TENANT_ID
|
||||||
// The workload identity mutating admissions webhook in Kubernetes injects these values into the pod.
|
// The workload identity mutating admissions webhook in Kubernetes injects these values into the pod.
|
||||||
// These environment variables are read using the default WorkloadIdentityCredentialOptions
|
// These environment variables are read using the default WorkloadIdentityCredentialOptions
|
||||||
|
|
||||||
workloadCred, err := azidentity.NewWorkloadIdentityCredential(nil)
|
workloadCred, err := azidentity.NewWorkloadIdentityCredential(nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
creds = append(creds, workloadCred)
|
*creds = append(*creds, workloadCred)
|
||||||
} else {
|
} else {
|
||||||
errs = append(errs, err)
|
*errs = append(*errs, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. MSI with timeout of 1 second (same as DefaultAzureCredential)
|
func (s EnvironmentSettings) addManagedIdentityProvider(timeout time.Duration, creds *[]azcore.TokenCredential, errs *[]error) {
|
||||||
{
|
|
||||||
c := s.GetMSI()
|
c := s.GetMSI()
|
||||||
msiCred, err := c.GetTokenCredential()
|
msiCred, err := c.GetTokenCredential()
|
||||||
|
|
||||||
useTimeout := true
|
useTimeout := true
|
||||||
if _, ok := os.LookupEnv(identityEndpoint); ok {
|
if _, ok := os.LookupEnv(identityEndpoint); ok {
|
||||||
// App Service & Service Fabric
|
// App Service, Functions, Service Fabric and Container Apps
|
||||||
useTimeout = false
|
useTimeout = false
|
||||||
} else {
|
} else {
|
||||||
if _, ok := os.LookupEnv(arcIMDSEndpoint); ok {
|
if _, ok := os.LookupEnv(arcIMDSEndpoint); ok {
|
||||||
|
|
@ -176,23 +164,99 @@ func (s EnvironmentSettings) GetTokenCredential() (azcore.TokenCredential, error
|
||||||
|
|
||||||
// We need to use a timeout for MSI on environments where it is not available because the request for the default IMDS endpoint can hang for several minutes.
|
// We need to use a timeout for MSI on environments where it is not available because the request for the default IMDS endpoint can hang for several minutes.
|
||||||
if useTimeout {
|
if useTimeout {
|
||||||
msiCred = &timeoutWrapper{cred: msiCred, authmethod: "managed identity", timeout: 1 * time.Second}
|
msiCred = &timeoutWrapper{cred: msiCred, authmethod: "managed identity", timeout: timeout}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
creds = append(creds, msiCred)
|
*creds = append(*creds, msiCred)
|
||||||
} else {
|
} else {
|
||||||
errs = append(errs, err)
|
*errs = append(*errs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. AzureCLICredential
|
func (s EnvironmentSettings) addCLIProvider(timeout time.Duration, creds *[]azcore.TokenCredential, errs *[]error) {
|
||||||
{
|
|
||||||
cred, credErr := azidentity.NewAzureCLICredential(nil)
|
cred, credErr := azidentity.NewAzureCLICredential(nil)
|
||||||
if credErr == nil {
|
if credErr == nil {
|
||||||
creds = append(creds, &timeoutWrapper{cred: cred, authmethod: "Azure CLI", timeout: 30 * time.Second})
|
*creds = append(*creds, &timeoutWrapper{cred: cred, authmethod: "Azure CLI", timeout: 30 * time.Second})
|
||||||
} else {
|
} else {
|
||||||
errs = append(errs, credErr)
|
*errs = append(*errs, credErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s EnvironmentSettings) addProviderByAuthMethodName(authMethod string, creds *[]azcore.TokenCredential, errs *[]error) {
|
||||||
|
switch authMethod {
|
||||||
|
case "clientcredentials", "creds":
|
||||||
|
s.addClientCredentialsProvider(creds, errs)
|
||||||
|
case "clientcertificate", "cert":
|
||||||
|
s.addClientCertificateProvider(creds, errs)
|
||||||
|
case "workloadidentity", "wi":
|
||||||
|
s.addWorkloadIdentityProvider(creds, errs)
|
||||||
|
case "managedidentity", "mi":
|
||||||
|
s.addManagedIdentityProvider(1*time.Second, creds, errs)
|
||||||
|
case "commandlineinterface", "cli":
|
||||||
|
s.addCLIProvider(30*time.Second, creds, errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAzureAuthMethods() []string {
|
||||||
|
return []string{"clientcredentials", "creds", "clientcertificate", "cert", "workloadidentity", "wi", "managedidentity", "mi", "commandlineinterface", "cli", "none"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenCredential returns an azcore.TokenCredential retrieved from the order specified via
|
||||||
|
// the azureAuthMethods component metadata property which denotes a comma-separated list of auth methods to try in order.
|
||||||
|
// The possible values contained are (case-insensitive):
|
||||||
|
// ServicePrincipal, Certificate, WorkloadIdentity, ManagedIdentity, CLI
|
||||||
|
// The string "None" can be used to disable Azure authentication.
|
||||||
|
//
|
||||||
|
// If the azureAuthMethods property is not present, the following order is used (which with the exception of step 5
|
||||||
|
// matches the DefaultAzureCredential order):
|
||||||
|
// 1. Client credentials
|
||||||
|
// 2. Client certificate
|
||||||
|
// 3. Workload identity
|
||||||
|
// 4. MSI (we use a timeout of 1 second when no compatible managed identity implementation is available)
|
||||||
|
// 5. Azure CLI
|
||||||
|
func (s EnvironmentSettings) GetTokenCredential() (azcore.TokenCredential, error) {
|
||||||
|
// Create a chain
|
||||||
|
var creds []azcore.TokenCredential
|
||||||
|
errs := make([]error, 0, 3)
|
||||||
|
|
||||||
|
authMethods, ok := s.GetEnvironment("AzureAuthMethods")
|
||||||
|
if !ok || strings.TrimSpace(authMethods) == "" {
|
||||||
|
// 1. Client credentials
|
||||||
|
s.addClientCredentialsProvider(&creds, &errs)
|
||||||
|
|
||||||
|
// 2. Client certificate
|
||||||
|
s.addClientCertificateProvider(&creds, &errs)
|
||||||
|
|
||||||
|
// 3. Workload identity
|
||||||
|
s.addWorkloadIdentityProvider(&creds, &errs)
|
||||||
|
|
||||||
|
// 4. MSI with timeout of 1 second (same as DefaultAzureCredential)
|
||||||
|
s.addManagedIdentityProvider(1*time.Second, &creds, &errs)
|
||||||
|
|
||||||
|
// 5. AzureCLICredential
|
||||||
|
s.addCLIProvider(30*time.Second, &creds, &errs)
|
||||||
|
} else {
|
||||||
|
authMethodIdentifiers := getAzureAuthMethods()
|
||||||
|
authMethods := strings.Split(strings.ToLower(strings.TrimSpace(authMethods)), ",")
|
||||||
|
for _, authMethod := range authMethods {
|
||||||
|
authMethod = strings.TrimSpace(authMethod)
|
||||||
|
found := false
|
||||||
|
for _, authMethodIdentifier := range authMethodIdentifiers {
|
||||||
|
if authMethod == authMethodIdentifier {
|
||||||
|
found = true
|
||||||
|
if authMethod != "none" {
|
||||||
|
s.addProviderByAuthMethodName(authMethod, &creds, &errs)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// If authMethod is "none", we don't add any provider and return an error
|
||||||
|
return nil, fmt.Errorf("all Azure auth methods have been disabled with auth method 'None'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("invalid Azure auth method: %v", authMethod)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,80 @@ func TestAuthorizorWithMSI(t *testing.T) {
|
||||||
assert.NotNil(t, spt)
|
assert.NotNil(t, spt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFallbackToMSIbutAzureAuthDisallowed(t *testing.T) {
|
||||||
|
os.Setenv("MSI_ENDPOINT", "test")
|
||||||
|
defer os.Unsetenv("MSI_ENDPOINT")
|
||||||
|
settings, err := NewEnvironmentSettings(
|
||||||
|
map[string]string{
|
||||||
|
"azureClientId": fakeClientID,
|
||||||
|
"vaultName": "vaultName",
|
||||||
|
"azureAuthMethods": "None",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = settings.GetTokenCredential()
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.ErrorContains(t, err, "all Azure auth methods have been disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFallbackToMSIandInAllowedList(t *testing.T) {
|
||||||
|
os.Setenv("MSI_ENDPOINT", "test")
|
||||||
|
defer os.Unsetenv("MSI_ENDPOINT")
|
||||||
|
settings, err := NewEnvironmentSettings(
|
||||||
|
map[string]string{
|
||||||
|
"azureClientId": fakeClientID,
|
||||||
|
"vaultName": "vaultName",
|
||||||
|
"azureAuthMethods": "clientcredentials,clientcertificate,workloadidentity,managedIdentity",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
testCertConfig := settings.GetMSI()
|
||||||
|
assert.NotNil(t, testCertConfig)
|
||||||
|
|
||||||
|
spt, err := settings.GetTokenCredential()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, spt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFallbackToMSIandNotInAllowedList(t *testing.T) {
|
||||||
|
os.Setenv("MSI_ENDPOINT", "test")
|
||||||
|
defer os.Unsetenv("MSI_ENDPOINT")
|
||||||
|
settings, err := NewEnvironmentSettings(
|
||||||
|
map[string]string{
|
||||||
|
"azureClientId": fakeClientID,
|
||||||
|
"vaultName": "vaultName",
|
||||||
|
"azureAuthMethods": "clientcredentials,clientcertificate,workloadidentity",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = settings.GetTokenCredential()
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.ErrorContains(t, err, "no suitable token provider for Azure AD")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFallbackToMSIandInvalidAuthMethod(t *testing.T) {
|
||||||
|
os.Setenv("MSI_ENDPOINT", "test")
|
||||||
|
defer os.Unsetenv("MSI_ENDPOINT")
|
||||||
|
settings, err := NewEnvironmentSettings(
|
||||||
|
map[string]string{
|
||||||
|
"azureClientId": fakeClientID,
|
||||||
|
"vaultName": "vaultName",
|
||||||
|
"azureAuthMethods": "clientcredentials,clientcertificate,workloadidentity,managedIdentity,cli,SUPERAUTH",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
testCertConfig := settings.GetMSI()
|
||||||
|
require.NotNil(t, testCertConfig)
|
||||||
|
|
||||||
|
_, err = settings.GetTokenCredential()
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.ErrorContains(t, err, "invalid Azure auth method: superauth")
|
||||||
|
}
|
||||||
|
|
||||||
func TestAuthorizorWithMSIAndUserAssignedID(t *testing.T) {
|
func TestAuthorizorWithMSIAndUserAssignedID(t *testing.T) {
|
||||||
os.Setenv("MSI_ENDPOINT", "test")
|
os.Setenv("MSI_ENDPOINT", "test")
|
||||||
defer os.Unsetenv("MSI_ENDPOINT")
|
defer os.Unsetenv("MSI_ENDPOINT")
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ var MetadataKeys = map[string][]string{ //nolint:gochecknoglobals
|
||||||
// Identifier for the Azure environment
|
// Identifier for the Azure environment
|
||||||
// Allowed values (case-insensitive): AzurePublicCloud/AzurePublic, AzureChinaCloud/AzureChina, AzureUSGovernmentCloud/AzureUSGovernment
|
// Allowed values (case-insensitive): AzurePublicCloud/AzurePublic, AzureChinaCloud/AzureChina, AzureUSGovernmentCloud/AzureUSGovernment
|
||||||
"AzureEnvironment": {"azureEnvironment", "azureCloud"},
|
"AzureEnvironment": {"azureEnvironment", "azureCloud"},
|
||||||
|
// Identifier for the Azure authentication methods to try (in order), comma-separated
|
||||||
|
// Allowed values (case-insensitive): ClientCredentials, creds, ClientCertificate, cert, WorkloadIdentity, wi, ManagedIdentity, mi, CommandLineInterface, cli, None
|
||||||
|
"AzureAuthMethods": {"azureAuthMethods", "azureADAuthMethods", "entraIDAuthMethods", "microsoftEntraIDAuthMethods"},
|
||||||
|
|
||||||
// Metadata keys for storage components
|
// Metadata keys for storage components
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue