-
Notifications
You must be signed in to change notification settings - Fork 15
Add powerplatform_environment_application_admin resource #786
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
Closed
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
3 changes: 3 additions & 0 deletions
3
examples/resources/powerplatform_environment_application_admin/import.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Import an existing application admin using the environment ID and application ID | ||
# Format: {environment_id}/{application_id} | ||
terraform import powerplatform_environment_application_admin.example 00000000-0000-0000-0000-000000000000/00000000-0000-0000-0000-000000000000 |
6 changes: 6 additions & 0 deletions
6
examples/resources/powerplatform_environment_application_admin/resource.tf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Ensure a service principal exists as an application user with System Administrator role | ||
# in an imported environment | ||
resource "powerplatform_environment_application_admin" "import_fix" { | ||
environment_id = "00000000-0000-0000-0000-000000000000" # GUID of environment | ||
application_id = "00000000-0000-0000-0000-000000000000" # GUID (client ID) of the service principal | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
internal/services/environment_application_admin/api_environment_application_admin.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
package environment_application_admin | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
|
||
"github.com/microsoft/terraform-provider-power-platform/internal/api" | ||
"github.com/microsoft/terraform-provider-power-platform/internal/constants" | ||
"github.com/microsoft/terraform-provider-power-platform/internal/customerrors" | ||
) | ||
|
||
func newEnvironmentApplicationAdminClient(clientApi *api.Client) client { | ||
return client{ | ||
Api: clientApi, | ||
} | ||
} | ||
|
||
type client struct { | ||
Api *api.Client | ||
} | ||
|
||
func (client *client) AddApplicationUser(ctx context.Context, environmentId, applicationId string) error { | ||
apiUrl := &url.URL{ | ||
Scheme: constants.HTTPS, | ||
Host: client.Api.GetConfig().Urls.BapiUrl, | ||
Path: "/providers/Microsoft.BusinessAppPlatform/scopes/admin/enroll", | ||
RawQuery: url.Values{ | ||
"api-version": []string{"2020-10-01"}, | ||
"environmentId": []string{environmentId}, | ||
"appId": []string{applicationId}, | ||
}.Encode(), | ||
} | ||
|
||
_, err := client.Api.Execute(ctx, nil, "POST", apiUrl.String(), nil, nil, []int{http.StatusOK}, nil) | ||
return err | ||
} | ||
|
||
func (client *client) GetApplicationUser(ctx context.Context, environmentId, applicationId string) (bool, error) { | ||
// Build the Dataverse API endpoint URL | ||
// Use the PowerApps API to query the Dataverse environment | ||
dataverseApiUrl := &url.URL{ | ||
Scheme: constants.HTTPS, | ||
Host: fmt.Sprintf("%s.api.%s", environmentId, client.Api.GetConfig().Urls.PowerAppsUrl), | ||
Path: fmt.Sprintf("/api/data/%s/applicationusers", constants.DATAVERSE_API_VERSION), | ||
RawQuery: url.Values{ | ||
"$filter": []string{fmt.Sprintf("applicationid eq '%s'", applicationId)}, | ||
}.Encode(), | ||
} | ||
|
||
var response applicationUserResponseDto | ||
apiResp, err := client.Api.Execute( | ||
ctx, | ||
[]string{client.Api.GetConfig().Urls.PowerAppsScope}, | ||
"GET", | ||
dataverseApiUrl.String(), | ||
nil, | ||
nil, | ||
[]int{http.StatusOK}, | ||
&response, | ||
) | ||
|
||
if err != nil { | ||
if apiResp != nil && apiResp.HttpResponse != nil && apiResp.HttpResponse.StatusCode == http.StatusNotFound { | ||
return false, nil // Environment or entity not found - not an error | ||
} | ||
if customerrors.Code(err) == customerrors.ERROR_OBJECT_NOT_FOUND { | ||
return false, nil | ||
} | ||
return false, err | ||
} | ||
|
||
// Check if we found the application user | ||
return len(response.Value) > 0, nil | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
package environment_application_admin | ||
|
||
type applicationUserDto struct { | ||
ApplicationId string `json:"applicationId"` | ||
} | ||
|
||
type applicationUserResponseDto struct { | ||
Value []applicationUserDataverseDto `json:"value"` | ||
} | ||
|
||
// This structure represents the response from Dataverse API for applicationUsers query | ||
type applicationUserDataverseDto struct { | ||
ApplicationId string `json:"applicationid"` | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
package environment_application_admin | ||
|
||
import ( | ||
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
"github.com/microsoft/terraform-provider-power-platform/internal/customtypes" | ||
"github.com/microsoft/terraform-provider-power-platform/internal/helpers" | ||
) | ||
|
||
type EnvironmentApplicationAdminResource struct { | ||
helpers.TypeInfo | ||
EnvironmentApplicationAdminClient client | ||
} | ||
|
||
type EnvironmentApplicationAdminResourceModel struct { | ||
Timeouts timeouts.Value `tfsdk:"timeouts"` | ||
EnvironmentId customtypes.UUIDValue `tfsdk:"environment_id"` | ||
ApplicationId customtypes.UUIDValue `tfsdk:"application_id"` | ||
Id types.String `tfsdk:"id"` | ||
} |
217 changes: 217 additions & 0 deletions
217
internal/services/environment_application_admin/resource_environment_application_admin.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
package environment_application_admin | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" | ||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" | ||
"github.com/hashicorp/terraform-plugin-framework/path" | ||
"github.com/hashicorp/terraform-plugin-framework/resource" | ||
"github.com/hashicorp/terraform-plugin-framework/resource/schema" | ||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" | ||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" | ||
"github.com/hashicorp/terraform-plugin-framework/schema/validator" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
"github.com/hashicorp/terraform-plugin-log/tflog" | ||
"github.com/microsoft/terraform-provider-power-platform/internal/api" | ||
"github.com/microsoft/terraform-provider-power-platform/internal/customtypes" | ||
"github.com/microsoft/terraform-provider-power-platform/internal/helpers" | ||
) | ||
|
||
// NewEnvironmentApplicationAdminResource creates a new instance of the resource. | ||
func NewEnvironmentApplicationAdminResource() resource.Resource { | ||
return &EnvironmentApplicationAdminResource{ | ||
TypeInfo: helpers.TypeInfo{ | ||
TypeName: "environment_application_admin", | ||
}, | ||
} | ||
} | ||
|
||
func (r *EnvironmentApplicationAdminResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { | ||
// update our own internal storage of the provider type name. | ||
r.ProviderTypeName = req.ProviderTypeName | ||
|
||
ctx, exitContext := helpers.EnterRequestContext(ctx, r.TypeInfo, req) | ||
defer exitContext() | ||
|
||
// Set the type name for the resource to providername_resourcename. | ||
resp.TypeName = r.FullTypeName() | ||
tflog.Debug(ctx, fmt.Sprintf("METADATA: %s", resp.TypeName)) | ||
} | ||
|
||
func (r *EnvironmentApplicationAdminResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { | ||
ctx, exitContext := helpers.EnterRequestContext(ctx, r.TypeInfo, req) | ||
defer exitContext() | ||
|
||
resp.Schema = schema.Schema{ | ||
MarkdownDescription: "Ensures a Microsoft Entra **service principal** exists in a Dataverse environment as an **application user** with the **System Administrator** role.\n\n" + | ||
"*Required for imported environments.* Environments created by the SP already include it.\n\n" + | ||
"**Deletion is a no‑op** — Dataverse currently exposes no API to remove application users. If you must revoke access, delete it manually in PPAC or via the Dataverse Web API.", | ||
Attributes: map[string]schema.Attribute{ | ||
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{ | ||
Create: true, | ||
Read: true, | ||
}), | ||
"environment_id": schema.StringAttribute{ | ||
MarkdownDescription: "Dataverse environment ID.", | ||
Required: true, | ||
CustomType: customtypes.UUIDType{}, | ||
PlanModifiers: []planmodifier.String{ | ||
stringplanmodifier.RequiresReplace(), | ||
}, | ||
Validators: []validator.String{ | ||
stringvalidator.LengthBetween(36, 36), | ||
}, | ||
}, | ||
"application_id": schema.StringAttribute{ | ||
MarkdownDescription: "Service‑principal *application_id* (client ID).", | ||
Required: true, | ||
CustomType: customtypes.UUIDType{}, | ||
PlanModifiers: []planmodifier.String{ | ||
stringplanmodifier.RequiresReplace(), | ||
}, | ||
Validators: []validator.String{ | ||
stringvalidator.LengthBetween(36, 36), | ||
}, | ||
}, | ||
"id": schema.StringAttribute{ | ||
MarkdownDescription: "Composite ID `{environment_id}/{application_id}`.", | ||
Computed: true, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func (r *EnvironmentApplicationAdminResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { | ||
ctx, exitContext := helpers.EnterRequestContext(ctx, r.TypeInfo, req) | ||
defer exitContext() | ||
|
||
if req.ProviderData == nil { | ||
// ProviderData will be null when Configure is called from ValidateConfig. It's ok. | ||
return | ||
} | ||
|
||
client, ok := req.ProviderData.(*api.ProviderClient) | ||
if !ok { | ||
resp.Diagnostics.AddError( | ||
"Unexpected ProviderData Type", | ||
fmt.Sprintf("Expected *api.ProviderClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), | ||
) | ||
return | ||
} | ||
|
||
r.EnvironmentApplicationAdminClient = newEnvironmentApplicationAdminClient(client.Api) | ||
} | ||
|
||
func (r *EnvironmentApplicationAdminResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { | ||
ctx, exitContext := helpers.EnterRequestContext(ctx, r.TypeInfo, req) | ||
defer exitContext() | ||
|
||
// Import format: {environment_id}/{application_id} | ||
idParts := strings.Split(req.ID, "/") | ||
if len(idParts) != 2 { | ||
resp.Diagnostics.AddError( | ||
"Error parsing import ID", | ||
fmt.Sprintf("Expected format: {environment_id}/{application_id}, got: %s", req.ID), | ||
) | ||
return | ||
} | ||
|
||
environmentId := idParts[0] | ||
applicationId := idParts[1] | ||
|
||
// Set the attributes in the state | ||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("environment_id"), environmentId)...) | ||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("application_id"), applicationId)...) | ||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...) | ||
} | ||
|
||
func (r *EnvironmentApplicationAdminResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { | ||
ctx, exitContext := helpers.EnterRequestContext(ctx, r.TypeInfo, req) | ||
defer exitContext() | ||
|
||
var state EnvironmentApplicationAdminResourceModel | ||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
// Query Dataverse to check if application user exists. | ||
exists, err := r.EnvironmentApplicationAdminClient.GetApplicationUser( | ||
ctx, | ||
state.EnvironmentId.ValueString(), | ||
state.ApplicationId.ValueString(), | ||
) | ||
|
||
if err != nil { | ||
resp.Diagnostics.AddError("Client error when reading application user", err.Error()) | ||
return | ||
} | ||
|
||
// If the application user doesn't exist, remove resource from state. | ||
if !exists { | ||
tflog.Debug(ctx, "Application user not found in Dataverse, removing from state", map[string]any{ | ||
"environment_id": state.EnvironmentId.ValueString(), | ||
"application_id": state.ApplicationId.ValueString(), | ||
}) | ||
resp.State.RemoveResource(ctx) | ||
return | ||
} | ||
|
||
// Application user exists, keep state | ||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) | ||
} | ||
|
||
func (r *EnvironmentApplicationAdminResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { | ||
ctx, exitContext := helpers.EnterRequestContext(ctx, r.TypeInfo, req) | ||
defer exitContext() | ||
|
||
var plan EnvironmentApplicationAdminResourceModel | ||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
// Add the application user to the environment | ||
err := r.EnvironmentApplicationAdminClient.AddApplicationUser( | ||
ctx, | ||
plan.EnvironmentId.ValueString(), | ||
plan.ApplicationId.ValueString(), | ||
) | ||
|
||
if err != nil { | ||
resp.Diagnostics.AddError("Failed to add service principal as application user", err.Error()) | ||
return | ||
} | ||
|
||
// Set the composite ID | ||
compositeId := fmt.Sprintf("%s/%s", plan.EnvironmentId.ValueString(), plan.ApplicationId.ValueString()) | ||
plan.Id = types.StringValue(compositeId) | ||
|
||
// Set state | ||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) | ||
} | ||
|
||
func (r *EnvironmentApplicationAdminResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { | ||
ctx, exitContext := helpers.EnterRequestContext(ctx, r.TypeInfo, req) | ||
defer exitContext() | ||
|
||
// Dataverse API does not expose a way to remove application users | ||
// Document this as a no-op in the resource description | ||
tflog.Info(ctx, "Delete is a no-op for environment_application_admin resource", map[string]any{ | ||
"message": "Dataverse does not provide an API to remove application users. The user must be removed manually if needed.", | ||
}) | ||
} | ||
|
||
func (r *EnvironmentApplicationAdminResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { | ||
ctx, exitContext := helpers.EnterRequestContext(ctx, r.TypeInfo, req) | ||
|
||
defer exitContext() | ||
|
||
// All attributes require replacement, so Update should never be called | ||
resp.Diagnostics.AddError("Update not supported", "Update operation should not be triggered as all attributes have ForceNew set") | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.