diff --git a/docs/resources/authorization_folder_role_assignment.md b/docs/resources/authorization_folder_role_assignment.md index 9b5707efd..a1495e865 100644 --- a/docs/resources/authorization_folder_role_assignment.md +++ b/docs/resources/authorization_folder_role_assignment.md @@ -43,7 +43,7 @@ import { - `resource_id` (String) folder Resource to assign the role to. - `role` (String) Role to be assigned. Available roles can be queried using stackit-cli: `stackit curl https://authorization.api.stackit.cloud/v2/permissions` -- `subject` (String) Identifier of user, service account or client. Usually email address or name in case of clients +- `subject` (String) Identifier of user, service account or client. Usually email address or name in case of clients. All letters must be lowercased. ### Read-Only diff --git a/docs/resources/authorization_organization_role_assignment.md b/docs/resources/authorization_organization_role_assignment.md index cf965f7ab..46174f1cc 100644 --- a/docs/resources/authorization_organization_role_assignment.md +++ b/docs/resources/authorization_organization_role_assignment.md @@ -36,7 +36,7 @@ import { - `resource_id` (String) organization Resource to assign the role to. - `role` (String) Role to be assigned. Available roles can be queried using stackit-cli: `stackit curl https://authorization.api.stackit.cloud/v2/permissions` -- `subject` (String) Identifier of user, service account or client. Usually email address or name in case of clients +- `subject` (String) Identifier of user, service account or client. Usually email address or name in case of clients. All letters must be lowercased. ### Read-Only diff --git a/docs/resources/authorization_project_role_assignment.md b/docs/resources/authorization_project_role_assignment.md index 9317957ed..6e7bd0c1c 100644 --- a/docs/resources/authorization_project_role_assignment.md +++ b/docs/resources/authorization_project_role_assignment.md @@ -43,7 +43,7 @@ import { - `resource_id` (String) project Resource to assign the role to. - `role` (String) Role to be assigned. Available roles can be queried using stackit-cli: `stackit curl https://authorization.api.stackit.cloud/v2/permissions` -- `subject` (String) Identifier of user, service account or client. Usually email address or name in case of clients +- `subject` (String) Identifier of user, service account or client. Usually email address or name in case of clients. All letters must be lowercased. ### Read-Only diff --git a/stackit/internal/services/authorization/roleassignments/resource.go b/stackit/internal/services/authorization/roleassignments/resource.go index 969da3cde..0925d17c9 100644 --- a/stackit/internal/services/authorization/roleassignments/resource.go +++ b/stackit/internal/services/authorization/roleassignments/resource.go @@ -99,7 +99,7 @@ func (r *roleAssignmentResource) Schema(_ context.Context, _ resource.SchemaRequ "id": "Terraform's internal resource identifier. It is structured as \"`resource_id`,`role`,`subject`\".", "resource_id": fmt.Sprintf("%s Resource to assign the role to.", r.apiName), "role": "Role to be assigned. Available roles can be queried using stackit-cli: `stackit curl https://authorization.api.stackit.cloud/v2/permissions`", - "subject": "Identifier of user, service account or client. Usually email address or name in case of clients", + "subject": "Identifier of user, service account or client. Usually email address or name in case of clients. All letters must be lowercased.", } resp.Schema = schema.Schema{ @@ -133,6 +133,9 @@ func (r *roleAssignmentResource) Schema(_ context.Context, _ resource.SchemaRequ "subject": schema.StringAttribute{ Description: descriptions["subject"], Required: true, + Validators: []validator.String{ + validate.IsLowercased(), + }, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, diff --git a/stackit/internal/validate/validate.go b/stackit/internal/validate/validate.go index 0af0f3c64..929285d9d 100644 --- a/stackit/internal/validate/validate.go +++ b/stackit/internal/validate/validate.go @@ -369,3 +369,27 @@ func ValidNoTrailingNewline() *Validator { }, } } + +// IsLowercased returns a Validator that checks if all letters in the input string are lowercase. +// Examples: +// - "example": valid +// - "example123": valid, numbers are not affected by case +// - "exaMPle123": invalid +func IsLowercased() *Validator { + description := `All letters in the value must be lowercase as defined by gos unicode.IsLower(rune).` + return &Validator{ + description: description, + validate: func(_ context.Context, request validator.StringRequest, response *validator.StringResponse) { + val := request.ConfigValue.ValueString() + lowercased := strings.ToLower(val) + if val != lowercased { + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + request.Path, + description, + val, + )) + return + } + }, + } +} diff --git a/stackit/internal/validate/validate_test.go b/stackit/internal/validate/validate_test.go index 3436a7a1f..c08a70d14 100644 --- a/stackit/internal/validate/validate_test.go +++ b/stackit/internal/validate/validate_test.go @@ -966,3 +966,45 @@ func TestValidNoTrailingNewline(t *testing.T) { }) } } + +func TestIsLowercased(t *testing.T) { + tests := []struct { + description string + input string + wantErr bool + }{ + { + description: "empty", + input: "", + }, + { + description: "simple lowercase", + input: "lowercase", + }, + { + description: "lowercase with numbers and symbols", + input: "lowercase123!@#", + }, + { + description: "uppercase letters", + input: "UpperCase", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + r := validator.StringResponse{} + IsLowercased().ValidateString(context.Background(), validator.StringRequest{ + ConfigValue: types.StringValue(tt.input), + }, &r) + + if tt.wantErr && !r.Diagnostics.HasError() { + t.Fatalf("Expected validation to fail for input: %q", tt.input) + } + if !tt.wantErr && r.Diagnostics.HasError() { + t.Fatalf("Expected validation to succeed for input: %q, but got errors: %v", tt.input, r.Diagnostics.Errors()) + } + }) + } +}