sigs.k8s.io/cluster-api-provider-azure@v1.14.3/azure/scope/identity.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package scope
    18  
    19  import (
    20  	"context"
    21  	"reflect"
    22  	"strings"
    23  
    24  	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
    25  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
    26  	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
    27  	"github.com/Azure/go-autorest/autorest"
    28  	"github.com/jongio/azidext/go/azidext"
    29  	"github.com/pkg/errors"
    30  	corev1 "k8s.io/api/core/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/types"
    33  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    34  	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
    35  	"sigs.k8s.io/controller-runtime/pkg/client"
    36  )
    37  
    38  // AzureSecretKey is the value for they client secret key.
    39  const AzureSecretKey = "clientSecret"
    40  
    41  // CredentialsProvider defines the behavior for azure identity based credential providers.
    42  type CredentialsProvider interface {
    43  	GetAuthorizer(ctx context.Context, tokenCredential azcore.TokenCredential, tokenAudience string) (autorest.Authorizer, error)
    44  	GetClientID() string
    45  	GetClientSecret(ctx context.Context) (string, error)
    46  	GetTenantID() string
    47  	GetTokenCredential(ctx context.Context, resourceManagerEndpoint, activeDirectoryEndpoint, tokenAudience string) (azcore.TokenCredential, error)
    48  }
    49  
    50  // AzureCredentialsProvider represents a credential provider with azure cluster identity.
    51  type AzureCredentialsProvider struct {
    52  	Client   client.Client
    53  	Identity *infrav1.AzureClusterIdentity
    54  }
    55  
    56  // AzureClusterCredentialsProvider wraps AzureCredentialsProvider with AzureCluster.
    57  type AzureClusterCredentialsProvider struct {
    58  	AzureCredentialsProvider
    59  	AzureCluster *infrav1.AzureCluster
    60  }
    61  
    62  // ManagedControlPlaneCredentialsProvider wraps AzureCredentialsProvider with AzureManagedControlPlane.
    63  type ManagedControlPlaneCredentialsProvider struct {
    64  	AzureCredentialsProvider
    65  	AzureManagedControlPlane *infrav1.AzureManagedControlPlane
    66  }
    67  
    68  var _ CredentialsProvider = (*AzureClusterCredentialsProvider)(nil)
    69  var _ CredentialsProvider = (*ManagedControlPlaneCredentialsProvider)(nil)
    70  
    71  // NewAzureClusterCredentialsProvider creates a new AzureClusterCredentialsProvider from the supplied inputs.
    72  func NewAzureClusterCredentialsProvider(ctx context.Context, kubeClient client.Client, azureCluster *infrav1.AzureCluster) (*AzureClusterCredentialsProvider, error) {
    73  	if azureCluster.Spec.IdentityRef == nil {
    74  		return nil, errors.New("failed to generate new AzureClusterCredentialsProvider from empty identityName")
    75  	}
    76  
    77  	ref := azureCluster.Spec.IdentityRef
    78  	// if the namespace isn't specified then assume it's in the same namespace as the AzureCluster
    79  	namespace := ref.Namespace
    80  	if namespace == "" {
    81  		namespace = azureCluster.Namespace
    82  	}
    83  	identity := &infrav1.AzureClusterIdentity{}
    84  	key := client.ObjectKey{Name: ref.Name, Namespace: namespace}
    85  	if err := kubeClient.Get(ctx, key, identity); err != nil {
    86  		return nil, errors.Errorf("failed to retrieve AzureClusterIdentity external object %q/%q: %v", key.Namespace, key.Name, err)
    87  	}
    88  
    89  	return &AzureClusterCredentialsProvider{
    90  		AzureCredentialsProvider{
    91  			Client:   kubeClient,
    92  			Identity: identity,
    93  		},
    94  		azureCluster,
    95  	}, nil
    96  }
    97  
    98  // GetAuthorizer returns an Azure authorizer based on the provided azure identity. It delegates to AzureCredentialsProvider with AzureCluster metadata.
    99  func (p *AzureClusterCredentialsProvider) GetAuthorizer(ctx context.Context, tokenCredential azcore.TokenCredential, tokenAudience string) (autorest.Authorizer, error) {
   100  	return p.AzureCredentialsProvider.GetAuthorizer(ctx, tokenCredential, tokenAudience)
   101  }
   102  
   103  // GetTokenCredential returns an Azure TokenCredential based on the provided azure identity.
   104  func (p *AzureClusterCredentialsProvider) GetTokenCredential(ctx context.Context, resourceManagerEndpoint, activeDirectoryEndpoint, tokenAudience string) (azcore.TokenCredential, error) {
   105  	return p.AzureCredentialsProvider.GetTokenCredential(ctx, resourceManagerEndpoint, activeDirectoryEndpoint, tokenAudience, p.AzureCluster.ObjectMeta)
   106  }
   107  
   108  // NewManagedControlPlaneCredentialsProvider creates a new ManagedControlPlaneCredentialsProvider from the supplied inputs.
   109  func NewManagedControlPlaneCredentialsProvider(ctx context.Context, kubeClient client.Client, managedControlPlane *infrav1.AzureManagedControlPlane) (*ManagedControlPlaneCredentialsProvider, error) {
   110  	if managedControlPlane.Spec.IdentityRef == nil {
   111  		return nil, errors.New("failed to generate new ManagedControlPlaneCredentialsProvider from empty identityName")
   112  	}
   113  
   114  	ref := managedControlPlane.Spec.IdentityRef
   115  	// if the namespace isn't specified then assume it's in the same namespace as the AzureManagedControlPlane
   116  	namespace := ref.Namespace
   117  	if namespace == "" {
   118  		namespace = managedControlPlane.Namespace
   119  	}
   120  	identity := &infrav1.AzureClusterIdentity{}
   121  	key := client.ObjectKey{Name: ref.Name, Namespace: namespace}
   122  	if err := kubeClient.Get(ctx, key, identity); err != nil {
   123  		return nil, errors.Errorf("failed to retrieve AzureClusterIdentity external object %q/%q: %v", key.Namespace, key.Name, err)
   124  	}
   125  
   126  	return &ManagedControlPlaneCredentialsProvider{
   127  		AzureCredentialsProvider{
   128  			Client:   kubeClient,
   129  			Identity: identity,
   130  		},
   131  		managedControlPlane,
   132  	}, nil
   133  }
   134  
   135  // GetAuthorizer returns an Azure authorizer based on the provided azure identity. It delegates to AzureCredentialsProvider with AzureManagedControlPlane metadata.
   136  func (p *ManagedControlPlaneCredentialsProvider) GetAuthorizer(ctx context.Context, tokenCredential azcore.TokenCredential, tokenAudience string) (autorest.Authorizer, error) {
   137  	return p.AzureCredentialsProvider.GetAuthorizer(ctx, tokenCredential, tokenAudience)
   138  }
   139  
   140  // GetTokenCredential returns an Azure TokenCredential based on the provided azure identity.
   141  func (p *ManagedControlPlaneCredentialsProvider) GetTokenCredential(ctx context.Context, resourceManagerEndpoint, activeDirectoryEndpoint, tokenAudience string) (azcore.TokenCredential, error) {
   142  	return p.AzureCredentialsProvider.GetTokenCredential(ctx, resourceManagerEndpoint, activeDirectoryEndpoint, tokenAudience, p.AzureManagedControlPlane.ObjectMeta)
   143  }
   144  
   145  // GetTokenCredential returns an Azure TokenCredential based on the provided azure identity.
   146  func (p *AzureCredentialsProvider) GetTokenCredential(ctx context.Context, resourceManagerEndpoint, activeDirectoryEndpoint, tokenAudience string, clusterMeta metav1.ObjectMeta) (azcore.TokenCredential, error) {
   147  	ctx, log, done := tele.StartSpanWithLogger(ctx, "azure.scope.AzureCredentialsProvider.GetTokenCredential")
   148  	defer done()
   149  
   150  	var authErr error
   151  	var cred azcore.TokenCredential
   152  
   153  	switch p.Identity.Spec.Type {
   154  	case infrav1.WorkloadIdentity:
   155  		azwiCredOptions, err := NewWorkloadIdentityCredentialOptions().
   156  			WithTenantID(p.Identity.Spec.TenantID).
   157  			WithClientID(p.Identity.Spec.ClientID).
   158  			WithDefaults()
   159  		if err != nil {
   160  			return nil, errors.Wrapf(err, "failed to setup azwi options for identity %s", p.Identity.Name)
   161  		}
   162  		cred, authErr = NewWorkloadIdentityCredential(azwiCredOptions)
   163  
   164  	case infrav1.ManualServicePrincipal:
   165  		log.Info("Identity type ManualServicePrincipal is deprecated and will be removed in a future release. See https://capz.sigs.k8s.io/topics/identities to find a supported identity type.")
   166  		fallthrough
   167  	case infrav1.ServicePrincipal:
   168  		clientSecret, err := p.GetClientSecret(ctx)
   169  		if err != nil {
   170  			return nil, errors.Wrap(err, "failed to get client secret")
   171  		}
   172  		options := azidentity.ClientSecretCredentialOptions{
   173  			ClientOptions: azcore.ClientOptions{
   174  				Cloud: cloud.Configuration{
   175  					ActiveDirectoryAuthorityHost: activeDirectoryEndpoint,
   176  					Services: map[cloud.ServiceName]cloud.ServiceConfiguration{
   177  						cloud.ResourceManager: {
   178  							Audience: tokenAudience,
   179  							Endpoint: resourceManagerEndpoint,
   180  						},
   181  					},
   182  				},
   183  			},
   184  		}
   185  		cred, authErr = azidentity.NewClientSecretCredential(p.GetTenantID(), p.Identity.Spec.ClientID, clientSecret, &options)
   186  
   187  	case infrav1.ServicePrincipalCertificate:
   188  		clientSecret, err := p.GetClientSecret(ctx)
   189  		if err != nil {
   190  			return nil, errors.Wrap(err, "failed to get client secret")
   191  		}
   192  		certs, key, err := azidentity.ParseCertificates([]byte(clientSecret), nil)
   193  		if err != nil {
   194  			return nil, errors.Wrap(err, "failed to parse certificate data")
   195  		}
   196  		cred, authErr = azidentity.NewClientCertificateCredential(p.GetTenantID(), p.Identity.Spec.ClientID, certs, key, nil)
   197  
   198  	case infrav1.UserAssignedMSI:
   199  		options := azidentity.ManagedIdentityCredentialOptions{
   200  			ID: azidentity.ClientID(p.Identity.Spec.ClientID),
   201  		}
   202  		cred, authErr = azidentity.NewManagedIdentityCredential(&options)
   203  
   204  	default:
   205  		return nil, errors.Errorf("identity type %s not supported", p.Identity.Spec.Type)
   206  	}
   207  
   208  	if authErr != nil {
   209  		return nil, errors.Errorf("failed to create credential: %v", authErr)
   210  	}
   211  
   212  	return cred, nil
   213  }
   214  
   215  // GetAuthorizer returns an Azure authorizer based on the provided azure identity, cluster metadata, and tokenCredential.
   216  func (p *AzureCredentialsProvider) GetAuthorizer(ctx context.Context, cred azcore.TokenCredential, tokenAudience string) (autorest.Authorizer, error) {
   217  	// We must use TokenAudience for StackCloud, otherwise we get an
   218  	// AADSTS500011 error from the API
   219  	scope := tokenAudience
   220  	if !strings.HasSuffix(scope, "/.default") {
   221  		scope += "/.default"
   222  	}
   223  	authorizer := azidext.NewTokenCredentialAdapter(cred, []string{scope})
   224  	return authorizer, nil
   225  }
   226  
   227  // GetClientID returns the Client ID associated with the AzureCredentialsProvider's Identity.
   228  func (p *AzureCredentialsProvider) GetClientID() string {
   229  	return p.Identity.Spec.ClientID
   230  }
   231  
   232  // GetClientSecret returns the Client Secret associated with the AzureCredentialsProvider's Identity.
   233  // NOTE: this only works if the Identity references a Service Principal Client Secret.
   234  // If using another type of credentials, such a Certificate, we return an empty string.
   235  func (p *AzureCredentialsProvider) GetClientSecret(ctx context.Context) (string, error) {
   236  	if p.hasClientSecret() {
   237  		secretRef := p.Identity.Spec.ClientSecret
   238  		key := types.NamespacedName{
   239  			Namespace: secretRef.Namespace,
   240  			Name:      secretRef.Name,
   241  		}
   242  		secret := &corev1.Secret{}
   243  
   244  		if err := p.Client.Get(ctx, key, secret); err != nil {
   245  			return "", errors.Wrap(err, "Unable to fetch ClientSecret")
   246  		}
   247  		return string(secret.Data[AzureSecretKey]), nil
   248  	}
   249  	return "", nil
   250  }
   251  
   252  // GetTenantID returns the Tenant ID associated with the AzureCredentialsProvider's Identity.
   253  func (p *AzureCredentialsProvider) GetTenantID() string {
   254  	return p.Identity.Spec.TenantID
   255  }
   256  
   257  // hasClientSecret returns true if the identity has a Service Principal Client Secret.
   258  // This does not include managed identities.
   259  func (p *AzureCredentialsProvider) hasClientSecret() bool {
   260  	switch p.Identity.Spec.Type {
   261  	case infrav1.ServicePrincipal, infrav1.ManualServicePrincipal, infrav1.ServicePrincipalCertificate:
   262  		return true
   263  	default:
   264  		return false
   265  	}
   266  }
   267  
   268  // IsClusterNamespaceAllowed indicates if the cluster namespace is allowed.
   269  func IsClusterNamespaceAllowed(ctx context.Context, k8sClient client.Client, allowedNamespaces *infrav1.AllowedNamespaces, namespace string) bool {
   270  	if allowedNamespaces == nil {
   271  		return false
   272  	}
   273  
   274  	// empty value matches with all namespaces
   275  	if reflect.DeepEqual(*allowedNamespaces, infrav1.AllowedNamespaces{}) {
   276  		return true
   277  	}
   278  
   279  	for _, v := range allowedNamespaces.NamespaceList {
   280  		if v == namespace {
   281  			return true
   282  		}
   283  	}
   284  
   285  	// Check if clusterNamespace is in the namespaces selected by the identity's allowedNamespaces selector.
   286  	namespaces := &corev1.NamespaceList{}
   287  	selector, err := metav1.LabelSelectorAsSelector(allowedNamespaces.Selector)
   288  	if err != nil {
   289  		return false
   290  	}
   291  
   292  	// If a Selector has a nil or empty selector, it should match nothing.
   293  	if selector.Empty() {
   294  		return false
   295  	}
   296  
   297  	if err := k8sClient.List(ctx, namespaces, client.MatchingLabelsSelector{Selector: selector}); err != nil {
   298  		return false
   299  	}
   300  
   301  	for _, n := range namespaces.Items {
   302  		if n.Name == namespace {
   303  			return true
   304  		}
   305  	}
   306  
   307  	return false
   308  }