bootstrapper: make Azure auth method configurable on cluster init (#1346)

* bootstrapper: make Azure auth method configurable on cluster init
* azure: convert uami resource ID to clientID


Co-authored-by: 3u13r <lc@edgeless.systems>
This commit is contained in:
Malte Poll 2023-04-03 15:01:25 +02:00 committed by GitHub
parent 5cb1899c27
commit d15968bed7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 307 additions and 209 deletions

View File

@ -13,7 +13,6 @@ go_library(
deps = [ deps = [
"//bootstrapper/internal/kubernetes/k8sapi", "//bootstrapper/internal/kubernetes/k8sapi",
"//bootstrapper/internal/kubernetes/kubewaiter", "//bootstrapper/internal/kubernetes/kubewaiter",
"//internal/cloud/azureshared",
"//internal/cloud/cloudprovider", "//internal/cloud/cloudprovider",
"//internal/cloud/gcpshared", "//internal/cloud/gcpshared",
"//internal/cloud/metadata", "//internal/cloud/metadata",

View File

@ -20,7 +20,6 @@ import (
"github.com/edgelesssys/constellation/v2/bootstrapper/internal/kubernetes/k8sapi" "github.com/edgelesssys/constellation/v2/bootstrapper/internal/kubernetes/k8sapi"
"github.com/edgelesssys/constellation/v2/bootstrapper/internal/kubernetes/kubewaiter" "github.com/edgelesssys/constellation/v2/bootstrapper/internal/kubernetes/kubewaiter"
"github.com/edgelesssys/constellation/v2/internal/cloud/azureshared"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared" "github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared"
"github.com/edgelesssys/constellation/v2/internal/cloud/openstack" "github.com/edgelesssys/constellation/v2/internal/cloud/openstack"
@ -477,24 +476,6 @@ func (k *KubeWrapper) setupExtraVals(ctx context.Context, serviceConfig constell
"subnetworkPodCIDR": serviceConfig.subnetworkPodCIDR, "subnetworkPodCIDR": serviceConfig.subnetworkPodCIDR,
} }
subscriptionID, resourceGroup, err := azureshared.BasicsFromProviderID(instance.ProviderID)
if err != nil {
return nil, err
}
creds, err := azureshared.ApplicationCredentialsFromURI(serviceConfig.cloudServiceAccountURI)
if err != nil {
return nil, err
}
extraVals["autoscaler"] = map[string]any{
"Azure": map[string]any{
"clientID": creds.AppClientID,
"clientSecret": creds.ClientSecretValue,
"resourceGroup": resourceGroup,
"subscriptionID": subscriptionID,
"tenantID": creds.TenantID,
},
}
case cloudprovider.OpenStack: case cloudprovider.OpenStack:
creds, err := openstack.AccountKeyFromURI(serviceConfig.cloudServiceAccountURI) creds, err := openstack.AccountKeyFromURI(serviceConfig.cloudServiceAccountURI)
if err != nil { if err != nil {

View File

@ -451,6 +451,9 @@ func (i *initCmd) getMarshaledServiceAccountURI(provider cloudprovider.Provider,
AppClientID: config.Provider.Azure.AppClientID, AppClientID: config.Provider.Azure.AppClientID,
ClientSecretValue: config.Provider.Azure.ClientSecretValue, ClientSecretValue: config.Provider.Azure.ClientSecretValue,
Location: config.Provider.Azure.Location, Location: config.Provider.Azure.Location,
// TODO(malt3): Switch preferred auth method to uami as planned by AB#2961
PreferredAuthMethod: azureshared.AuthMethodServicePrincipal,
UamiResourceID: config.Provider.Azure.UserAssignedIdentity,
} }
return creds.ToCloudServiceAccountURI(), nil return creds.ToCloudServiceAccountURI(), nil

View File

@ -181,7 +181,6 @@ go_library(
"charts/edgeless/constellation-services/charts/autoscaler/Chart.yaml", "charts/edgeless/constellation-services/charts/autoscaler/Chart.yaml",
"charts/edgeless/constellation-services/charts/autoscaler/templates/aws-deployment.yaml", "charts/edgeless/constellation-services/charts/autoscaler/templates/aws-deployment.yaml",
"charts/edgeless/constellation-services/charts/autoscaler/templates/azure-deployment.yaml", "charts/edgeless/constellation-services/charts/autoscaler/templates/azure-deployment.yaml",
"charts/edgeless/constellation-services/charts/autoscaler/templates/azure-secret.yaml",
"charts/edgeless/constellation-services/charts/autoscaler/templates/clusterrole.yaml", "charts/edgeless/constellation-services/charts/autoscaler/templates/clusterrole.yaml",
"charts/edgeless/constellation-services/charts/autoscaler/templates/clusterrolebinding.yaml", "charts/edgeless/constellation-services/charts/autoscaler/templates/clusterrolebinding.yaml",
"charts/edgeless/constellation-services/charts/autoscaler/templates/gcp-deployment.yaml", "charts/edgeless/constellation-services/charts/autoscaler/templates/gcp-deployment.yaml",

View File

@ -24,43 +24,25 @@ spec:
- name: cluster-autoscaler - name: cluster-autoscaler
image: {{ .Values.image | quote }} image: {{ .Values.image | quote }}
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command:
- ./cluster-autoscaler
args:
- --cloud-provider=azure
- --cloud-config=/etc/azure/azure.json
- --logtostderr=true
- --namespace=kube-system
- --stderrthreshold=info
- --v=2
volumeMounts:
- name: azureconfig
mountPath: /etc/azure
readOnly: true
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /health-check path: /health-check
port: 8085 port: 8085
ports: ports:
- containerPort: 8085 - containerPort: 8085
env:
- name: ARM_SUBSCRIPTION_ID
valueFrom:
secretKeyRef:
key: SubscriptionID
name: cluster-autoscaler-azure
- name: ARM_RESOURCE_GROUP
valueFrom:
secretKeyRef:
key: ResourceGroup
name: cluster-autoscaler-azure
- name: ARM_TENANT_ID
valueFrom:
secretKeyRef:
key: TenantID
name: cluster-autoscaler-azure
- name: ARM_CLIENT_ID
valueFrom:
secretKeyRef:
key: ClientID
name: cluster-autoscaler-azure
- name: ARM_CLIENT_SECRET
valueFrom:
secretKeyRef:
key: ClientSecret
name: cluster-autoscaler-azure
- name: ARM_VM_TYPE
valueFrom:
secretKeyRef:
key: VMType
name: cluster-autoscaler-azure
resources: {} resources: {}
dnsPolicy: ClusterFirst dnsPolicy: ClusterFirst
nodeSelector: nodeSelector:
@ -78,4 +60,8 @@ spec:
key: node.cloudprovider.kubernetes.io/uninitialized key: node.cloudprovider.kubernetes.io/uninitialized
operator: Equal operator: Equal
value: "true" value: "true"
volumes:
- name: azureconfig
secret:
secretName: azureconfig
{{- end -}} {{- end -}}

View File

@ -1,15 +0,0 @@
{{- if eq .Values.csp "Azure" -}}
apiVersion: v1
kind: Secret
metadata:
name: cluster-autoscaler-azure
namespace: {{ .Release.Namespace }}
data:
ClientID: {{ .Values.Azure.clientID | b64enc }}
ClientSecret: {{ .Values.Azure.clientSecret | b64enc }}
ResourceGroup: {{ .Values.Azure.resourceGroup | b64enc }}
SubscriptionID: {{ .Values.Azure.subscriptionID | b64enc }}
TenantID: {{ .Values.Azure.tenantID | b64enc }}
{{/* b64encode("vmss") */}}
VMType: dm1zcw==
{{- end -}}

View File

@ -16,60 +16,12 @@
"examples": [ "examples": [
"registry.k8s.io/autoscaling/cluster-autoscaler:v1.23.1" "registry.k8s.io/autoscaling/cluster-autoscaler:v1.23.1"
] ]
},
"Azure": {
"description": "Config values required for deployment on Azure",
"type": "object",
"properties": {
"clientID": {
"description": "Client ID of the service account used to access the Azure API.",
"type": "string"
},
"clientSecret": {
"description": "Secret of the service account used to access the Azure API.",
"type": "string"
},
"resourceGroup": {
"description": "Resource group in which the cluster is running.",
"type": "string"
},
"subscriptionID": {
"description": "Subscription ID of the Azure subscription.",
"type": "string"
},
"tenantID": {
"description": "Tenant ID of the Azure subscription.",
"type": "string"
}
},
"required": [
"clientID",
"clientSecret",
"resourceGroup",
"subscriptionID",
"tenantID"
]
} }
}, },
"required": [ "required": [
"csp", "csp",
"image" "image"
], ],
"if": {
"properties": {
"csp": {
"const": "Azure"
}
},
"required": [
"csp"
]
},
"then": {
"required": [
"Azure"
]
},
"title": "Values", "title": "Values",
"type": "object" "type": "object"
} }

