Skip to content

Commit 33f1d68

Browse files
fix: use incremental permission endpoints for role updates (#60)
The PATCH endpoint for resource-scoped roles returns 500 when updating a role with a large number of changed permissions (~40+). This switches role updates to use the dedicated AssignPermissions/RemovePermissions endpoints which handle incremental changes reliably at any scale. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 87fa2cb commit 33f1d68

1 file changed

Lines changed: 77 additions & 7 deletions

File tree

internal/provider/roles/client.go

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"github.com/permitio/permit-golang/pkg/models"
66
"github.com/permitio/permit-golang/pkg/permit"
77
"github.com/permitio/terraform-provider-permit-io/internal/provider/common"
8+
"github.com/samber/lo"
89
)
910

1011
type roleClient struct {
@@ -87,7 +88,7 @@ func (c *roleClient) Read(ctx context.Context, key string, resourceKey *string)
8788
}
8889

8990
func (c *roleClient) Update(ctx context.Context, plan roleModel) (roleModel, error) {
90-
permissions, err := common.ConvertElementsToSlice[string](ctx, plan.Permissions.Elements())
91+
desiredPermissions, err := common.ConvertElementsToSlice[string](ctx, plan.Permissions.Elements())
9192

9293
if err != nil {
9394
return roleModel{}, err
@@ -101,35 +102,104 @@ func (c *roleClient) Update(ctx context.Context, plan roleModel) (roleModel, err
101102

102103
var updatedModel roleModel
103104
if plan.isResourceRole() {
105+
resourceKey := plan.Resource.ValueString()
106+
roleKey := plan.Key.ValueString()
107+
108+
// Read current role to get current permissions for diff calculation
109+
currentRole, err := c.client.Api.ResourceRoles.Get(ctx, resourceKey, roleKey)
110+
if err != nil {
111+
return roleModel{}, err
112+
}
113+
114+
// PATCH only name/description/extends — permissions are handled separately
115+
// via dedicated endpoints to avoid server-side errors with large permission sets.
104116
roleUpdate := models.ResourceRoleUpdate{
105117
Name: plan.Name.ValueStringPointer(),
106118
Description: plan.Description.ValueStringPointer(),
107-
Permissions: permissions,
108119
Extends: extends,
109120
}
110121

111-
updatedRole, err := c.client.Api.ResourceRoles.Update(ctx, plan.Resource.ValueString(), plan.Key.ValueString(), roleUpdate)
122+
_, err = c.client.Api.ResourceRoles.Update(ctx, resourceKey, roleKey, roleUpdate)
123+
if err != nil {
124+
return roleModel{}, err
125+
}
126+
127+
// Compute permission diff and apply incrementally
128+
toRemove, toAdd := lo.Difference(currentRole.Permissions, desiredPermissions)
129+
130+
if len(toRemove) > 0 {
131+
_, err = c.client.Api.ResourceRoles.RemovePermissions(
132+
ctx, resourceKey, roleKey,
133+
*models.NewRemoveRolePermissions(toRemove),
134+
)
135+
if err != nil {
136+
return roleModel{}, err
137+
}
138+
}
139+
140+
if len(toAdd) > 0 {
141+
_, err = c.client.Api.ResourceRoles.AssignPermissions(
142+
ctx, resourceKey, roleKey,
143+
*models.NewAddRolePermissions(toAdd),
144+
)
145+
if err != nil {
146+
return roleModel{}, err
147+
}
148+
}
112149

150+
// Read final state
151+
finalRole, err := c.client.Api.ResourceRoles.Get(ctx, resourceKey, roleKey)
113152
if err != nil {
114153
return roleModel{}, err
115154
}
116155

117-
updatedModel = tfModelFromResourceRoleRead(plan.Resource.ValueString(), *updatedRole)
156+
updatedModel = tfModelFromResourceRoleRead(resourceKey, *finalRole)
118157
} else {
158+
roleKey := plan.Key.ValueString()
159+
160+
// Read current role to get current permissions for diff calculation
161+
currentRole, err := c.client.Api.Roles.Get(ctx, roleKey)
162+
if err != nil {
163+
return roleModel{}, err
164+
}
165+
166+
// PATCH only name/description/extends — permissions are handled separately
167+
// via dedicated endpoints to avoid server-side errors with large permission sets.
119168
roleUpdate := models.RoleUpdate{
120169
Name: plan.Name.ValueStringPointer(),
121170
Description: plan.Description.ValueStringPointer(),
122-
Permissions: permissions,
123171
Extends: extends,
124172
}
125173

126-
updatedRole, err := c.client.Api.Roles.Update(ctx, plan.Key.ValueString(), roleUpdate)
174+
_, err = c.client.Api.Roles.Update(ctx, roleKey, roleUpdate)
175+
if err != nil {
176+
return roleModel{}, err
177+
}
178+
179+
// Compute permission diff and apply incrementally
180+
toRemove, toAdd := lo.Difference(currentRole.Permissions, desiredPermissions)
181+
182+
if len(toRemove) > 0 {
183+
err = c.client.Api.Roles.RemovePermissions(ctx, roleKey, toRemove)
184+
if err != nil {
185+
return roleModel{}, err
186+
}
187+
}
188+
189+
if len(toAdd) > 0 {
190+
err = c.client.Api.Roles.AssignPermissions(ctx, roleKey, toAdd)
191+
if err != nil {
192+
return roleModel{}, err
193+
}
194+
}
127195

196+
// Read final state
197+
finalRole, err := c.client.Api.Roles.Get(ctx, roleKey)
128198
if err != nil {
129199
return roleModel{}, err
130200
}
131201

132-
updatedModel = tfModelFromRoleRead(*updatedRole)
202+
updatedModel = tfModelFromRoleRead(*finalRole)
133203
}
134204

135205
return updatedModel, nil

0 commit comments

Comments
 (0)