github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/azure/credentials.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package azure 5 6 import ( 7 "context" 8 "fmt" 9 "io" 10 11 "github.com/Azure/azure-sdk-for-go/sdk/azcore" 12 "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" 13 "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" 14 "github.com/Azure/azure-sdk-for-go/sdk/azidentity" 15 "github.com/juju/errors" 16 17 "github.com/juju/juju/cloud" 18 "github.com/juju/juju/environs" 19 "github.com/juju/juju/provider/azure/internal/azureauth" 20 "github.com/juju/juju/provider/azure/internal/azurecli" 21 ) 22 23 const ( 24 credAttrAppId = "application-id" 25 credAttrApplicationObjectId = "application-object-id" 26 credAttrSubscriptionId = "subscription-id" 27 credAttrManagedSubscriptionId = "managed-subscription-id" 28 credAttrAppPassword = "application-password" 29 30 // clientCredentialsAuthType is the auth-type for the 31 // "client credentials" OAuth flow, which requires a 32 // service principal with a password. 33 clientCredentialsAuthType cloud.AuthType = "service-principal-secret" 34 35 // deviceCodeAuthType is the auth-type for the interactive 36 // "device code" OAuth flow. 37 deviceCodeAuthType cloud.AuthType = cloud.InteractiveAuthType 38 ) 39 40 type ServicePrincipalCreator interface { 41 InteractiveCreate(sdkCtx context.Context, stderr io.Writer, params azureauth.ServicePrincipalParams) (appid, spid, password string, _ error) 42 Create(sdkCtx context.Context, params azureauth.ServicePrincipalParams) (appid, spid, password string, _ error) 43 } 44 45 type AzureCLI interface { 46 ListAccounts() ([]azurecli.Account, error) 47 FindAccountsWithCloudName(name string) ([]azurecli.Account, error) 48 ShowAccount(subscription string) (*azurecli.Account, error) 49 ListClouds() ([]azurecli.Cloud, error) 50 } 51 52 // environPoviderCredentials is an implementation of 53 // environs.ProviderCredentials for the Azure Resource 54 // Manager cloud provider. 55 type environProviderCredentials struct { 56 servicePrincipalCreator ServicePrincipalCreator 57 azureCLI AzureCLI 58 transporter policy.Transporter 59 } 60 61 // CredentialSchemas is part of the environs.ProviderCredentials interface. 62 func (c environProviderCredentials) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema { 63 interactiveSchema := cloud.CredentialSchema{{ 64 credAttrSubscriptionId, cloud.CredentialAttr{Description: "Azure subscription ID"}, 65 }} 66 if _, err := c.azureCLI.ShowAccount(""); err == nil { 67 // If az account show returns successfully then we can 68 // use that to get at least some login details, otherwise 69 // we need the user to supply their subscription ID. 70 interactiveSchema[0].CredentialAttr.Optional = true 71 } 72 return map[cloud.AuthType]cloud.CredentialSchema{ 73 // deviceCodeAuthType is the interactive device-code oauth 74 // flow. This is only supported on the client side; it will 75 // be used to generate a service principal, and transformed 76 // into clientCredentialsAuthType. 77 deviceCodeAuthType: interactiveSchema, 78 79 // clientCredentialsAuthType is the "client credentials" 80 // oauth flow, which requires a service principal with a 81 // password. 82 clientCredentialsAuthType: { 83 { 84 credAttrAppId, cloud.CredentialAttr{Description: "Azure Active Directory application ID"}, 85 }, { 86 credAttrApplicationObjectId, cloud.CredentialAttr{ 87 Description: "Azure Active Directory application Object ID", 88 Optional: true, 89 }, 90 }, { 91 credAttrSubscriptionId, cloud.CredentialAttr{Description: "Azure subscription ID"}, 92 }, { 93 credAttrManagedSubscriptionId, cloud.CredentialAttr{ 94 Description: "Azure managed subscription ID", 95 Optional: true, 96 }, 97 }, { 98 credAttrAppPassword, cloud.CredentialAttr{ 99 Description: "Azure Active Directory application password", 100 Hidden: true, 101 }, 102 }, 103 }, 104 } 105 } 106 107 // DetectCredentials is part of the environs.ProviderCredentials 108 // interface. It attempts to detect subscription IDs from accounts 109 // configured in the Azure CLI. 110 func (c environProviderCredentials) DetectCredentials(cloudName string) (*cloud.CloudCredential, error) { 111 // Attempt to get accounts from az. 112 accounts, err := c.azureCLI.ListAccounts() 113 if err != nil { 114 logger.Debugf("error getting accounts from az: %s", err) 115 return nil, errors.NotFoundf("credentials") 116 } 117 if len(accounts) < 1 { 118 return nil, errors.NotFoundf("credentials") 119 } 120 clouds, err := c.azureCLI.ListClouds() 121 if err != nil { 122 logger.Debugf("error getting clouds from az: %s", err) 123 return nil, errors.NotFoundf("credentials") 124 } 125 cloudMap := make(map[string]azurecli.Cloud, len(clouds)) 126 for _, cloud := range clouds { 127 cloudMap[cloud.Name] = cloud 128 } 129 var defaultCredential string 130 authCredentials := make(map[string]cloud.Credential) 131 for _, acc := range accounts { 132 cloudInfo, ok := cloudMap[acc.CloudName] 133 if !ok { 134 continue 135 } 136 cred, err := c.accountCredential(acc) 137 if err != nil { 138 logger.Debugf("cannot get credential for %s: %s", acc.Name, err) 139 continue 140 } 141 cred.Label = fmt.Sprintf("%s subscription %s", cloudInfo.Name, acc.Name) 142 authCredentials[acc.Name] = cred 143 if acc.IsDefault { 144 defaultCredential = acc.Name 145 } 146 } 147 if len(authCredentials) < 1 { 148 return nil, errors.NotFoundf("credentials") 149 } 150 return &cloud.CloudCredential{ 151 DefaultCredential: defaultCredential, 152 AuthCredentials: authCredentials, 153 }, nil 154 } 155 156 // FinalizeCredential is part of the environs.ProviderCredentials interface. 157 func (c environProviderCredentials) FinalizeCredential( 158 ctx environs.FinalizeCredentialContext, 159 args environs.FinalizeCredentialParams, 160 ) (*cloud.Credential, error) { 161 switch authType := args.Credential.AuthType(); authType { 162 case deviceCodeAuthType: 163 subscriptionId := args.Credential.Attributes()[credAttrSubscriptionId] 164 165 var azCloudName string 166 switch args.CloudName { 167 case "azure": 168 azCloudName = azureauth.AzureCloud 169 case "azure-china": 170 azCloudName = azureauth.AzureChinaCloud 171 case "azure-gov": 172 azCloudName = azureauth.AzureUSGovernment 173 default: 174 return nil, errors.Errorf("unknown Azure cloud name %q", args.CloudName) 175 } 176 177 if subscriptionId != "" { 178 opts := azcore.ClientOptions{ 179 Cloud: azureCloud(args.CloudName, args.CloudEndpoint, args.CloudIdentityEndpoint), 180 Transport: c.transporter, 181 } 182 clientOpts := arm.ClientOptions{ClientOptions: opts} 183 sdkCtx := context.Background() 184 tenantID, err := azureauth.DiscoverTenantID(sdkCtx, subscriptionId, clientOpts) 185 if err != nil { 186 return nil, errors.Trace(err) 187 } 188 return c.deviceCodeCredential(ctx, args, azureauth.ServicePrincipalParams{ 189 CloudName: azCloudName, 190 SubscriptionId: subscriptionId, 191 TenantId: tenantID, 192 }) 193 } 194 195 params, err := c.getServicePrincipalParams(azCloudName) 196 if err != nil { 197 return nil, errors.Trace(err) 198 } 199 return c.azureCLICredential(ctx, args, params) 200 case clientCredentialsAuthType: 201 return &args.Credential, nil 202 default: 203 return nil, errors.NotSupportedf("%q auth-type", authType) 204 } 205 } 206 207 func (c environProviderCredentials) deviceCodeCredential( 208 ctx environs.FinalizeCredentialContext, 209 args environs.FinalizeCredentialParams, 210 params azureauth.ServicePrincipalParams, 211 ) (*cloud.Credential, error) { 212 sdkCtx := context.Background() 213 applicationId, spObjectId, password, err := c.servicePrincipalCreator.InteractiveCreate(sdkCtx, ctx.GetStderr(), params) 214 if err != nil { 215 return nil, errors.Trace(err) 216 } 217 out := cloud.NewCredential(clientCredentialsAuthType, map[string]string{ 218 credAttrSubscriptionId: params.SubscriptionId, 219 credAttrAppId: applicationId, 220 credAttrApplicationObjectId: spObjectId, 221 credAttrAppPassword: password, 222 }) 223 out.Label = args.Credential.Label 224 return &out, nil 225 } 226 227 func (c environProviderCredentials) azureCLICredential( 228 ctx environs.FinalizeCredentialContext, 229 args environs.FinalizeCredentialParams, 230 params azureauth.ServicePrincipalParams, 231 ) (*cloud.Credential, error) { 232 cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{ 233 TenantID: params.TenantId, 234 }) 235 if err != nil { 236 return nil, errors.Trace(err) 237 } 238 params.Credential = cred 239 240 if err != nil { 241 return nil, errors.Annotatef(err, "cannot get access token for %s", params.SubscriptionId) 242 } 243 244 sdkCtx := context.Background() 245 applicationId, spObjectId, password, err := c.servicePrincipalCreator.Create(sdkCtx, params) 246 if err != nil { 247 return nil, errors.Annotatef(err, "cannot create service principal") 248 } 249 out := cloud.NewCredential(clientCredentialsAuthType, map[string]string{ 250 credAttrSubscriptionId: params.SubscriptionId, 251 credAttrAppId: applicationId, 252 credAttrApplicationObjectId: spObjectId, 253 credAttrAppPassword: password, 254 }) 255 out.Label = args.Credential.Label 256 return &out, nil 257 } 258 259 func (c environProviderCredentials) accountCredential( 260 acc azurecli.Account, 261 ) (cloud.Credential, error) { 262 cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{ 263 TenantID: acc.AuthTenantId(), 264 AdditionallyAllowedTenants: []string{"*"}, 265 }) 266 if err != nil { 267 return cloud.Credential{}, errors.Annotate(err, "cannot get az cli credential") 268 } 269 sdkCtx := context.Background() 270 applicationId, spObjectId, password, err := c.servicePrincipalCreator.Create(sdkCtx, azureauth.ServicePrincipalParams{ 271 Credential: cred, 272 SubscriptionId: acc.ID, 273 TenantId: acc.TenantId, 274 }) 275 if err != nil { 276 return cloud.Credential{}, errors.Annotate(err, "cannot get service principal") 277 } 278 279 return cloud.NewCredential(clientCredentialsAuthType, map[string]string{ 280 credAttrSubscriptionId: acc.ID, 281 credAttrAppId: applicationId, 282 credAttrApplicationObjectId: spObjectId, 283 credAttrAppPassword: password, 284 }), nil 285 } 286 287 func (c environProviderCredentials) getServicePrincipalParams(cloudName string) (azureauth.ServicePrincipalParams, error) { 288 accounts, err := c.azureCLI.FindAccountsWithCloudName(cloudName) 289 if err != nil { 290 return azureauth.ServicePrincipalParams{}, errors.Annotatef(err, "cannot get accounts") 291 } 292 if len(accounts) < 1 { 293 return azureauth.ServicePrincipalParams{}, errors.Errorf("no %s accounts found", cloudName) 294 } 295 acc := accounts[0] 296 for _, a := range accounts[1:] { 297 if a.IsDefault { 298 acc = a 299 } 300 } 301 return azureauth.ServicePrincipalParams{ 302 CloudName: cloudName, 303 SubscriptionId: acc.ID, 304 TenantId: acc.AuthTenantId(), 305 }, nil 306 307 }