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 }