Skip to content

Commit 8307a83

Browse files
committed
review changes
Signed-off-by: Mauritz Uphoff <mauritz.uphoff@stackit.cloud>
1 parent b8991b7 commit 8307a83

8 files changed

Lines changed: 247 additions & 7 deletions

stackit/internal/services/authorization/authorization_acc_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"maps"
7+
"regexp"
78
"strings"
89
"testing"
910

@@ -25,11 +26,20 @@ var (
2526
//go:embed testdata/resource-project-role-assignment.tf
2627
resourceProjectRoleAssignment string
2728

29+
//go:embed testdata/resource-project-role-assignment-duplicate.tf
30+
resourceProjectRoleAssignmentDuplicate string
31+
2832
//go:embed testdata/resource-folder-role-assignment.tf
2933
resourceFolderRoleAssignment string
3034

35+
//go:embed testdata/resource-folder-role-assignment-duplicate.tf
36+
resourceFolderRoleAssignmentDuplicate string
37+
3138
//go:embed testdata/resource-org-role-assignment.tf
3239
resourceOrgRoleAssignment string
40+
41+
//go:embed testdata/resource-org-role-assignment-duplicate.tf
42+
resourceOrgRoleAssignmentDuplicate string
3343
)
3444

3545
var testProjectName = fmt.Sprintf("proj-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))
@@ -155,6 +165,13 @@ func TestAccProjectRoleAssignmentResource(t *testing.T) {
155165
resource.TestCheckResourceAttr("stackit_authorization_project_role_assignment.pra", "subject", testutil.ConvertConfigVariable(testConfigVarsProjectRoleAssignmentUpdated()["subject"])),
156166
),
157167
},
168+
// Duplicate assignment should fail
169+
{
170+
ConfigVariables: testConfigVarsProjectRoleAssignmentUpdated(),
171+
Config: testutil.AuthorizationProviderConfig() + "\n" + resourceProjectRoleAssignmentDuplicate,
172+
ExpectError: regexp.MustCompile(`Error while checking for duplicate role assignments`),
173+
},
174+
158175
// Deletion is done by the framework implicitly
159176
},
160177
})
@@ -234,6 +251,12 @@ func TestAccFolderRoleAssignmentResource(t *testing.T) {
234251
resource.TestCheckResourceAttr("stackit_authorization_folder_role_assignment.fra", "subject", testutil.ConvertConfigVariable(testConfigVarsFolderRoleAssignmentUpdated()["subject"])),
235252
),
236253
},
254+
// Duplicate assignment should fail
255+
{
256+
ConfigVariables: testConfigVarsFolderRoleAssignmentUpdated(),
257+
Config: testutil.AuthorizationProviderConfig() + "\n" + resourceFolderRoleAssignmentDuplicate,
258+
ExpectError: regexp.MustCompile(`Error while checking for duplicate role assignments`),
259+
},
237260
// Deletion is done by the framework implicitly
238261
},
239262
})
@@ -295,6 +318,12 @@ func TestAccOrgRoleAssignmentResource(t *testing.T) {
295318
resource.TestCheckResourceAttr("stackit_authorization_organization_role_assignment.ora", "subject", testutil.ConvertConfigVariable(testConfigVarsOrgRoleAssignmentUpdated()["subject"])),
296319
),
297320
},
321+
// Duplicate assignment should fail
322+
{
323+
ConfigVariables: testConfigVarsOrgRoleAssignmentUpdated(),
324+
Config: testutil.AuthorizationProviderConfig() + "\n" + resourceOrgRoleAssignmentDuplicate,
325+
ExpectError: regexp.MustCompile(`Error while checking for duplicate role assignments`),
326+
},
298327
// Deletion is done by the framework implicitly
299328
},
300329
})

stackit/internal/services/authorization/roleassignments/resource.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ var (
3636
_ resource.ResourceWithConfigure = &roleAssignmentResource{}
3737
_ resource.ResourceWithImportState = &roleAssignmentResource{}
3838

39-
errRoleAssignmentNotFound = errors.New("response members did not contain expected role assignment")
39+
errRoleAssignmentNotFound = errors.New("response members did not contain expected role assignment")
40+
errRoleAssignmentDuplicateFound = errors.New("found a duplicate role assignment")
4041
)
4142

