quorate/packages/versitygw-terraform-provider/internal/provider/user_resource.go

290 lines
9.2 KiB
Go

package provider
import (
"context"
"fmt"
"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/int64planmodifier"
"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"
"github.com/quorate/terraform-provider-versitygw/internal/client"
)
var (
_ resource.Resource = (*UserResource)(nil)
_ resource.ResourceWithConfigure = (*UserResource)(nil)
)
func NewUserResource() resource.Resource {
return &UserResource{}
}
type UserResource struct {
client *client.Client
}
type userModel struct {
ID types.String `tfsdk:"id"`
AccessKeyID types.String `tfsdk:"access_key_id"`
SecretAccessKey types.String `tfsdk:"secret_access_key"`
Role types.String `tfsdk:"role"`
UserID types.Int64 `tfsdk:"user_id"`
GroupID types.Int64 `tfsdk:"group_id"`
ProjectID types.Int64 `tfsdk:"project_id"`
}
func (r *UserResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_user"
}
func (r *UserResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "VersityGW IAM user managed through the VersityGW admin API.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
Description: "User access key ID.",
},
"access_key_id": schema.StringAttribute{
Required: true,
Description: "VersityGW username/access key ID.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"secret_access_key": schema.StringAttribute{
Required: true,
Sensitive: true,
WriteOnly: true,
Description: "VersityGW user secret access key.",
},
"role": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "VersityGW user role: user, userplus, or admin. Defaults to user.",
},
"user_id": schema.Int64Attribute{
Optional: true,
Computed: true,
Description: "Optional POSIX user ID stored on the VersityGW user.",
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"group_id": schema.Int64Attribute{
Optional: true,
Computed: true,
Description: "Optional POSIX group ID stored on the VersityGW user.",
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"project_id": schema.Int64Attribute{
Optional: true,
Computed: true,
Description: "Optional project ID stored on the VersityGW user.",
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
},
}
}
func (r *UserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
api, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError("Unexpected provider data", fmt.Sprintf("Expected *client.Client, got %T", req.ProviderData))
return
}
r.client = api
}
func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan userModel
var config userModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
if !validateUser(plan, config.SecretAccessKey, &resp.Diagnostics) {
return
}
role := valueOrDefault(plan.Role, "user")
account := client.Account{
Access: plan.AccessKeyID.ValueString(),
Secret: config.SecretAccessKey.ValueString(),
Role: role,
UserID: int64OrZero(plan.UserID),
GroupID: int64OrZero(plan.GroupID),
ProjectID: int64OrZero(plan.ProjectID),
}
if err := r.client.CreateUser(ctx, account); err != nil {
if !client.IsErrorCode(err, "XAdminUserExists") {
resp.Diagnostics.AddError("Create VersityGW user failed", err.Error())
return
}
secret := account.Secret
props := client.MutableProps{
Secret: &secret,
Role: account.Role,
UserID: &account.UserID,
GroupID: &account.GroupID,
ProjectID: &account.ProjectID,
}
if err := r.client.UpdateUser(ctx, account.Access, props); err != nil {
resp.Diagnostics.AddError("Adopt existing VersityGW user failed", err.Error())
return
}
}
plan.ID = plan.AccessKeyID
plan.Role = types.StringValue(role)
plan.UserID = types.Int64Value(account.UserID)
plan.GroupID = types.Int64Value(account.GroupID)
plan.ProjectID = types.Int64Value(account.ProjectID)
plan.SecretAccessKey = types.StringNull()
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state userModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
account, found, err := r.client.GetUser(ctx, state.AccessKeyID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Read VersityGW user failed", err.Error())
return
}
if !found {
resp.State.RemoveResource(ctx)
return
}
state.ID = types.StringValue(account.Access)
state.AccessKeyID = types.StringValue(account.Access)
state.Role = types.StringValue(account.Role)
state.UserID = types.Int64Value(account.UserID)
state.GroupID = types.Int64Value(account.GroupID)
state.ProjectID = types.Int64Value(account.ProjectID)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan userModel
var state userModel
var config userModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
if !validateUser(plan, config.SecretAccessKey, &resp.Diagnostics) {
return
}
role := valueOrDefault(plan.Role, "user")
secret := config.SecretAccessKey.ValueString()
userID := int64OrZero(plan.UserID)
groupID := int64OrZero(plan.GroupID)
projectID := int64OrZero(plan.ProjectID)
props := client.MutableProps{
Secret: &secret,
Role: role,
UserID: &userID,
GroupID: &groupID,
ProjectID: &projectID,
}
if err := r.client.UpdateUser(ctx, state.AccessKeyID.ValueString(), props); err != nil {
resp.Diagnostics.AddError("Update VersityGW user failed", err.Error())
return
}
plan.ID = plan.AccessKeyID
plan.Role = types.StringValue(role)
plan.UserID = types.Int64Value(userID)
plan.GroupID = types.Int64Value(groupID)
plan.ProjectID = types.Int64Value(projectID)
plan.SecretAccessKey = types.StringNull()
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *UserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state userModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if err := r.client.DeleteUser(ctx, state.AccessKeyID.ValueString()); err != nil {
resp.Diagnostics.AddError("Delete VersityGW user failed", err.Error())
return
}
}
func validateUser(model userModel, secretAccessKey types.String, diags interface {
AddAttributeError(path.Path, string, string)
}) bool {
ok := true
if model.AccessKeyID.IsNull() || model.AccessKeyID.IsUnknown() || model.AccessKeyID.ValueString() == "" {
diags.AddAttributeError(path.Root("access_key_id"), "Invalid access key ID", "access_key_id must be non-empty.")
ok = false
}
if secretAccessKey.IsNull() || secretAccessKey.IsUnknown() || secretAccessKey.ValueString() == "" {
diags.AddAttributeError(path.Root("secret_access_key"), "Invalid secret access key", "secret_access_key must be non-empty.")
ok = false
}
role := valueOrDefault(model.Role, "user")
if role != "user" && role != "userplus" && role != "admin" {
diags.AddAttributeError(path.Root("role"), "Invalid role", "role must be one of: user, userplus, admin.")
ok = false
}
if isNegative(model.UserID) {
diags.AddAttributeError(path.Root("user_id"), "Invalid user ID", "user_id must be non-negative.")
ok = false
}
if isNegative(model.GroupID) {
diags.AddAttributeError(path.Root("group_id"), "Invalid group ID", "group_id must be non-negative.")
ok = false
}
if isNegative(model.ProjectID) {
diags.AddAttributeError(path.Root("project_id"), "Invalid project ID", "project_id must be non-negative.")
ok = false
}
return ok
}
func valueOrDefault(value types.String, fallback string) string {
if value.IsNull() || value.IsUnknown() || value.ValueString() == "" {
return fallback
}
return value.ValueString()
}
func int64OrZero(value types.Int64) int64 {
if value.IsNull() || value.IsUnknown() {
return 0
}
return value.ValueInt64()
}
func isNegative(value types.Int64) bool {
return !value.IsNull() && !value.IsUnknown() && value.ValueInt64() < 0
}