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