View File

@ -23,43 +23,25 @@ spec:
- name: cluster-autoscaler - name: cluster-autoscaler
image: autoscalerImage image: autoscalerImage
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command:
- ./cluster-autoscaler
args:
- --cloud-provider=azure
- --cloud-config=/etc/azure/azure.json
- --logtostderr=true
- --namespace=kube-system
- --stderrthreshold=info
- --v=2
volumeMounts:
- name: azureconfig
mountPath: /etc/azure
readOnly: true
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /health-check path: /health-check
port: 8085 port: 8085
ports: ports:
- containerPort: 8085 - containerPort: 8085
env:
- name: ARM_SUBSCRIPTION_ID
valueFrom:
secretKeyRef:
key: SubscriptionID
name: cluster-autoscaler-azure
- name: ARM_RESOURCE_GROUP
valueFrom:
secretKeyRef:
key: ResourceGroup
name: cluster-autoscaler-azure
- name: ARM_TENANT_ID
valueFrom:
secretKeyRef:
key: TenantID
name: cluster-autoscaler-azure
- name: ARM_CLIENT_ID
valueFrom:
secretKeyRef:
key: ClientID
name: cluster-autoscaler-azure
- name: ARM_CLIENT_SECRET
valueFrom:
secretKeyRef:
key: ClientSecret
name: cluster-autoscaler-azure
- name: ARM_VM_TYPE
valueFrom:
secretKeyRef:
key: VMType
name: cluster-autoscaler-azure
resources: {} resources: {}
dnsPolicy: ClusterFirst dnsPolicy: ClusterFirst
nodeSelector: nodeSelector:
@ -77,3 +59,7 @@ spec:
key: node.cloudprovider.kubernetes.io/uninitialized key: node.cloudprovider.kubernetes.io/uninitialized
operator: Equal operator: Equal
value: "true" value: "true"
volumes:
- name: azureconfig
secret:
secretName: azureconfig

