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

feat: azapi_client_config - add object id #657

Merged
merged 7 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/data-sources/client_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ output "tenant_id" {
### Read-Only

- `id` (String) The ID of this resource.
- `subscription_id` (String)
- `tenant_id` (String)
- `object_id` (String) The object ID of the identity. E.g. `00000000-0000-0000-0000-000000000000`
- `subscription_id` (String) The subscription ID. E.g. `00000000-0000-0000-0000-000000000000`
- `subscription_resource_id` (String) The resource ID of the subscription. E.g. `/subscriptions/00000000-0000-0000-0000-000000000000`
- `tenant_id` (String) The tenant ID. E.g. `00000000-0000-0000-0000-000000000000`

<a id="nestedblock--timeouts"></a>
### Nested Schema for `timeouts`
Expand Down
112 changes: 106 additions & 6 deletions internal/clients/account.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,47 @@
package clients

Check failure on line 1 in internal/clients/account.go

View workflow job for this annotation

GitHub Actions / tflint

package requires newer Go version go1.22

import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log"
"os/exec"
"strings"
"sync"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)

type ResourceManagerAccount struct {
tenantId *string
subscriptionId *string
objectId *string
mutex *sync.Mutex
client *Client
}

func NewResourceManagerAccount(tenantId, subscriptionId string) ResourceManagerAccount {
func NewResourceManagerAccount(client *Client) ResourceManagerAccount {
out := ResourceManagerAccount{
mutex: &sync.Mutex{},
}
if tenantId != "" {
out.tenantId = &tenantId
if client != nil && client.Account.tenantId != nil && *client.Account.tenantId != "" {
out.tenantId = client.Account.tenantId
}
if subscriptionId != "" {
out.subscriptionId = &subscriptionId
if client != nil && client.Account.subscriptionId != nil && *client.Account.subscriptionId != "" {
out.subscriptionId = client.Account.subscriptionId
}
// We lazy load object ID because it's not always needed and could cause a performance hit
out.client = client
return out
}

func (account *ResourceManagerAccount) GetTenantId() string {
account.mutex.Lock()
defer account.mutex.Unlock()

if account.tenantId != nil {
return *account.tenantId
}
Expand All @@ -52,6 +62,7 @@
func (account *ResourceManagerAccount) GetSubscriptionId() string {
account.mutex.Lock()
defer account.mutex.Unlock()

if account.subscriptionId != nil {
return *account.subscriptionId
}
Expand All @@ -62,12 +73,64 @@
}

if account.subscriptionId == nil {
log.Printf("[DEBUG] No default subscription ID found")
log.Printf("[DEBUG] No subscription ID found")
return ""
}

return *account.subscriptionId
}

func (account *ResourceManagerAccount) GetObjectId() string {
account.mutex.Lock()
defer account.mutex.Unlock()

if account.objectId != nil {
return *account.objectId
}

tok, err := account.client.Option.Cred.GetToken(account.client.StopContext, policy.TokenRequestOptions{
TenantID: account.client.Option.TenantId,
Scopes: []string{account.client.Option.CloudCfg.Services[cloud.ResourceManager].Endpoint + "/.default"}})
if err != nil {
log.Printf("[DEBUG] Error getting requesting token from credentials: %s", err)
}

if tok.Token == "" {
err = account.loadSignedInUserFromAzCmd()
if err != nil {
log.Printf("[DEBUG] Error getting user object ID from az cli: %s", err)
}
} else {
cl, err := parseTokenClaims(tok.Token)
if err != nil {
log.Printf("[DEBUG] Error getting object id from token: %s", err)
}
if cl != nil && cl.ObjectId != "" {
account.objectId = &cl.ObjectId
}
}

if account.objectId == nil {
log.Printf("[DEBUG] No object ID found")
return ""
}

return *account.objectId
}

func (account *ResourceManagerAccount) loadSignedInUserFromAzCmd() error {
var userModel struct {
ObjectId string `json:"id"`
}
err := jsonUnmarshalAzCmd(&userModel, "ad", "signed-in-user", "show")
if err != nil {
return fmt.Errorf("obtaining defaults from az cmd: %s", err)
}

account.objectId = &userModel.ObjectId
return nil
}

func (account *ResourceManagerAccount) loadDefaultsFromAzCmd() error {
var accountModel struct {
SubscriptionID string `json:"id"`
Expand Down Expand Up @@ -115,3 +178,40 @@

return nil
}

func parseTokenClaims(token string) (*tokenClaims, error) {
// Parse the token to get the claims
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, errors.New("parseTokenClaims: token does not have 3 parts")
}
decoded, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("parseTokenClaims: error decoding token: %s", err)
}
var claims tokenClaims
err = json.Unmarshal(decoded, &claims)
if err != nil {
return nil, fmt.Errorf("parseTokenClaims: error unmarshalling claims: %w", err)
}
return &claims, nil
}

type tokenClaims struct {
Audience string `json:"aud"`
Expires int64 `json:"exp"`
IssuedAt int64 `json:"iat"`
Issuer string `json:"iss"`
IdentityProvider string `json:"idp"`
ObjectId string `json:"oid"`
Roles []string `json:"roles"`
Scopes string `json:"scp"`
Subject string `json:"sub"`
TenantRegionScope string `json:"tenant_region_scope"`
TenantId string `json:"tid"`
Version string `json:"ver"`

AppDisplayName string `json:"app_displayname,omitempty"`
AppId string `json:"appid,omitempty"`
IdType string `json:"idtyp,omitempty"`
}
4 changes: 3 additions & 1 deletion internal/clients/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Client struct {
DataPlaneClient *DataPlaneClient

Account ResourceManagerAccount
Option *Option
}

type Option struct {
Expand All @@ -42,6 +43,7 @@ type Option struct {
func (client *Client) Build(ctx context.Context, o *Option) error {
client.StopContext = ctx
client.Features = o.Features
client.Option = o

azlog.SetListener(func(cls azlog.Event, msg string) {
log.Printf("[DEBUG] %s %s: %s\n", time.Now().Format(time.StampMicro), cls, msg)
Expand Down Expand Up @@ -131,7 +133,7 @@ func (client *Client) Build(ctx context.Context, o *Option) error {
}
client.DataPlaneClient = dataPlaneClient

client.Account = NewResourceManagerAccount(o.TenantId, o.SubscriptionId)
client.Account = NewResourceManagerAccount(client)

return nil
}
30 changes: 23 additions & 7 deletions internal/services/azapi_client_config_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import (
)

type ClientConfigDataSourceModel struct {
ID types.String `tfsdk:"id"`
TenantID types.String `tfsdk:"tenant_id"`
SubscriptionID types.String `tfsdk:"subscription_id"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
ID types.String `tfsdk:"id"`
TenantID types.String `tfsdk:"tenant_id"`
SubscriptionID types.String `tfsdk:"subscription_id"`
SubscriptionResourceID types.String `tfsdk:"subscription_resource_id"`
ObjectID types.String `tfsdk:"object_id"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
}

type ClientConfigDataSource struct {
Expand Down Expand Up @@ -44,11 +46,23 @@ func (r *ClientConfigDataSource) Schema(ctx context.Context, request datasource.
},

"tenant_id": schema.StringAttribute{
Computed: true,
Computed: true,
MarkdownDescription: "The tenant ID. E.g. `00000000-0000-0000-0000-000000000000`",
},

"subscription_id": schema.StringAttribute{
Computed: true,
Computed: true,
MarkdownDescription: "The subscription ID. E.g. `00000000-0000-0000-0000-000000000000`",
},

"subscription_resource_id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "The resource ID of the subscription. E.g. `/subscriptions/00000000-0000-0000-0000-000000000000`",
},

"object_id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "The object ID of the identity. E.g. `00000000-0000-0000-0000-000000000000`",
},
},

Expand All @@ -71,15 +85,17 @@ func (r *ClientConfigDataSource) Read(ctx context.Context, request datasource.Re
if response.Diagnostics.HasError() {
return
}

ctx, cancel := context.WithTimeout(ctx, readTimeout)
defer cancel()

subscriptionId := r.ProviderData.Account.GetSubscriptionId()
tenantId := r.ProviderData.Account.GetTenantId()
objectId := r.ProviderData.Account.GetObjectId()

model.ID = types.StringValue(fmt.Sprintf("clientConfigs/subscriptionId=%s;tenantId=%s", subscriptionId, tenantId))
model.SubscriptionID = types.StringValue(subscriptionId)
model.SubscriptionResourceID = types.StringValue(fmt.Sprintf("/subscriptions/%s", subscriptionId))
model.TenantID = types.StringValue(tenantId)
model.ObjectID = types.StringValue(objectId)
response.Diagnostics.Append(response.State.Set(ctx, &model)...)
}
6 changes: 4 additions & 2 deletions internal/services/azapi_client_config_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (

type ClientConfigDataSource struct{}

var idRegex *regexp.Regexp = regexp.MustCompile("^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$")

func TestAccClientConfigDataSource_basic(t *testing.T) {
data := acceptance.BuildTestData(t, "data.azapi_client_config", "test")
r := ClientConfigDataSource{}
Expand All @@ -25,6 +27,7 @@ func TestAccClientConfigDataSource_basic(t *testing.T) {
Check: resource.ComposeTestCheckFunc(
check.That(data.ResourceName).Key("tenant_id").HasValue(tenantId),
check.That(data.ResourceName).Key("subscription_id").HasValue(subscriptionId),
check.That(data.ResourceName).Key("object_id").MatchesRegex(idRegex),
),
},
})
Expand All @@ -38,14 +41,13 @@ func TestAccClientConfigDataSource_azcli(t *testing.T) {
data := acceptance.BuildTestData(t, "data.azapi_client_config", "test")
r := ClientConfigDataSource{}

idRegex := regexp.MustCompile("^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$")

data.DataSourceTest(t, []resource.TestStep{
{
Config: r.basic(),
Check: resource.ComposeTestCheckFunc(
check.That(data.ResourceName).Key("tenant_id").MatchesRegex(idRegex),
check.That(data.ResourceName).Key("subscription_id").MatchesRegex(idRegex),
check.That(data.ResourceName).Key("object_id").MatchesRegex(idRegex),
),
},
})
Expand Down
Loading