github.com/Azure/aad-pod-identity@v1.8.17/pkg/nmi/standard.go (about)

     1  package nmi
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/Azure/go-autorest/autorest/adal"
    10  	"k8s.io/klog/v2"
    11  
    12  	aadpodid "github.com/Azure/aad-pod-identity/pkg/apis/aadpodidentity"
    13  	"github.com/Azure/aad-pod-identity/pkg/auth"
    14  	"github.com/Azure/aad-pod-identity/pkg/k8s"
    15  	"github.com/Azure/aad-pod-identity/pkg/utils"
    16  )
    17  
    18  // StandardClient implements the TokenClient interface
    19  type StandardClient struct {
    20  	TokenClient
    21  	KubeClient                         k8s.Client
    22  	ListPodIDsRetryAttemptsForCreated  int
    23  	ListPodIDsRetryAttemptsForAssigned int
    24  	ListPodIDsRetryIntervalInSeconds   int
    25  	IsNamespaced                       bool
    26  }
    27  
    28  // NewStandardTokenClient creates new standard nmi client
    29  func NewStandardTokenClient(client k8s.Client, config Config) (*StandardClient, error) {
    30  	return &StandardClient{
    31  		KubeClient:                         client,
    32  		ListPodIDsRetryAttemptsForCreated:  config.RetryAttemptsForCreated,
    33  		ListPodIDsRetryAttemptsForAssigned: config.RetryAttemptsForAssigned,
    34  		ListPodIDsRetryIntervalInSeconds:   config.FindIdentityRetryIntervalInSeconds,
    35  		IsNamespaced:                       config.Namespaced,
    36  	}, nil
    37  }
    38  
    39  // GetIdentities gets the azure identity that matches the podns/podname and client id
    40  func (sc *StandardClient) GetIdentities(ctx context.Context, podns, podname, clientID, resourceID string) (*aadpodid.AzureIdentity, error) {
    41  	podIDs, identityInCreatedStateFound, err := sc.listPodIDsWithRetry(ctx, podns, podname, clientID, resourceID)
    42  	if err != nil {
    43  		// if identity not found in created state return nil identity which is then used to send 403 error
    44  		if !identityInCreatedStateFound {
    45  			return nil, err
    46  		}
    47  		// identity found in created state but there was an error, then return empty struct which will result in 404 error
    48  		return &aadpodid.AzureIdentity{}, err
    49  	}
    50  
    51  	// filter out if we are in namespaced mode
    52  	var filterPodIdentities []aadpodid.AzureIdentity
    53  	for _, val := range podIDs {
    54  		val := val // avoid implicit memory aliasing in for loop
    55  		if sc.IsNamespaced || aadpodid.IsNamespacedIdentity(&val) {
    56  			// namespaced mode
    57  			if val.Namespace == podns {
    58  				// matched namespace
    59  				filterPodIdentities = append(filterPodIdentities, val)
    60  			} else {
    61  				// unmatched namespaced
    62  				klog.Errorf("pod:%s/%s has identity %s/%s but identity is namespaced will be ignored", podns, podname, val.Name, val.Namespace)
    63  			}
    64  		} else {
    65  			// not in namespaced mode
    66  			filterPodIdentities = append(filterPodIdentities, val)
    67  		}
    68  	}
    69  
    70  	// If the client did not request a specific identity, then return the first identity
    71  	if len(clientID) == 0 && len(resourceID) == 0 {
    72  		id := filterPodIdentities[0]
    73  		klog.Infof("no clientID or resourceID in request. %s/%s has been matched with azure identity %s/%s", podns, podname, id.Namespace, id.Name)
    74  		return &id, nil
    75  	}
    76  
    77  	for _, id := range filterPodIdentities {
    78  		// if client id exists in the request, then send the first identity that matched the client id
    79  		if len(clientID) != 0 && id.Spec.ClientID == clientID {
    80  			klog.Infof("clientID in request: %s, %s/%s has been matched with azure identity %s/%s", utils.RedactClientID(clientID), podns, podname, id.Namespace, id.Name)
    81  			return &id, nil
    82  		}
    83  
    84  		// if resource id exists in the request, then send the first identity that matched the resource id
    85  		if len(resourceID) != 0 && id.Spec.ResourceID == resourceID {
    86  			return &id, nil
    87  		}
    88  	}
    89  
    90  	return nil, fmt.Errorf("no azure identity found for request clientID %s", utils.RedactClientID(clientID))
    91  }
    92  
    93  // listPodIDsWithRetry returns a list of matched identities in Assigned state, boolean indicating if at least an identity was found in Created state and error if any
    94  func (sc *StandardClient) listPodIDsWithRetry(ctx context.Context, podns, podname, rqClientID, rqResourceID string) ([]aadpodid.AzureIdentity, bool, error) {
    95  	attempt := 0
    96  	var err error
    97  	var idStateMap map[string][]aadpodid.AzureIdentity
    98  
    99  	identityUnspecified := len(rqClientID) == 0 && len(rqResourceID) == 0
   100  	isRequestedIdentity := func(podID aadpodid.AzureIdentity) bool {
   101  		return len(rqClientID) != 0 && strings.EqualFold(rqClientID, podID.Spec.ClientID) ||
   102  			len(rqResourceID) != 0 && strings.EqualFold(rqResourceID, podID.Spec.ResourceID)
   103  	}
   104  
   105  	// this loop will run to ensure we have assigned identities before we return. If there are no assigned identities in created state within 80s (16 retries * 5s wait) then we return an error.
   106  	// If we get an assigned identity in created state within 80s, then loop will continue until 100s to find assigned identity in assigned state.
   107  	// Retry interval for CREATED state is set to 80s because avg time for identity to be assigned to the node is 35-37s.
   108  	for attempt < sc.ListPodIDsRetryAttemptsForCreated+sc.ListPodIDsRetryAttemptsForAssigned {
   109  		idStateMap, err = sc.KubeClient.ListPodIds(podns, podname)
   110  		if err == nil {
   111  			if identityUnspecified {
   112  				// check to ensure backward compatibility with assignedIDs that have no state
   113  				// assigned identites created with old version of mic will not contain a state. So first we check to see if an assigned identity with
   114  				// no state exists that matches req client id.
   115  				if len(idStateMap[""]) != 0 {
   116  					klog.Warningf("found assignedIDs with no state for pod:%s/%s. AssignedIDs created with old version of mic.", podns, podname)
   117  					return idStateMap[""], true, nil
   118  				}
   119  				if len(idStateMap[aadpodid.AssignedIDAssigned]) != 0 {
   120  					return idStateMap[aadpodid.AssignedIDAssigned], true, nil
   121  				}
   122  				if len(idStateMap[aadpodid.AssignedIDCreated]) == 0 && attempt >= sc.ListPodIDsRetryAttemptsForCreated {
   123  					return nil, false, fmt.Errorf("getting assigned identities for pod %s/%s in CREATED state failed after %d attempts, retry duration [%d]s, error: %+v. Check MIC pod logs for identity assignment errors",
   124  						podns, podname, sc.ListPodIDsRetryAttemptsForCreated, sc.ListPodIDsRetryIntervalInSeconds, err)
   125  				}
   126  			} else {
   127  				// if the identity was specified, we need to ensure the identity with this client
   128  				// exists and is in Assigned state
   129  				// check to ensure backward compatibility with assignedIDs that have no state
   130  				for _, podID := range idStateMap[""] {
   131  					if isRequestedIdentity(podID) {
   132  						klog.Warningf("found assignedIDs with no state for pod:%s/%s. AssignedIDs created with old version of mic.", podns, podname)
   133  						return idStateMap[""], true, nil
   134  					}
   135  				}
   136  				for _, podID := range idStateMap[aadpodid.AssignedIDAssigned] {
   137  					if isRequestedIdentity(podID) {
   138  						return idStateMap[aadpodid.AssignedIDAssigned], true, nil
   139  					}
   140  				}
   141  				var foundMatch bool
   142  				for _, podID := range idStateMap[aadpodid.AssignedIDCreated] {
   143  					if isRequestedIdentity(podID) {
   144  						foundMatch = true
   145  						break
   146  					}
   147  				}
   148  				if !foundMatch && attempt >= sc.ListPodIDsRetryAttemptsForCreated {
   149  					return nil, false, fmt.Errorf("clientID in request: %s, getting assigned identities for pod %s/%s in CREATED state failed after %d attempts, retry duration [%d]s, error: %+v. Check MIC pod logs for identity assignment errors",
   150  						utils.RedactClientID(rqClientID), podns, podname, sc.ListPodIDsRetryAttemptsForCreated, sc.ListPodIDsRetryIntervalInSeconds, err)
   151  				}
   152  			}
   153  		}
   154  		attempt++
   155  
   156  		select {
   157  		case <-time.After(time.Duration(sc.ListPodIDsRetryIntervalInSeconds) * time.Second):
   158  		case <-ctx.Done():
   159  			err = ctx.Err()
   160  			return nil, true, err
   161  		}
   162  		klog.V(4).Infof("failed to get assigned ids for pod:%s/%s in ASSIGNED state, retrying attempt: %d", podns, podname, attempt)
   163  	}
   164  	return nil, true, fmt.Errorf("getting assigned identities for pod %s/%s in ASSIGNED state failed after %d attempts, retry duration [%d]s, error: %+v. Check MIC pod logs for identity assignment errors",
   165  		podns, podname, sc.ListPodIDsRetryAttemptsForCreated+sc.ListPodIDsRetryAttemptsForAssigned, sc.ListPodIDsRetryIntervalInSeconds, err)
   166  }
   167  
   168  // GetTokens returns ADAL tokens based on the request and its pod identity.
   169  func (sc *StandardClient) GetTokens(ctx context.Context, rqClientID, rqResource string, azureID aadpodid.AzureIdentity) ([]*adal.Token, error) {
   170  	rqHasClientID := len(rqClientID) != 0
   171  	clientID := azureID.Spec.ClientID
   172  
   173  	idType := azureID.Spec.Type
   174  	switch idType {
   175  	case aadpodid.UserAssignedMSI:
   176  		if rqHasClientID && !strings.EqualFold(rqClientID, clientID) {
   177  			klog.Warningf("clientid mismatch, requested:%s available:%s", rqClientID, clientID)
   178  		}
   179  		klog.Infof("matched identityType:%v clientid:%s resource:%s", idType, utils.RedactClientID(clientID), rqResource)
   180  		token, err := auth.GetServicePrincipalTokenFromMSIWithUserAssignedID(clientID, rqResource)
   181  		return []*adal.Token{token}, err
   182  	case aadpodid.ServicePrincipal:
   183  		tenantID := azureID.Spec.TenantID
   184  		auxiliaryTenantIDs := azureID.Spec.AuxiliaryTenantIDs
   185  		adEndpoint := azureID.Spec.ADEndpoint
   186  		secretRef := &azureID.Spec.ClientPassword
   187  		klog.Infof("matched identityType:%v adendpoint:%s tenantid:%s auxiliaryTenantIDs:%v clientid:%s resource:%s",
   188  			idType, adEndpoint, tenantID, auxiliaryTenantIDs, utils.RedactClientID(clientID), rqResource)
   189  		secret, err := sc.KubeClient.GetSecret(secretRef)
   190  		if err != nil {
   191  			return nil, fmt.Errorf("failed to get Kubernetes secret %s/%s, err: %v", secretRef.Namespace, secretRef.Name, err)
   192  		}
   193  		clientSecret := ""
   194  		for _, v := range secret.Data {
   195  			clientSecret = string(v)
   196  			break
   197  		}
   198  		tokens, err := auth.GetServicePrincipalToken(adEndpoint, tenantID, clientID, clientSecret, rqResource, auxiliaryTenantIDs)
   199  		return tokens, err
   200  	case aadpodid.ServicePrincipalCertificate:
   201  		tenantID := azureID.Spec.TenantID
   202  		adEndpoint := azureID.Spec.ADEndpoint
   203  		secretRef := &azureID.Spec.ClientPassword
   204  		klog.Infof("matched identityType:%v adendpoint:%s tenantid:%s clientid:%s resource:%s",
   205  			idType, adEndpoint, tenantID, utils.RedactClientID(clientID), rqResource)
   206  		secret, err := sc.KubeClient.GetSecret(secretRef)
   207  		if err != nil {
   208  			return nil, fmt.Errorf("failed to get Kubernetes secret %s/%s, err: %v", secretRef.Namespace, secretRef.Name, err)
   209  		}
   210  		certificate, password := secret.Data["certificate"], secret.Data["password"]
   211  		token, err := auth.GetServicePrincipalTokenWithCertificate(adEndpoint, tenantID, clientID,
   212  			certificate, string(password), rqResource)
   213  		return []*adal.Token{token}, err
   214  	default:
   215  		return nil, fmt.Errorf("unsupported identity type %+v", idType)
   216  	}
   217  }