View File

@ -125,19 +125,30 @@ func (c *Cloud) GetCCMConfig(ctx context.Context, providerID string, cloudServic
return nil, fmt.Errorf("could not dereference load balancer name") return nil, fmt.Errorf("could not dereference load balancer name")
} }
var uamiClientID string
useManagedIdentityExtension := creds.PreferredAuthMethod == azureshared.AuthMethodUserAssignedIdentity
if useManagedIdentityExtension {
uamiClientID, err = c.getUAMIClientIDFromURI(ctx, providerID, creds.UamiResourceID)
if err != nil {
return nil, fmt.Errorf("retrieving user-assigned managed identity client ID: %w", err)
}
}
config := cloudConfig{ config := cloudConfig{
Cloud: "AzurePublicCloud", Cloud: "AzurePublicCloud",
TenantID: creds.TenantID, TenantID: creds.TenantID,
SubscriptionID: subscriptionID, SubscriptionID: subscriptionID,
ResourceGroup: resourceGroup, ResourceGroup: resourceGroup,
LoadBalancerSku: "standard", LoadBalancerSku: "standard",
SecurityGroupName: securityGroupName, SecurityGroupName: securityGroupName,
LoadBalancerName: *loadBalancer.Name, LoadBalancerName: *loadBalancer.Name,
UseInstanceMetadata: true, UseInstanceMetadata: true,
VMType: "vmss", VMType: "vmss",
Location: creds.Location, Location: creds.Location,
AADClientID: creds.AppClientID, UseManagedIdentityExtension: useManagedIdentityExtension,
AADClientSecret: creds.ClientSecretValue, UserAssignedIdentityID: uamiClientID,
AADClientID: creds.AppClientID,
AADClientSecret: creds.ClientSecretValue,
} }
return json.Marshal(config) return json.Marshal(config)
@ -304,6 +315,24 @@ func (c *Cloud) getInstance(ctx context.Context, providerID string) (metadata.In
return instance, nil return instance, nil
} }
func (c *Cloud) getUAMIClientIDFromURI(ctx context.Context, providerID, resourceID string) (string, error) {
// userAssignedIdentityURI := "/subscriptions/{subscription-id}/resourcegroups/{resource-group}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identity-name}"
_, resourceGroup, scaleSet, instanceID, err := azureshared.ScaleSetInformationFromProviderID(providerID)
if err != nil {
return "", fmt.Errorf("invalid provider ID: %w", err)
}
vmResp, err := c.scaleSetsVMAPI.Get(ctx, resourceGroup, scaleSet, instanceID, nil)
if err != nil {
return "", fmt.Errorf("retrieving instance: %w", err)
}
for rID, v := range vmResp.Identity.UserAssignedIdentities {
if rID == resourceID {
return *v.ClientID, nil
}
}
return "", fmt.Errorf("no user assinged identity found for resource ID %s", resourceID)
}
// getNetworkSecurityGroupName returns the security group name of the resource group. // getNetworkSecurityGroupName returns the security group name of the resource group.
func (c *Cloud) getNetworkSecurityGroupName(ctx context.Context, resourceGroup, uid string) (string, error) { func (c *Cloud) getNetworkSecurityGroupName(ctx context.Context, resourceGroup, uid string) (string, error) {
pager := c.secGroupAPI.NewListPager(resourceGroup, nil) pager := c.secGroupAPI.NewListPager(resourceGroup, nil)
@ -383,23 +412,25 @@ func (c *Cloud) getVMInterfaces(ctx context.Context, vm armcompute.VirtualMachin
} }
type cloudConfig struct { type cloudConfig struct {
Cloud string `json:"cloud,omitempty"` Cloud string `json:"cloud,omitempty"`
TenantID string `json:"tenantId,omitempty"` TenantID string `json:"tenantId,omitempty"`
SubscriptionID string `json:"subscriptionId,omitempty"` SubscriptionID string `json:"subscriptionId,omitempty"`
ResourceGroup string `json:"resourceGroup,omitempty"` ResourceGroup string `json:"resourceGroup,omitempty"`
Location string `json:"location,omitempty"` Location string `json:"location,omitempty"`
SubnetName string `json:"subnetName,omitempty"` SubnetName string `json:"subnetName,omitempty"`
SecurityGroupName string `json:"securityGroupName,omitempty"` SecurityGroupName string `json:"securityGroupName,omitempty"`
SecurityGroupResourceGroup string `json:"securityGroupResourceGroup,omitempty"` SecurityGroupResourceGroup string `json:"securityGroupResourceGroup,omitempty"`
LoadBalancerName string `json:"loadBalancerName,omitempty"` LoadBalancerName string `json:"loadBalancerName,omitempty"`
LoadBalancerSku string `json:"loadBalancerSku,omitempty"` LoadBalancerSku string `json:"loadBalancerSku,omitempty"`
VNetName string `json:"vnetName,omitempty"` VNetName string `json:"vnetName,omitempty"`
VNetResourceGroup string `json:"vnetResourceGroup,omitempty"` VNetResourceGroup string `json:"vnetResourceGroup,omitempty"`
CloudProviderBackoff bool `json:"cloudProviderBackoff,omitempty"` CloudProviderBackoff bool `json:"cloudProviderBackoff,omitempty"`
UseInstanceMetadata bool `json:"useInstanceMetadata,omitempty"` UseInstanceMetadata bool `json:"useInstanceMetadata,omitempty"`
VMType string `json:"vmType,omitempty"` VMType string `json:"vmType,omitempty"`
AADClientID string `json:"aadClientId,omitempty"` UseManagedIdentityExtension bool `json:"useManagedIdentityExtension,omitempty"`
AADClientSecret string `json:"aadClientSecret,omitempty"` UserAssignedIdentityID string `json:"userAssignedIdentityID,omitempty"`
AADClientID string `json:"aadClientId,omitempty"`
AADClientSecret string `json:"aadClientSecret,omitempty"`
} }
// convertToInstanceMetadata converts a armcomputev2.VirtualMachineScaleSetVM to a metadata.InstanceMetadata. // convertToInstanceMetadata converts a armcomputev2.VirtualMachineScaleSetVM to a metadata.InstanceMetadata.

View File

@ -52,10 +52,13 @@ func TestGetCCMConfig(t *testing.T) {
Name: to.Ptr("security-group"), Name: to.Ptr("security-group"),
} }
uamiClientID := "uami-client-id"
testCases := map[string]struct { testCases := map[string]struct {
imdsAPI imdsAPI imdsAPI imdsAPI
loadBalancerAPI loadBalancerAPI loadBalancerAPI loadBalancerAPI
secGroupAPI securityGroupsAPI secGroupAPI securityGroupsAPI
scaleSetsVMAPI virtualMachineScaleSetVMsAPI
providerID string providerID string
cloudServiceAccountURI string cloudServiceAccountURI string
wantErr bool wantErr bool
@ -75,8 +78,50 @@ func TestGetCCMConfig(t *testing.T) {
list: []armnetwork.SecurityGroup{goodSecurityGroup}, list: []armnetwork.SecurityGroup{goodSecurityGroup},
}, },
}, },
scaleSetsVMAPI: &stubVirtualMachineScaleSetVMsAPI{
getVM: armcompute.VirtualMachineScaleSetVM{
Identity: &armcompute.VirtualMachineIdentity{
UserAssignedIdentities: map[string]*armcompute.UserAssignedIdentitiesValue{
"subscriptions/9b352db0-82af-408c-a02c-36fbffbf7015/resourceGroups/resourceGroupName/providers/Microsoft.ManagedIdentity/userAssignedIdentities/UAMIName": {ClientID: &uamiClientID},
},
},
},
},
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set/virtualMachines/0", providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set/virtualMachines/0",
cloudServiceAccountURI: "serviceaccount://azure?tenant_id=tenant-id&client_id=client-id&client_secret=client-secret&location=westeurope", cloudServiceAccountURI: "serviceaccount://azure?tenant_id=tenant-id&client_id=client-id&client_secret=client-secret&location=westeurope&preferred_auth_method=userassignedidentity&uami_resource_id=subscriptions%2F9b352db0-82af-408c-a02c-36fbffbf7015%2FresourceGroups%2FresourceGroupName%2Fproviders%2FMicrosoft.ManagedIdentity%2FuserAssignedIdentities%2FUAMIName",
wantConfig: cloudConfig{
Cloud: "AzurePublicCloud",
TenantID: "tenant-id",
SubscriptionID: "subscription-id",
ResourceGroup: "resource-group",
LoadBalancerSku: "standard",
SecurityGroupName: "security-group",
LoadBalancerName: "load-balancer",
UseInstanceMetadata: true,
UseManagedIdentityExtension: true,
UserAssignedIdentityID: uamiClientID,
VMType: "vmss",
Location: "westeurope",
AADClientID: "client-id",
AADClientSecret: "client-secret",
},
},
"no app registration": {
imdsAPI: &stubIMDSAPI{
uidVal: "uid",
},
loadBalancerAPI: &stubLoadBalancersAPI{
pager: &stubLoadBalancersClientListPager{
list: []armnetwork.LoadBalancer{goodLB},
},
},
secGroupAPI: &stubSecurityGroupsAPI{
pager: &stubSecurityGroupsClientListPager{
list: []armnetwork.SecurityGroup{goodSecurityGroup},
},
},
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set/virtualMachines/0",
cloudServiceAccountURI: "serviceaccount://azure?tenant_id=tenant-id&location=westeurope",
wantConfig: cloudConfig{ wantConfig: cloudConfig{
Cloud: "AzurePublicCloud", Cloud: "AzurePublicCloud",
TenantID: "tenant-id", TenantID: "tenant-id",
@ -88,8 +133,6 @@ func TestGetCCMConfig(t *testing.T) {
UseInstanceMetadata: true, UseInstanceMetadata: true,
VMType: "vmss", VMType: "vmss",
Location: "westeurope", Location: "westeurope",
AADClientID: "client-id",
AADClientSecret: "client-secret",
}, },
}, },
"missing UID tag": { "missing UID tag": {
@ -303,6 +346,7 @@ func TestGetCCMConfig(t *testing.T) {
imds: tc.imdsAPI, imds: tc.imdsAPI,
loadBalancerAPI: tc.loadBalancerAPI, loadBalancerAPI: tc.loadBalancerAPI,
secGroupAPI: tc.secGroupAPI, secGroupAPI: tc.secGroupAPI,
scaleSetsVMAPI: tc.scaleSetsVMAPI,
} }
config, err := cloud.GetCCMConfig(context.Background(), tc.providerID, tc.cloudServiceAccountURI) config, err := cloud.GetCCMConfig(context.Background(), tc.providerID, tc.cloudServiceAccountURI)
if tc.wantErr { if tc.wantErr {

View File

@ -5,6 +5,7 @@ go_library(
name = "azureshared", name = "azureshared",
srcs = [ srcs = [
"appcredentials.go", "appcredentials.go",
"authmethod_string.go",
"azureshared.go", "azureshared.go",
"metadata.go", "metadata.go",
], ],

View File

@ -9,15 +9,19 @@ package azureshared
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"strings"
) )
// ApplicationCredentials is a set of Azure AD application credentials. // ApplicationCredentials is a set of Azure API credentials.
// It can contain a client secret and carries the preferred authentication method.
// It is the equivalent of a service account key in other cloud providers. // It is the equivalent of a service account key in other cloud providers.
type ApplicationCredentials struct { type ApplicationCredentials struct {
TenantID string TenantID string
AppClientID string AppClientID string
ClientSecretValue string ClientSecretValue string
Location string Location string
UamiResourceID string
PreferredAuthMethod AuthMethod
} }
// ApplicationCredentialsFromURI converts a cloudServiceAccountURI into Azure ApplicationCredentials. // ApplicationCredentialsFromURI converts a cloudServiceAccountURI into Azure ApplicationCredentials.
@ -33,11 +37,14 @@ func ApplicationCredentialsFromURI(cloudServiceAccountURI string) (ApplicationCr
return ApplicationCredentials{}, fmt.Errorf("invalid service account URI: invalid host: %s", uri.Host) return ApplicationCredentials{}, fmt.Errorf("invalid service account URI: invalid host: %s", uri.Host)
} }
query := uri.Query() query := uri.Query()
preferredAuthMethod := FromString(query.Get("preferred_auth_method"))
return ApplicationCredentials{ return ApplicationCredentials{
TenantID: query.Get("tenant_id"), TenantID: query.Get("tenant_id"),
AppClientID: query.Get("client_id"), AppClientID: query.Get("client_id"),
ClientSecretValue: query.Get("client_secret"), ClientSecretValue: query.Get("client_secret"),
Location: query.Get("location"), Location: query.Get("location"),
UamiResourceID: query.Get("uami_resource_id"),
PreferredAuthMethod: preferredAuthMethod,
}, nil }, nil
} }
@ -45,9 +52,19 @@ func ApplicationCredentialsFromURI(cloudServiceAccountURI string) (ApplicationCr
func (c ApplicationCredentials) ToCloudServiceAccountURI() string { func (c ApplicationCredentials) ToCloudServiceAccountURI() string {
query := url.Values{} query := url.Values{}
query.Add("tenant_id", c.TenantID) query.Add("tenant_id", c.TenantID)
query.Add("client_id", c.AppClientID)
query.Add("client_secret", c.ClientSecretValue)
query.Add("location", c.Location) query.Add("location", c.Location)
if c.AppClientID != "" {
query.Add("client_id", c.AppClientID)
}
if c.ClientSecretValue != "" {
query.Add("client_secret", c.ClientSecretValue)
}
if c.UamiResourceID != "" {
query.Add("uami_resource_id", c.UamiResourceID)
}
if c.PreferredAuthMethod != AuthMethodUnknown {
query.Add("preferred_auth_method", c.PreferredAuthMethod.String())
}
uri := url.URL{ uri := url.URL{
Scheme: "serviceaccount", Scheme: "serviceaccount",
Host: "azure", Host: "azure",
@ -55,3 +72,29 @@ func (c ApplicationCredentials) ToCloudServiceAccountURI() string {
} }
return uri.String() return uri.String()
} }
//go:generate stringer -type=AuthMethod -trimprefix=AuthMethod
// AuthMethod is the authentication method used for the Azure API.
type AuthMethod uint32
// FromString converts a string into an AuthMethod.
func FromString(s string) AuthMethod {
switch strings.ToLower(s) {
case strings.ToLower(AuthMethodServicePrincipal.String()):
return AuthMethodServicePrincipal
case strings.ToLower(AuthMethodUserAssignedIdentity.String()):
return AuthMethodUserAssignedIdentity
default:
return AuthMethodUnknown
}
}
const (
// AuthMethodUnknown is default value for AuthMethod.
AuthMethodUnknown AuthMethod = iota
// AuthMethodServicePrincipal uses a client ID and secret.
AuthMethodServicePrincipal
// AuthMethodUserAssignedIdentity uses a user assigned identity.
AuthMethodUserAssignedIdentity
)

View File

@ -21,6 +21,20 @@ func TestMain(m *testing.M) {
func TestApplicationCredentialsFromURI(t *testing.T) { func TestApplicationCredentialsFromURI(t *testing.T) {
creds := ApplicationCredentials{ creds := ApplicationCredentials{
TenantID: "tenant-id",
AppClientID: "client-id",
ClientSecretValue: "client-secret",
Location: "location",
UamiResourceID: "subscriptions/9b352db0-82af-408c-a02c-36fbffbf7015/resourceGroups/resourceGroupName/providers/Microsoft.ManagedIdentity/userAssignedIdentities/UAMIName",
PreferredAuthMethod: AuthMethodServicePrincipal,
}
credsWithoutSecret := ApplicationCredentials{
TenantID: "tenant-id",
Location: "location",
UamiResourceID: "subscriptions/9b352db0-82af-408c-a02c-36fbffbf7015/resourceGroups/resourceGroupName/providers/Microsoft.ManagedIdentity/userAssignedIdentities/UAMIName",
PreferredAuthMethod: AuthMethodUserAssignedIdentity,
}
credsWithoutPreferrredAuthMethod := ApplicationCredentials{
TenantID: "tenant-id", TenantID: "tenant-id",
AppClientID: "client-id", AppClientID: "client-id",
ClientSecretValue: "client-secret", ClientSecretValue: "client-secret",
@ -32,9 +46,17 @@ func TestApplicationCredentialsFromURI(t *testing.T) {
wantErr bool wantErr bool
}{ }{
"getApplicationCredentials works": { "getApplicationCredentials works": {
cloudServiceAccountURI: "serviceaccount://azure?tenant_id=tenant-id&client_id=client-id&client_secret=client-secret&location=location", cloudServiceAccountURI: "serviceaccount://azure?tenant_id=tenant-id&client_id=client-id&client_secret=client-secret&location=location&preferred_auth_method=serviceprincipal&uami_resource_id=subscriptions%2F9b352db0-82af-408c-a02c-36fbffbf7015%2FresourceGroups%2FresourceGroupName%2Fproviders%2FMicrosoft.ManagedIdentity%2FuserAssignedIdentities%2FUAMIName",
wantCreds: creds, wantCreds: creds,
}, },
"can parse URI without app registration / secret": {
cloudServiceAccountURI: "serviceaccount://azure?tenant_id=tenant-id&location=location&preferred_auth_method=userassignedidentity&uami_resource_id=subscriptions%2F9b352db0-82af-408c-a02c-36fbffbf7015%2FresourceGroups%2FresourceGroupName%2Fproviders%2FMicrosoft.ManagedIdentity%2FuserAssignedIdentities%2FUAMIName",
wantCreds: credsWithoutSecret,
},
"can parse URI without preferred auth method": {
cloudServiceAccountURI: "serviceaccount://azure?tenant_id=tenant-id&client_id=client-id&client_secret=client-secret&location=location",
wantCreds: credsWithoutPreferrredAuthMethod,
},
"invalid URI fails": { "invalid URI fails": {
cloudServiceAccountURI: "\x00", cloudServiceAccountURI: "\x00",
wantErr: true, wantErr: true,
@ -66,25 +88,66 @@ func TestApplicationCredentialsFromURI(t *testing.T) {
} }
func TestToCloudServiceAccountURI(t *testing.T) { func TestToCloudServiceAccountURI(t *testing.T) {
assert := assert.New(t) testCases := map[string]struct {
require := require.New(t) credentials ApplicationCredentials
key := ApplicationCredentials{ wantURLValues url.Values
TenantID: "tenant-id", }{
AppClientID: "client-id", "client id and secret without preferred auth method": {
ClientSecretValue: "client-secret", credentials: ApplicationCredentials{
Location: "location", TenantID: "tenant-id",
AppClientID: "client-id",
ClientSecretValue: "client-secret",
Location: "location",
},
wantURLValues: url.Values{
"tenant_id": []string{"tenant-id"},
"client_id": []string{"client-id"},
"client_secret": []string{"client-secret"},
"location": []string{"location"},
},
},
"client id and secret with preferred auth method": {
credentials: ApplicationCredentials{
TenantID: "tenant-id",
AppClientID: "client-id",
ClientSecretValue: "client-secret",
Location: "location",
PreferredAuthMethod: AuthMethodServicePrincipal,
},
wantURLValues: url.Values{
"tenant_id": []string{"tenant-id"},
"client_id": []string{"client-id"},
"client_secret": []string{"client-secret"},
"location": []string{"location"},
"preferred_auth_method": []string{"ServicePrincipal"},
},
},
"only preferred auth method": {
credentials: ApplicationCredentials{
TenantID: "tenant-id",
Location: "location",
PreferredAuthMethod: AuthMethodUserAssignedIdentity,
},
wantURLValues: url.Values{
"tenant_id": []string{"tenant-id"},
"location": []string{"location"},
"preferred_auth_method": []string{"UserAssignedIdentity"},
},
},
} }
cloudServiceAccountURI := key.ToCloudServiceAccountURI() for name, tc := range testCases {
uri, err := url.Parse(cloudServiceAccountURI) t.Run(name, func(t *testing.T) {
require.NoError(err) assert := assert.New(t)
query := uri.Query() require := require.New(t)
assert.Equal("serviceaccount", uri.Scheme)
assert.Equal("azure", uri.Host) cloudServiceAccountURI := tc.credentials.ToCloudServiceAccountURI()
assert.Equal(url.Values{ uri, err := url.Parse(cloudServiceAccountURI)
"tenant_id": []string{"tenant-id"}, require.NoError(err)
"client_id": []string{"client-id"}, query := uri.Query()
"client_secret": []string{"client-secret"}, assert.Equal("serviceaccount", uri.Scheme)
"location": []string{"location"}, assert.Equal("azure", uri.Host)
}, query) assert.Equal(tc.wantURLValues, query)
})
}
} }

View File

@ -0,0 +1,25 @@
// Code generated by "stringer -type=AuthMethod -trimprefix=AuthMethod"; DO NOT EDIT.
package azureshared
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[AuthMethodUnknown-0]
_ = x[AuthMethodServicePrincipal-1]
_ = x[AuthMethodUserAssignedIdentity-2]
}
const _AuthMethod_name = "UnknownServicePrincipalUserAssignedIdentity"
var _AuthMethod_index = [...]uint8{0, 7, 23, 43}
func (i AuthMethod) String() string {
if i >= AuthMethod(len(_AuthMethod_index)-1) {
return "AuthMethod(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _AuthMethod_name[_AuthMethod_index[i]:_AuthMethod_index[i+1]]
}