diff --git a/docs/index.md b/docs/index.md index 4c09752..d20ff6a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -64,6 +64,7 @@ provider "msgraph" { - `custom_correlation_request_id` (String) The value of the `x-ms-correlation-request-id` header, otherwise an auto-generated UUID will be used. This can also be sourced from the `ARM_CORRELATION_REQUEST_ID` environment variable. - `disable_correlation_request_id` (Boolean) This will disable the x-ms-correlation-request-id header. - `disable_terraform_partner_id` (Boolean) Disable sending the Terraform Partner ID if a custom `partner_id` isn't specified, which allows Microsoft to better understand the usage of Terraform. The Partner ID does not give HashiCorp any direct access to usage information. This can also be sourced from the `ARM_DISABLE_TERRAFORM_PARTNER_ID` environment variable. Defaults to `false`. +- `environment` (String) The cloud environment which should be used. Possible values are: `global` (also `public`), `usgovernmentl4` (also `usgovernment`), `usgovernmentl5` (also `dod`), and `china`. Defaults to `global`. This can also be sourced from the `ARM_ENVIRONMENT` environment variable. - `oidc_azure_service_connection_id` (String) The Azure Pipelines Service Connection ID to use for authentication. This can also be sourced from the `ARM_OIDC_AZURE_SERVICE_CONNECTION_ID` environment variable. - `oidc_request_token` (String) The bearer token for the request to the OIDC provider. This can also be sourced from the `ARM_OIDC_REQUEST_TOKEN` or `ACTIONS_ID_TOKEN_REQUEST_TOKEN` Environment Variables. - `oidc_request_url` (String) The URL for the OIDC provider from which to request an ID token. This can also be sourced from the `ARM_OIDC_REQUEST_URL` or `ACTIONS_ID_TOKEN_REQUEST_URL` Environment Variables. diff --git a/internal/acceptance/testclient.go b/internal/acceptance/testclient.go index 5d84251..8e9f88a 100644 --- a/internal/acceptance/testclient.go +++ b/internal/acceptance/testclient.go @@ -26,15 +26,23 @@ func BuildTestClient() (*clients.Client, error) { if _client == nil { var cloudConfig cloud.Configuration env := os.Getenv("ARM_ENVIRONMENT") + var environment string switch strings.ToLower(env) { - case "public": + case "public", "global", "": cloudConfig = cloud.AzurePublic - case "usgovernment": + environment = "global" + case "usgovernment", "usgovernmentl4": cloudConfig = cloud.AzureGovernment + environment = "usgovernmentl4" + case "usgovernmentl5", "dod": + cloudConfig = cloud.AzureGovernment + environment = "usgovernmentl5" case "china": cloudConfig = cloud.AzureChina + environment = "china" default: cloudConfig = cloud.AzurePublic + environment = "global" } model := provider.MSGraphProviderModel{} @@ -117,9 +125,10 @@ func BuildTestClient() (*clients.Client, error) { } copt := &clients.Option{ - Cred: cred, - CloudCfg: cloudConfig, - TenantId: os.Getenv("ARM_TENANT_ID"), + Cred: cred, + CloudCfg: cloudConfig, + TenantId: os.Getenv("ARM_TENANT_ID"), + Environment: environment, } client := &clients.Client{} diff --git a/internal/clients/client.go b/internal/clients/client.go index 8fb802f..5e87cea 100644 --- a/internal/clients/client.go +++ b/internal/clients/client.go @@ -27,6 +27,7 @@ type Option struct { CloudCfg cloud.Configuration CustomCorrelationRequestID string TenantId string + Environment string } func (client *Client) Build(ctx context.Context, o *Option) error { @@ -86,7 +87,7 @@ func (client *Client) Build(ctx context.Context, o *Option) error { "$format", } - msgraphClient, err := NewMSGraphClient(o.Cred, &policy.ClientOptions{ + msgraphClient, err := NewMSGraphClient(MSGraphEndpointForEnvironment(o.Environment), o.Cred, &policy.ClientOptions{ Logging: policy.LogOptions{ IncludeBody: false, AllowedHeaders: allowedHeaders, diff --git a/internal/clients/msgraph_client.go b/internal/clients/msgraph_client.go index 31f9d7e..0f5e348 100644 --- a/internal/clients/msgraph_client.go +++ b/internal/clients/msgraph_client.go @@ -16,24 +16,53 @@ const ( nextLinkKey = "@odata.nextLink" ) +const DefaultEnvironment = "global" + +// EnvironmentEndpoints maps environment names to their Microsoft Graph endpoint URLs. +var EnvironmentEndpoints = map[string]string{ + "global": "https://graph.microsoft.com", + "public": "https://graph.microsoft.com", + "usgovernmentl4": "https://graph.microsoft.us", + "usgovernment": "https://graph.microsoft.us", + "usgovernmentl5": "https://dod-graph.microsoft.us", + "dod": "https://dod-graph.microsoft.us", + "china": "https://microsoftgraph.chinacloudapi.cn", +} + +// MSGraphEndpointForEnvironment returns the Microsoft Graph endpoint URL for the given environment name. +func MSGraphEndpointForEnvironment(env string) string { + if endpoint, ok := EnvironmentEndpoints[env]; ok { + return endpoint + } + return EnvironmentEndpoints[DefaultEnvironment] +} + type MSGraphClient struct { host string pl runtime.Pipeline } -func NewMSGraphClient(credential azcore.TokenCredential, opt *policy.ClientOptions) (*MSGraphClient, error) { +// Host returns the Microsoft Graph endpoint base URL (e.g. "https://graph.microsoft.com"). +func (client *MSGraphClient) Host() string { + return client.host +} + +func NewMSGraphClient(endpoint string, credential azcore.TokenCredential, opt *policy.ClientOptions) (*MSGraphClient, error) { + if endpoint == "" { + endpoint = EnvironmentEndpoints[DefaultEnvironment] + } pl := runtime.NewPipeline(moduleName, moduleVersion, runtime.PipelineOptions{ AllowedHeaders: nil, AllowedQueryParameters: nil, APIVersion: runtime.APIVersionOptions{}, PerCall: nil, PerRetry: []policy.Policy{ - runtime.NewBearerTokenPolicy(credential, []string{"https://graph.microsoft.com/.default"}, nil), + runtime.NewBearerTokenPolicy(credential, []string{endpoint + "/.default"}, nil), }, Tracing: runtime.TracingOptions{}, }, opt) return &MSGraphClient{ - host: "https://graph.microsoft.com", + host: endpoint, pl: pl, }, nil } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 3dbf502..cfd20ac 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -26,6 +26,16 @@ import ( var _ provider.Provider = &MSGraphProvider{} +var validEnvironments = []string{ + "global", + "public", + "usgovernmentl4", + "usgovernment", + "usgovernmentl5", + "dod", + "china", +} + type MSGraphProvider struct{} type MSGraphProviderModel struct { @@ -47,6 +57,7 @@ type MSGraphProviderModel struct { UsePowerShell types.Bool `tfsdk:"use_powershell"` UseMSI types.Bool `tfsdk:"use_msi"` UseAKSWorkloadIdentity types.Bool `tfsdk:"use_aks_workload_identity"` + Environment types.String `tfsdk:"environment"` PartnerID types.String `tfsdk:"partner_id"` CustomCorrelationRequestID types.String `tfsdk:"custom_correlation_request_id"` DisableCorrelationRequestID types.Bool `tfsdk:"disable_correlation_request_id"` @@ -227,6 +238,15 @@ func (p *MSGraphProvider) Schema(ctx context.Context, req provider.SchemaRequest MarkdownDescription: "Should AKS Workload Identity be used for Authentication? This can also be sourced from the `ARM_USE_AKS_WORKLOAD_IDENTITY` Environment Variable. Defaults to `false`. When set, `client_id`, `tenant_id` and `oidc_token_file_path` will be detected from the environment and do not need to be specified.", }, + // Cloud environment + "environment": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf(validEnvironments...), + }, + MarkdownDescription: "The cloud environment which should be used. Possible values are: `global` (also `public`), `usgovernmentl4` (also `usgovernment`), `usgovernmentl5` (also `dod`), and `china`. Defaults to `global`. This can also be sourced from the `ARM_ENVIRONMENT` environment variable.", + }, + // Managed Tracking GUID for User-agent "partner_id": schema.StringAttribute{ Optional: true, @@ -418,6 +438,27 @@ func (p *MSGraphProvider) Configure(ctx context.Context, req provider.ConfigureR } } + if model.Environment.IsNull() { + if v := os.Getenv("ARM_ENVIRONMENT"); v != "" { + v = strings.ToLower(v) + valid := false + for _, env := range validEnvironments { + if v == env { + valid = true + break + } + } + if !valid { + resp.Diagnostics.AddError("Invalid `environment` value", + fmt.Sprintf("The value %q provided via ARM_ENVIRONMENT is not a valid environment. Valid values are: global (also public), usgovernmentl4 (also usgovernment), usgovernmentl5 (also dod), china", v)) + return + } + model.Environment = types.StringValue(v) + } else { + model.Environment = types.StringValue(clients.DefaultEnvironment) + } + } + option := azidentity.DefaultAzureCredentialOptions{ TenantID: model.TenantID.ValueString(), } @@ -435,6 +476,7 @@ func (p *MSGraphProvider) Configure(ctx context.Context, req provider.ConfigureR CustomCorrelationRequestID: model.CustomCorrelationRequestID.ValueString(), CloudCfg: cloud.Configuration{}, TenantId: model.TenantID.ValueString(), + Environment: model.Environment.ValueString(), } client := &clients.Client{} if err = client.Build(ctx, copt); err != nil { diff --git a/internal/services/msgraph_resource.go b/internal/services/msgraph_resource.go index e3fc5b7..2ab432f 100644 --- a/internal/services/msgraph_resource.go +++ b/internal/services/msgraph_resource.go @@ -435,7 +435,7 @@ func (r *MSGraphResource) Read(ctx context.Context, req resource.ReadRequest, re if v, _ := req.Private.GetKey(ctx, FlagMoveState); v != nil && string(v) == "true" { body := map[string]string{ - "@odata.id": fmt.Sprintf("https://graph.microsoft.com/v1.0/directoryObjects/%s", model.Id.ValueString()), + "@odata.id": fmt.Sprintf("%s/v1.0/directoryObjects/%s", r.client.Host(), model.Id.ValueString()), } data, err := json.Marshal(body) if err != nil {