Skip to content

Commit 859066b

Browse files
[fix][GOV] Update Read() method to update state with permissions provisioned only via terraform (#48)
* added mapping * added mapping * Added tests * added http recordings for replay tests * commented tests * lint * linter * tweaks and new testing approach --------- Co-authored-by: Luke Wright <lukew@retool.com>
1 parent 924ce94 commit 859066b

File tree

6 files changed

+3144
-180
lines changed

6 files changed

+3144
-180
lines changed

internal/provider/permissions/resource.go

Lines changed: 103 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -260,76 +260,25 @@ func (r *permissionResource) grantPermission(ctx context.Context, subject permis
260260
return diags
261261
}
262262

263-
func (r *permissionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
264-
// Retrieve values from plan.
265-
var plan permissionsResourceModel
266-
var planSubject permissionSubjectModel
267-
268-
diags := req.Plan.Get(ctx, &plan)
269-
resp.Diagnostics.Append(diags...)
270-
if resp.Diagnostics.HasError() {
271-
return
272-
}
273-
274-
diags = plan.Subject.As(ctx, &planSubject, basetypes.ObjectAsOptions{})
275-
resp.Diagnostics.Append(diags...)
276-
if resp.Diagnostics.HasError() {
277-
return
278-
}
279-
280-
for _, planPermission := range plan.Permissions {
281-
diags = r.grantPermission(ctx, planSubject, planPermission)
282-
283-
resp.Diagnostics.Append(diags...)
284-
if resp.Diagnostics.HasError() {
285-
return
286-
}
287-
}
288-
289-
// Set state to fully populated data.
290-
diags = resp.State.Set(ctx, plan)
291-
resp.Diagnostics.Append(diags...)
292-
if resp.Diagnostics.HasError() {
293-
tflog.Error(ctx, "Error creating permissions", map[string]interface{}{"error": "Could not set state"})
294-
return
295-
}
296-
}
297-
298-
func (r *permissionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
299-
var state permissionsResourceModel
300-
301-
diags := req.State.Get(ctx, &state)
302-
resp.Diagnostics.Append(diags...)
303-
if resp.Diagnostics.HasError() {
304-
return
305-
}
306-
307-
var stateSubject permissionSubjectModel
308-
309-
diags = state.Subject.As(ctx, &stateSubject, basetypes.ObjectAsOptions{})
310-
resp.Diagnostics.Append(diags...)
311-
if resp.Diagnostics.HasError() {
312-
return
313-
}
314-
263+
func (r *permissionResource) fetchPermissionsForSubject(ctx context.Context, subject permissionSubjectModel) ([]permissionModel, diag.Diagnostics) {
315264
var permissions []permissionModel
265+
var allDiags diag.Diagnostics
316266

317-
subjectID := stateSubject.ID.ValueString() + "|" + stateSubject.Type.ValueString()
267+
subjectID := subject.ID.ValueString() + "|" + subject.Type.ValueString()
318268

319-
// We'll need to get all the permissions for the given subject.
320269
for _, objectType := range []string{"app", "folder", "resource", "resource_configuration"} {
321-
request := api.NewPermissionsListObjectsPostRequest(createNewAPIPermissionsSubject(stateSubject), objectType)
270+
request := api.NewPermissionsListObjectsPostRequest(createNewAPIPermissionsSubject(subject), objectType)
322271

323-
tflog.Info(ctx, "Reading permission", map[string]interface{}{"subjectId": subjectID})
272+
tflog.Info(ctx, "Fetching permissions", map[string]interface{}{"subjectId": subjectID, "objectType": objectType})
324273

325274
permissionsResponse, httpResponse, err := r.client.PermissionsAPI.PermissionsListObjectsPost(ctx).PermissionsListObjectsPostRequest(*request).Execute()
326275
if err != nil {
327-
resp.Diagnostics.AddError(
276+
allDiags.AddError(
328277
"Error reading permission",
329278
fmt.Sprintf("Could not read permissions for id: %s, object type: %s, error: %s", subjectID, objectType, err.Error()),
330279
)
331280
tflog.Error(ctx, "Error reading group", utils.AddHTTPStatusCode(map[string]any{"permissionId": subjectID, "objectType": objectType, "error": err.Error()}, httpResponse))
332-
return
281+
return nil, allDiags
333282
}
334283

335284
// Now let's populate the state with permissions based on our API response.
@@ -372,18 +321,103 @@ func (r *permissionResource) Read(ctx context.Context, req resource.ReadRequest,
372321
Type: types.StringValue(objectType),
373322
}
374323
object, diags := types.ObjectValueFrom(ctx, objValue.AttributeTypes(), objValue)
375-
resp.Diagnostics.Append(diags...)
376-
if resp.Diagnostics.HasError() {
377-
return
324+
allDiags.Append(diags...)
325+
if allDiags.HasError() {
326+
return nil, allDiags
378327
}
379328
permissions = append(permissions, permissionModel{
380329
Object: object,
381330
AccessLevel: types.StringValue(accessLevel),
382331
})
383332
}
384333
}
334+
return permissions, allDiags
335+
}
385336

386-
state.Permissions = permissions
337+
func (r *permissionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
338+
// Retrieve values from plan.
339+
var plan permissionsResourceModel
340+
var planSubject permissionSubjectModel
341+
342+
diags := req.Plan.Get(ctx, &plan)
343+
resp.Diagnostics.Append(diags...)
344+
if resp.Diagnostics.HasError() {
345+
return
346+
}
347+
348+
diags = plan.Subject.As(ctx, &planSubject, basetypes.ObjectAsOptions{})
349+
resp.Diagnostics.Append(diags...)
350+
if resp.Diagnostics.HasError() {
351+
return
352+
}
353+
354+
for _, planPermission := range plan.Permissions {
355+
diags = r.grantPermission(ctx, planSubject, planPermission)
356+
357+
resp.Diagnostics.Append(diags...)
358+
if resp.Diagnostics.HasError() {
359+
return
360+
}
361+
}
362+
363+
// Set state to fully populated data.
364+
diags = resp.State.Set(ctx, plan)
365+
resp.Diagnostics.Append(diags...)
366+
if resp.Diagnostics.HasError() {
367+
tflog.Error(ctx, "Error creating permissions", map[string]interface{}{"error": "Could not set state"})
368+
return
369+
}
370+
}
371+
372+
func (r *permissionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
373+
var state permissionsResourceModel
374+
375+
diags := req.State.Get(ctx, &state)
376+
resp.Diagnostics.Append(diags...)
377+
if resp.Diagnostics.HasError() {
378+
return
379+
}
380+
381+
var stateSubject permissionSubjectModel
382+
var managedPermissionKeys = make(map[string]bool)
383+
384+
for _, permission := range state.Permissions {
385+
var obj permissionObjectModel
386+
diags := permission.Object.As(ctx, &obj, basetypes.ObjectAsOptions{})
387+
if diags.HasError() {
388+
return
389+
}
390+
key := obj.ID.ValueString() + "|" + obj.Type.ValueString()
391+
managedPermissionKeys[key] = true
392+
}
393+
394+
diags = state.Subject.As(ctx, &stateSubject, basetypes.ObjectAsOptions{})
395+
resp.Diagnostics.Append(diags...)
396+
if resp.Diagnostics.HasError() {
397+
return
398+
}
399+
400+
allPermissions, diags := r.fetchPermissionsForSubject(ctx, stateSubject)
401+
resp.Diagnostics.Append(diags...)
402+
if resp.Diagnostics.HasError() {
403+
return
404+
}
405+
406+
var filteredPermissions []permissionModel
407+
for _, perm := range allPermissions {
408+
var obj permissionObjectModel
409+
diags := perm.Object.As(ctx, &obj, basetypes.ObjectAsOptions{})
410+
if diags.HasError() {
411+
resp.Diagnostics.Append(diags...)
412+
return
413+
}
414+
key := obj.ID.ValueString() + "|" + obj.Type.ValueString()
415+
if managedPermissionKeys[key] {
416+
filteredPermissions = append(filteredPermissions, perm)
417+
}
418+
}
419+
420+
state.Permissions = filteredPermissions
387421

388422
diags = resp.State.Set(ctx, &state)
389423
resp.Diagnostics.Append(diags...)
@@ -548,4 +582,11 @@ func (r *permissionResource) ImportState(ctx context.Context, req resource.Impor
548582
Type: types.StringValue(subjType),
549583
}
550584
resp.State.SetAttribute(ctx, path.Root("subject"), subject)
585+
586+
allPermissions, diags := r.fetchPermissionsForSubject(ctx, subject)
587+
resp.Diagnostics.Append(diags...)
588+
if resp.Diagnostics.HasError() {
589+
return
590+
}
591+
resp.State.SetAttribute(ctx, path.Root("permissions"), allPermissions)
551592
}

internal/provider/permissions/resource_test.go

Lines changed: 169 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package permissions_test
22

33
import (
4+
// "context".
45
"fmt"
6+
// "strconv".
57
"testing"
68

79
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
810
"github.com/hashicorp/terraform-plugin-testing/terraform"
911

1012
"github.com/tryretool/terraform-provider-retool/internal/acctest"
13+
// "github.com/tryretool/terraform-provider-retool/internal/sdk/api".
1114
)
1215

1316
const testPermissionsConfig = `
@@ -43,8 +46,7 @@ resource "retool_permissions" "test_permissions" {
4346
}
4447
`
4548

46-
const testUpdatedPermissionsConfig = `
47-
resource "retool_group" "test_group" {
49+
const testUpdatedPermissionsConfig = `resource "retool_group" "test_group" {
4850
name = "tf-acc-test-group"
4951
}
5052
@@ -127,3 +129,168 @@ func TestAccPermissions(t *testing.T) {
127129
},
128130
})
129131
}
132+
133+
// TestAccPermissions_ManagedDeletion verifies that the permissions resource properly
134+
// manages only the permissions it creates by testing updates and removals.
135+
func TestAccPermissions_ManagedDeletion(t *testing.T) {
136+
// Step 1: Create permissions for two folders.
137+
configWithTwoPerms := `
138+
resource "retool_group" "test_group" {
139+
name = "tf-acc-test-group-managed-del"
140+
}
141+
142+
resource "retool_folder" "test_folder1" {
143+
name = "tf-acc-test-folder-managed-1"
144+
folder_type = "app"
145+
}
146+
147+
resource "retool_folder" "test_folder2" {
148+
name = "tf-acc-test-folder-managed-2"
149+
folder_type = "app"
150+
}
151+
152+
resource "retool_permissions" "test_permissions" {
153+
subject = {
154+
type = "group"
155+
id = retool_group.test_group.id
156+
}
157+
permissions = [
158+
{
159+
object = {
160+
type = "folder"
161+
id = retool_folder.test_folder1.id
162+
}
163+
access_level = "use"
164+
},
165+
{
166+
object = {
167+
type = "folder"
168+
id = retool_folder.test_folder2.id
169+
}
170+
access_level = "edit"
171+
},
172+
]
173+
}
174+
`
175+
176+
// Step 2: Remove one permission from config.
177+
configWithOnePerm := `
178+
resource "retool_group" "test_group" {
179+
name = "tf-acc-test-group-managed-del"
180+
}
181+
182+
resource "retool_folder" "test_folder1" {
183+
name = "tf-acc-test-folder-managed-1"
184+
folder_type = "app"
185+
}
186+
187+
resource "retool_folder" "test_folder2" {
188+
name = "tf-acc-test-folder-managed-2"
189+
folder_type = "app"
190+
}
191+
192+
resource "retool_permissions" "test_permissions" {
193+
subject = {
194+
type = "group"
195+
id = retool_group.test_group.id
196+
}
197+
permissions = [
198+
{
199+
object = {
200+
type = "folder"
201+
id = retool_folder.test_folder2.id
202+
}
203+
access_level = "edit"
204+
},
205+
]
206+
}
207+
`
208+
209+
acctest.Test(t, resource.TestCase{
210+
Steps: []resource.TestStep{
211+
// Create permissions for two folders.
212+
{
213+
Config: configWithTwoPerms,
214+
Check: resource.ComposeAggregateTestCheckFunc(
215+
resource.TestCheckResourceAttr("retool_permissions.test_permissions", "permissions.#", "2"),
216+
// Don't check specific indices as order may vary.
217+
),
218+
},
219+
// Remove one permission from config.
220+
{
221+
Config: configWithOnePerm,
222+
Check: resource.ComposeAggregateTestCheckFunc(
223+
// Verify only one permission remains.
224+
resource.TestCheckResourceAttr("retool_permissions.test_permissions", "permissions.#", "1"),
225+
resource.TestCheckResourceAttr("retool_permissions.test_permissions", "permissions.0.access_level", "edit"),
226+
),
227+
},
228+
},
229+
})
230+
}
231+
232+
// TestAccPermissions_ImportAndRead verifies that importing permissions and subsequent
233+
// reads correctly maintain the imported permissions in state.
234+
func TestAccPermissions_ImportAndRead(t *testing.T) {
235+
config := `
236+
resource "retool_group" "test_group_import" {
237+
name = "tf-acc-test-group-import"
238+
}
239+
240+
resource "retool_folder" "test_folder_import" {
241+
name = "tf-acc-test-folder-import"
242+
folder_type = "app"
243+
}
244+
245+
resource "retool_permissions" "test_permissions_import" {
246+
subject = {
247+
type = "group"
248+
id = retool_group.test_group_import.id
249+
}
250+
permissions = [
251+
{
252+
object = {
253+
type = "folder"
254+
id = retool_folder.test_folder_import.id
255+
}
256+
access_level = "own"
257+
},
258+
]
259+
}
260+
`
261+
262+
acctest.Test(t, resource.TestCase{
263+
Steps: []resource.TestStep{
264+
// Create the permissions.
265+
{
266+
Config: config,
267+
Check: resource.ComposeAggregateTestCheckFunc(
268+
resource.TestCheckResourceAttr("retool_permissions.test_permissions_import", "permissions.#", "1"),
269+
resource.TestCheckResourceAttr("retool_permissions.test_permissions_import", "permissions.0.access_level", "own"),
270+
),
271+
},
272+
// Import the permissions.
273+
{
274+
ResourceName: "retool_permissions.test_permissions_import",
275+
ImportState: true,
276+
ImportStateIdFunc: func(state *terraform.State) (string, error) {
277+
permissions, ok := state.RootModule().Resources["retool_permissions.test_permissions_import"]
278+
if !ok {
279+
return "", fmt.Errorf("Resource not found")
280+
}
281+
return "group|" + permissions.Primary.Attributes["subject.id"], nil
282+
},
283+
ImportStateVerify: true,
284+
ImportStateVerifyIdentifierAttribute: "subject.id",
285+
},
286+
// Re-apply config to trigger Read() and verify state is maintained.
287+
{
288+
Config: config,
289+
Check: resource.ComposeAggregateTestCheckFunc(
290+
resource.TestCheckResourceAttr("retool_permissions.test_permissions_import", "permissions.#", "1"),
291+
resource.TestCheckResourceAttr("retool_permissions.test_permissions_import", "permissions.0.access_level", "own"),
292+
),
293+
},
294+
},
295+
})
296+
}

terraform-provider-retool

144 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)