Security fixes from audit: - Escape user-supplied strings (ext_relation_id, interface_name) with url.PathEscape before interpolating into API URL paths, preventing path traversal via crafted values like "../admin" or "foo/bar" - Mark auth token URL attributes as Sensitive in both virtfusion_user_auth_token and virtfusion_user_server_auth_token resources, since the URL embeds the signed token - Truncate raw API error response bodies to 500 bytes in error messages to prevent leaking sensitive data from verbose Laravel error responses Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
139 lines
4.8 KiB
Go
139 lines
4.8 KiB
Go
// Copyright (c) EZSCALE.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package provider
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"time"
|
|
|
|
"terraform-provider-virtfusion/internal/client"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
)
|
|
|
|
// Ensure provider-defined types fully satisfy framework interfaces.
|
|
var (
|
|
_ resource.Resource = &UserPasswordResetResource{}
|
|
_ resource.ResourceWithConfigure = &UserPasswordResetResource{}
|
|
)
|
|
|
|
// NewUserPasswordResetResource creates a new user password reset resource.
|
|
func NewUserPasswordResetResource() resource.Resource {
|
|
return &UserPasswordResetResource{}
|
|
}
|
|
|
|
// UserPasswordResetResource defines the resource implementation.
|
|
type UserPasswordResetResource struct {
|
|
client *client.Client
|
|
}
|
|
|
|
// UserPasswordResetResourceModel describes the resource data model.
|
|
type UserPasswordResetResourceModel struct {
|
|
ID types.String `tfsdk:"id"`
|
|
ExtRelationID types.String `tfsdk:"ext_relation_id"`
|
|
Triggers types.Map `tfsdk:"triggers"`
|
|
}
|
|
|
|
func (r *UserPasswordResetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
resp.TypeName = req.ProviderTypeName + "_user_password_reset"
|
|
}
|
|
|
|
func (r *UserPasswordResetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
|
resp.Schema = schema.Schema{
|
|
MarkdownDescription: "Resets the password for a VirtFusion user by external relation ID. This is a trigger-style resource — the reset is executed on create and can be re-triggered by changing the `triggers` attribute.",
|
|
Attributes: map[string]schema.Attribute{
|
|
"id": schema.StringAttribute{
|
|
MarkdownDescription: "The identifier for this password reset.",
|
|
Computed: true,
|
|
PlanModifiers: []planmodifier.String{
|
|
stringplanmodifier.UseStateForUnknown(),
|
|
},
|
|
},
|
|
"ext_relation_id": schema.StringAttribute{
|
|
MarkdownDescription: "The external relation ID of the user to reset the password for.",
|
|
Required: true,
|
|
},
|
|
"triggers": schema.MapAttribute{
|
|
MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the password reset to be re-executed. Works like `triggers` in `terraform_data`.",
|
|
ElementType: types.StringType,
|
|
Optional: true,
|
|
PlanModifiers: []planmodifier.Map{
|
|
mapplanmodifier.RequiresReplace(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (r *UserPasswordResetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
|
if req.ProviderData == nil {
|
|
return
|
|
}
|
|
|
|
c, ok := req.ProviderData.(*client.Client)
|
|
if !ok {
|
|
resp.Diagnostics.AddError(
|
|
"Unexpected Resource Configure Type",
|
|
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
|
)
|
|
return
|
|
}
|
|
|
|
r.client = c
|
|
}
|
|
|
|
func (r *UserPasswordResetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
|
var data UserPasswordResetResourceModel
|
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
apiPath := fmt.Sprintf("/users/%s/byExtRelation/resetPassword", url.PathEscape(data.ExtRelationID.ValueString()))
|
|
_, err := r.client.Post(ctx, apiPath, nil)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Error Resetting User Password",
|
|
fmt.Sprintf("Could not reset password for user with ext_relation_id %q: %s", data.ExtRelationID.ValueString(), err),
|
|
)
|
|
return
|
|
}
|
|
|
|
data.ID = types.StringValue(fmt.Sprintf("%s-%d", data.ExtRelationID.ValueString(), time.Now().UnixNano()))
|
|
|
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
}
|
|
|
|
func (r *UserPasswordResetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
|
var data UserPasswordResetResourceModel
|
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// Return stored state as-is for trigger-style resources.
|
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
}
|
|
|
|
func (r *UserPasswordResetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
|
var data UserPasswordResetResourceModel
|
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
}
|
|
|
|
func (r *UserPasswordResetResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
|
|
// No-op: password resets are not reversible. Removing from state only.
|
|
}
|