teamcity/user_role_assignment.go (308 lines of code) (raw):
package teamcity
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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"strconv"
"strings"
"terraform-provider-teamcity/client"
)
var (
_ resource.Resource = &userRoleAssignmentResource{}
_ resource.ResourceWithConfigure = &userRoleAssignmentResource{}
_ resource.ResourceWithImportState = &userRoleAssignmentResource{}
)
type userRoleAssignmentResource struct {
client *client.Client
}
func NewUserRoleAssignmentResource() resource.Resource {
return &userRoleAssignmentResource{}
}
func (r *userRoleAssignmentResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_user_role_assignment"
}
type userRoleAssignmentResourceModel struct {
Id types.String `tfsdk:"id"`
UserId types.String `tfsdk:"user_id"`
Username types.String `tfsdk:"username"`
RoleId types.String `tfsdk:"role_id"`
Scope types.String `tfsdk:"scope"`
}
func (r *userRoleAssignmentResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages role assignments for TeamCity users. This resource allows you to assign roles to users either globally or for specific projects.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Description: "The ID of the role assignment (computed).",
},
"user_id": schema.StringAttribute{
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
Description: "The ID of the user to assign the role to. Either user_id or username must be specified.",
},
"username": schema.StringAttribute{
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Description: "The username of the user to assign the role to. Either user_id or username must be specified.",
},
"role_id": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Description: "The ID of the role to assign.",
},
"scope": schema.StringAttribute{
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Description: "The scope of the role assignment. Use 'g' for global scope or 'p:PROJECT_ID' for project-specific scope. Defaults to global if not specified.",
},
},
}
}
func (r *userRoleAssignmentResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
r.client = req.ProviderData.(*client.Client)
}
func (r *userRoleAssignmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan userRoleAssignmentResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Validate that either user_id or username is specified
if plan.UserId.IsNull() && plan.Username.IsNull() {
resp.Diagnostics.AddError(
"Invalid configuration",
"Either 'user_id' or 'username' must be specified",
)
return
}
// Get user to obtain ID if username was provided
var user *client.User
var err error
var userId string
if !plan.Username.IsNull() {
user, err = r.client.GetUserByName(plan.Username.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error getting user",
err.Error(),
)
return
}
if user == nil {
resp.Diagnostics.AddError(
"User not found",
"User with username '"+plan.Username.ValueString()+"' not found",
)
return
}
userId = strconv.FormatInt(*user.Id, 10)
} else {
userId = plan.UserId.ValueString()
// Verify user exists
user, err = r.client.GetUser(userId)
if err != nil {
resp.Diagnostics.AddError(
"Error getting user",
err.Error(),
)
return
}
if user == nil {
resp.Diagnostics.AddError(
"User not found",
"User with ID '"+userId+"' not found",
)
return
}
}
// Determine scope
scope := "g" // default to global
if !plan.Scope.IsNull() {
scope = plan.Scope.ValueString()
}
// Build updated user with new role
updatedUser := client.User{
Id: user.Id,
Username: user.Username,
}
// Copy existing roles and add new one
updatedUser.Roles = &client.RoleAssignments{
RoleAssignment: []client.RoleAssignment{},
}
// Copy existing roles
if user.Roles != nil {
for _, role := range user.Roles.RoleAssignment {
updatedUser.Roles.RoleAssignment = append(updatedUser.Roles.RoleAssignment, role)
}
}
// Add new role
updatedUser.Roles.RoleAssignment = append(updatedUser.Roles.RoleAssignment, client.RoleAssignment{
Id: plan.RoleId.ValueString(),
Scope: scope,
})
// Update user
_, err = r.client.SetUser(updatedUser)
if err != nil {
resp.Diagnostics.AddError(
"Error assigning role to user",
err.Error(),
)
return
}
// Set state
state := userRoleAssignmentResourceModel{
Id: types.StringValue(fmt.Sprintf("%s_%s_%s", userId, plan.RoleId.ValueString(), scope)),
UserId: types.StringValue(userId),
Username: types.StringValue(user.Username),
RoleId: plan.RoleId,
Scope: types.StringValue(scope),
}
diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
}
func (r *userRoleAssignmentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state userRoleAssignmentResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Get user to verify role assignment still exists
user, err := r.client.GetUser(state.UserId.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error reading user",
err.Error(),
)
return
}
if user == nil {
resp.State.RemoveResource(ctx)
return
}
// Update username in case it changed
state.Username = types.StringValue(user.Username)
// Check if role assignment still exists
found := false
if user.Roles != nil {
for _, role := range user.Roles.RoleAssignment {
if role.Id == state.RoleId.ValueString() && role.Scope == state.Scope.ValueString() {
found = true
break
}
}
}
if !found {
resp.State.RemoveResource(ctx)
return
}
// Role assignment exists, update state
diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
}
func (r *userRoleAssignmentResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// This resource doesn't support updates - all attributes have RequiresReplace
// The framework will handle this by destroying and recreating the resource
resp.Diagnostics.AddError(
"Update not supported",
"This resource does not support updates. All changes require resource replacement.",
)
}
func (r *userRoleAssignmentResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state userRoleAssignmentResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Get current user
user, err := r.client.GetUser(state.UserId.ValueString())
if err != nil {
// If user doesn't exist, consider it deleted
if strings.Contains(err.Error(), "404") {
return
}
resp.Diagnostics.AddError(
"Error getting user",
err.Error(),
)
return
}
if user == nil {
// User doesn't exist, nothing to delete
return
}
// Build updated user without the role
updatedUser := client.User{
Id: user.Id,
Username: user.Username,
}
updatedUser.Roles = &client.RoleAssignments{
RoleAssignment: []client.RoleAssignment{},
}
// Copy existing roles except the one being deleted
if user.Roles != nil {
for _, role := range user.Roles.RoleAssignment {
if !(role.Id == state.RoleId.ValueString() && role.Scope == state.Scope.ValueString()) {
updatedUser.Roles.RoleAssignment = append(updatedUser.Roles.RoleAssignment, role)
}
}
}
// Update user
_, err = r.client.SetUser(updatedUser)
if err != nil {
resp.Diagnostics.AddError(
"Error removing role from user",
err.Error(),
)
}
}
func (r *userRoleAssignmentResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Import ID format: user_id/role_id/scope or username/role_id/scope
parts := strings.Split(req.ID, "/")
if len(parts) != 3 {
resp.Diagnostics.AddError(
"Invalid import ID",
"Import ID must be in the format: user_id/role_id/scope or username/role_id/scope",
)
return
}
// Try to determine if first part is user_id or username
var userId string
var username string
// Check if it's a numeric ID
if _, err := strconv.ParseInt(parts[0], 10, 64); err == nil {
// It's a user ID
userId = parts[0]
// Get username
user, err := r.client.GetUser(userId)
if err != nil {
resp.Diagnostics.AddError(
"Error getting user",
err.Error(),
)
return
}
if user != nil {
username = user.Username
}
} else {
// It's a username
username = parts[0]
// Get user ID
user, err := r.client.GetUserByName(username)
if err != nil {
resp.Diagnostics.AddError(
"Error getting user",
err.Error(),
)
return
}
if user != nil {
userId = strconv.FormatInt(*user.Id, 10)
}
}
state := userRoleAssignmentResourceModel{
Id: types.StringValue(fmt.Sprintf("%s_%s_%s", userId, parts[1], parts[2])),
UserId: types.StringValue(userId),
Username: types.StringValue(username),
RoleId: types.StringValue(parts[1]),
Scope: types.StringValue(parts[2]),
}
diags := resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
}