4243
type Model struct {
@@ -153,6 +154,17 @@ func (r *roleAssignmentResource) Create(ctx context.Context, req resource.Create
153154
ctx = tflog.SetField(ctx, "role", model.Role.ValueString())
154155
ctx = tflog.SetField(ctx, "resource_type", r.apiName)
155156

157+
listMemberResp, err := r.authorizationClient.ListMembers(ctx, r.apiName, model.ResourceId.ValueString()).Subject(model.Subject.ValueString()).Execute()
158+
if err != nil {
159+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error listing current resource members", fmt.Sprintf("Calling API: %v", err))
160+
return
161+
}
162+
163+
if err := checkDuplicate(model, listMemberResp); err != nil {
164+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error while checking for duplicate role assignments", err.Error())
165+
return
166+
}
167+
156168
// Create new project role assignment
157169
payload, err := toCreatePayload(&model, &r.apiName)
158170
if err != nil {
@@ -338,3 +350,18 @@ func mapListMembersResponse(resp *authorization.ListMembersResponse, model *Mode
338350

339351
return errRoleAssignmentNotFound
340352
}
353+
354+
// Prevent creating duplicate <resource_id, role, subject> assignments.
355+
// Duplicates lead to inconsistent state when one is removed (API removes the role entirely, Terraform plan then fails).
356+
func checkDuplicate(model Model, listMemberResp *authorization.ListMembersResponse) error { //nolint:gocritic // A read only copy is required since an api response is parsed into the model and this check should not affect the model parameter
357+
err := mapListMembersResponse(listMemberResp, &model)
358+
359+
if err != nil {
360+
if errors.Is(err, errRoleAssignmentNotFound) {
361+
return nil
362+
}
363+
return err
364+
}
365+
366+
return errRoleAssignmentDuplicateFound
367+
}

stackit/internal/services/authorization/roleassignments/resource_test.go

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package roleassignments
22

33
import (
4+
"errors"
45
"testing"
56

67
"github.com/google/go-cmp/cmp"
@@ -210,9 +211,7 @@ func TestMapListMembersResponse(t *testing.T) {
210211

211212
for _, tt := range tests {
212213
t.Run(tt.name, func(t *testing.T) {
213-
model := tt.inputModel // copy pointer to avoid overriding test data
214-
215-
err := mapListMembersResponse(tt.resp, model)
214+
err := mapListMembersResponse(tt.resp, tt.inputModel)
216215

217216
if tt.expectError && err == nil {
218217
t.Fatalf("Expected error but got none")
@@ -222,10 +221,138 @@ func TestMapListMembersResponse(t *testing.T) {
222221
}
223222

224223
if !tt.expectError {
225-
if diff := cmp.Diff(tt.expected, model); diff != "" {
224+
if diff := cmp.Diff(tt.expected, tt.inputModel); diff != "" {
226225
t.Errorf("Mapped model mismatch (-want +got):\n%s", diff)
227226
}
228227
}
229228
})
230229
}
231230
}
231+
232+
func TestCheckDuplicate(t *testing.T) {
233+
role := "editor"
234+
subject := "foo.bar@stackit.cloud"
235+
resourceID := "project"
236+
237+
tests := []struct {
238+
name string
239+
resp *authorization.ListMembersResponse
240+
inputModel Model
241+
expectError bool
242+
expectedErr error
243+
}{
244+
{
245+
name: "no members => no duplicate",
246+
resp: &authorization.ListMembersResponse{
247+
ResourceId: &resourceID,
248+
Members: &[]authorization.Member{},
249+
},
250+
inputModel: Model{
251+
ResourceId: types.StringValue(resourceID),
252+
Role: types.StringValue(role),
253+
Subject: types.StringValue(subject),
254+
},
255+
expectError: false,
256+
},
257+
{
258+
name: "members but no matching role/subject => no duplicate",
259+
resp: &authorization.ListMembersResponse{
260+
ResourceId: &resourceID,
261+
Members: &[]authorization.Member{
262+
{
263+
Role: utils.Ptr("reader"),
264+
Subject: utils.Ptr("foo.bar@stackit.cloud"),
265+
},
266+
{
267+
Role: utils.Ptr("editor"),
268+
Subject: utils.Ptr("someoneelse@stackit.cloud"),
269+
},
270+
},
271+
},
272+
inputModel: Model{
273+
ResourceId: types.StringValue(resourceID),
274+
Role: types.StringValue(role),
275+
Subject: types.StringValue(subject),
276+
},
277+
expectError: false,
278+
},
279+
{
280+
name: "matching role/subject exists => duplicate error",
281+
resp: &authorization.ListMembersResponse{
282+
ResourceId: &resourceID,
283+
Members: &[]authorization.Member{
284+
{
285+
Role: &role,
286+
Subject: &subject,
287+
},
288+
},
289+
},
290+
inputModel: Model{
291+
ResourceId: types.StringValue(resourceID),
292+
Role: types.StringValue(role),
293+
Subject: types.StringValue(subject),
294+
},
295+
expectError: true,
296+
expectedErr: errRoleAssignmentDuplicateFound,
297+
},
298+
{
299+
name: "nil response input => propagated error",
300+
resp: nil,
301+
inputModel: Model{
302+
ResourceId: types.StringValue(resourceID),
303+
Role: types.StringValue(role),
304+
Subject: types.StringValue(subject),
305+
},
306+
expectError: true,
307+
// we don't compare by value here, just expect some error
308+
},
309+
{
310+
name: "nil members input => propagated error",
311+
resp: &authorization.ListMembersResponse{
312+
ResourceId: &resourceID,
313+
Members: nil,
314+
},
315+
inputModel: Model{
316+
ResourceId: types.StringValue(resourceID),
317+
Role: types.StringValue(role),
318+
Subject: types.StringValue(subject),
319+
},
320+
expectError: true,
321+
},
322+
{
323+
name: "nil resource_id input => propagated error",
324+
resp: &authorization.ListMembersResponse{
325+
ResourceId: nil,
326+
Members: &[]authorization.Member{
327+
{
328+
Role: &role,
329+
Subject: &subject,
330+
},
331+
},
332+
},
333+
inputModel: Model{
334+
ResourceId: types.StringValue(resourceID),
335+
Role: types.StringValue(role),
336+
Subject: types.StringValue(subject),
337+
},
338+
expectError: true,
339+
},
340+
}
341+
342+
for _, tt := range tests {
343+
t.Run(tt.name, func(t *testing.T) {
344+
err := checkDuplicate(tt.inputModel, tt.resp)
345+
346+
if tt.expectError && err == nil {
347+
t.Fatalf("Expected error but got none")
348+
}
349+
if !tt.expectError && err != nil {
350+
t.Fatalf("Expected no error, but got: %v", err)
351+
}
352+
353+
if tt.expectError && tt.expectedErr != nil && !errors.Is(err, tt.expectedErr) {
354+
t.Fatalf("Expected error %v, but got: %v", tt.expectedErr, err)
355+
}
356+
})
357+
}
358+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
variable "name" {}
2+
variable "role" {}
3+
variable "owner_email" {}
4+
variable "subject" {}
5+
variable "parent_container_id" {}
6+
7+
resource "stackit_resourcemanager_folder" "folder" {
8+
name = var.name
9+
owner_email = var.owner_email
10+
parent_container_id = var.parent_container_id
11+
}
12+
13+
resource "stackit_authorization_folder_role_assignment" "fra" {
14+
resource_id = stackit_resourcemanager_folder.folder.folder_id
15+
role = var.role
16+
subject = var.subject
17+
}
18+
19+
# Second assignment – duplicates stackit_authorization_folder_role_assignment.fra (same resource_id, role, subject)
20+
resource "stackit_authorization_folder_role_assignment" "fra2" {
21+
resource_id = stackit_resourcemanager_folder.folder.folder_id
22+
role = var.role
23+
subject = var.subject
24+
}

stackit/internal/services/authorization/testdata/resource-folder-role-assignment.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ resource "stackit_resourcemanager_folder" "folder" {
1313
resource "stackit_authorization_folder_role_assignment" "fra" {
1414
resource_id = stackit_resourcemanager_folder.folder.folder_id
1515
role = var.role
16-
subject = var.owner_email
16+
subject = var.subject
1717
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
variable "role" {}
2+
variable "subject" {}
3+
variable "parent_container_id" {}
4+
5+
resource "stackit_authorization_organization_role_assignment" "ora" {
6+
resource_id = var.parent_container_id
7+
role = var.role
8+
subject = var.subject
9+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
variable "name" {}
2+
variable "role" {}
3+
variable "owner_email" {}
4+
variable "subject" {}
5+
variable "parent_container_id" {}
6+
7+
resource "stackit_resourcemanager_project" "project" {
8+
name = var.name
9+
owner_email = var.owner_email
10+
parent_container_id = var.parent_container_id
11+
}
12+
13+
resource "stackit_authorization_project_role_assignment" "pra" {
14+
resource_id = stackit_resourcemanager_project.project.project_id
15+
role = var.role
16+
subject = var.subject
17+
}
18+
19+
# Second assignment – duplicates stackit_authorization_project_role_assignment.pra (same resource_id, role, subject)
20+
resource "stackit_authorization_project_role_assignment" "pra2" {
21+
resource_id = stackit_resourcemanager_project.project.project_id
22+
role = var.role
23+
subject = var.subject
24+
}

stackit/internal/services/authorization/testdata/resource-project-role-assignment.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ resource "stackit_resourcemanager_project" "project" {
1313
resource "stackit_authorization_project_role_assignment" "pra" {
1414
resource_id = stackit_resourcemanager_project.project.project_id
1515
role = var.role
16-
subject = var.owner_email
16+
subject = var.subject
1717
}

0 commit comments

Comments
 (0)