Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Active Directory User Credentials #316

Merged
merged 9 commits into from
Sep 15, 2017
167 changes: 98 additions & 69 deletions azurerm/config.go

Large diffs are not rendered by default.

152 changes: 143 additions & 9 deletions azurerm/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import (
"log"
"strings"
"sync"
"time"

"github.com/Azure/azure-sdk-for-go/arm/resources/resources"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure/cli"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/helper/mutexkv"
"github.com/hashicorp/terraform/helper/schema"
Expand All @@ -23,25 +26,25 @@ func Provider() terraform.ResourceProvider {
Schema: map[string]*schema.Schema{
"subscription_id": {
Type: schema.TypeString,
Required: true,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARM_SUBSCRIPTION_ID", ""),
},

"client_id": {
Type: schema.TypeString,
Required: true,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_ID", ""),
},

"client_secret": {
Type: schema.TypeString,
Required: true,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_SECRET", ""),
},

"tenant_id": {
Type: schema.TypeString,
Required: true,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARM_TENANT_ID", ""),
},

Expand Down Expand Up @@ -150,17 +153,24 @@ func Provider() terraform.ResourceProvider {
type Config struct {
ManagementURL string

SubscriptionID string
// Core
ClientID string
ClientSecret string
SubscriptionID string
TenantID string
Environment string
SkipProviderRegistration bool

// Service Principal Auth
ClientSecret string

// Bearer Auth
AccessToken *adal.Token
IsCloudShell bool

validateCredentialsOnce sync.Once
}

func (c *Config) validate() error {
func (c *Config) validateServicePrincipal() error {
var err *multierror.Error

if c.SubscriptionID == "" {
Expand All @@ -182,6 +192,118 @@ func (c *Config) validate() error {
return err.ErrorOrNil()
}

func (c *Config) validateBearerAuth() error {
var err *multierror.Error

if c.AccessToken == nil {
err = multierror.Append(err, fmt.Errorf("Access Token was not found in your Azure CLI Credentials.\n\nPlease login to the Azure CLI again via `az login`"))
}

if c.ClientID == "" {
err = multierror.Append(err, fmt.Errorf("Client ID was not found in your Azure CLI Credentials.\n\nPlease login to the Azure CLI again via `az login`"))
}

if c.SubscriptionID == "" {
err = multierror.Append(err, fmt.Errorf("Subscription ID was not found in your Azure CLI Credentials.\n\nPlease login to the Azure CLI again via `az login`"))
}

if c.TenantID == "" {
err = multierror.Append(err, fmt.Errorf("Tenant ID was not found in your Azure CLI Credentials.\n\nPlease login to the Azure CLI again via `az login`"))
}

return err.ErrorOrNil()
}

func (c *Config) LoadTokensFromAzureCLI() error {
profilePath, err := cli.ProfilePath()
if err != nil {
return fmt.Errorf("Error loading the Profile Path from the Azure CLI: %+v", err)
}

profile, err := cli.LoadProfile(profilePath)
if err != nil {
return fmt.Errorf("Error loading Profile from the Azure CLI: %+v", err)
}

// pull out the TenantID and Subscription ID from the Azure Profile
for _, subscription := range profile.Subscriptions {
if subscription.IsDefault {
c.SubscriptionID = subscription.ID
c.TenantID = subscription.TenantID
c.Environment = normalizeEnvironmentName(subscription.EnvironmentName)
break
}
}

// TODO: validation if there's no TenantID

// pull out the ClientID and the AccessToken from the Azure Access Token
tokensPath, err := cli.AccessTokensPath()
if err != nil {
return fmt.Errorf("Error loading the Tokens Path from the Azure CLI: %+v", err)
}

tokens, err := cli.LoadTokens(tokensPath)
if err != nil {
return fmt.Errorf("Error loading Access Tokens from the Azure CLI: %+v", err)
}

foundToken := false
for _, accessToken := range tokens {
token, err := accessToken.ToADALToken()
if err != nil {
return fmt.Errorf("[DEBUG] Error converting access token to token: %+v", err)
}

expirationDate, err := cli.ParseExpirationDate(accessToken.ExpiresOn)
if err != nil {
return fmt.Errorf("Error parsing expiration date: %q", accessToken.ExpiresOn)
}

if expirationDate.UTC().Before(time.Now().UTC()) {
log.Printf("[DEBUG] Token '%s' has expired", token.AccessToken)
continue
}

// TODO: replace this with something that's not terrible
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this TODO happen in this PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope - I can't find a better way of doing this (as such I've removed it for the moment)

if !strings.Contains(accessToken.Resource, "management") {
log.Printf("[DEBUG] Resource '%s' isn't a management domain", accessToken.Resource)
continue
}

if !strings.HasSuffix(accessToken.Authority, c.TenantID) {
log.Printf("[DEBUG] Resource '%s' isn't for the correct Tenant", accessToken.Resource)
continue
}

// note: we don't make use of the CLI Refresh Token at this time, but we potentially could
c.ClientID = accessToken.ClientID
c.AccessToken = &token
c.IsCloudShell = accessToken.RefreshToken == ""
foundToken = true
break
}

if !foundToken {
return fmt.Errorf("No valid (unexpired) Azure CLI Auth Tokens found. Please run `az login`.")
}

return nil
}

func normalizeEnvironmentName(input string) string {
// Environment is stored as `Azure{Environment}Cloud`
output := strings.ToLower(input)
output = strings.TrimPrefix(output, "azure")
output = strings.TrimSuffix(output, "cloud")

// however Azure Public is `AzureCloud` in the CLI Profile and not `AzurePublicCloud`.
if output == "" {
return "public"
}
return output
}

func providerConfigure(p *schema.Provider) schema.ConfigureFunc {
return func(d *schema.ResourceData) (interface{}, error) {
config := &Config{
Expand All @@ -193,8 +315,20 @@ func providerConfigure(p *schema.Provider) schema.ConfigureFunc {
SkipProviderRegistration: d.Get("skip_provider_registration").(bool),
}

if err := config.validate(); err != nil {
return nil, err
if config.ClientSecret != "" {
log.Printf("[DEBUG] Client Secret specified - using Service Principal for Authentication")
if err := config.validateServicePrincipal(); err != nil {
return nil, err
}
} else {
log.Printf("[DEBUG] No Client Secret specified - loading credentials from Azure CLI")
if err := config.LoadTokensFromAzureCLI(); err != nil {
return nil, err
}

if err := config.validateBearerAuth(); err != nil {
return nil, fmt.Errorf("Please specify either a Service Principal, or log in with the Azure CLI (using `az login`)")
}
}

client, err := config.getArmClient()
Expand Down
6 changes: 6 additions & 0 deletions vendor/github.com/Azure/go-autorest/autorest/adal/msi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions vendor/github.com/Azure/go-autorest/autorest/adal/msi_windows.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 32 additions & 27 deletions vendor/github.com/Azure/go-autorest/autorest/adal/token.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.