diff --git a/castai/provider.go b/castai/provider.go
index 30c86bc2..4958aa45 100644
--- a/castai/provider.go
+++ b/castai/provider.go
@@ -54,6 +54,7 @@ func Provider(version string) *schema.Provider {
"castai_sso_connection": resourceSSOConnection(),
"castai_workload_scaling_policy": resourceWorkloadScalingPolicy(),
"castai_organization_group": resourceOrganizationGroup(),
+ "castai_role_bindings": resourceRoleBindings(),
},
DataSourcesMap: map[string]*schema.Resource{
diff --git a/castai/resource_role_bindings.go b/castai/resource_role_bindings.go
new file mode 100644
index 00000000..7f4ea89d
--- /dev/null
+++ b/castai/resource_role_bindings.go
@@ -0,0 +1,448 @@
+package castai
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
+ "github.com/samber/lo"
+
+ "github.com/castai/terraform-provider-castai/castai/sdk"
+)
+
+const (
+ FieldRoleBindingsOrganizationID = "organization_id"
+ FieldRoleBindingsName = "name"
+ FieldRoleBindingsDescription = "description"
+ FieldRoleBindingsRoleID = "role_id"
+ FieldRoleBindingsScope = "scope"
+ FieldRoleBindingsScopeKind = "kind"
+ FieldRoleBindingsScopeResourceID = "resource_id"
+ FieldRoleBindingsSubjects = "subjects"
+ FieldRoleBindingsSubject = "subject"
+ FieldRoleBindingsSubjectKind = "kind"
+ FieldRoleBindingsSubjectUserID = "user_id"
+ FieldRoleBindingsSubjectServiceAccountID = "service_account_id"
+ FieldRoleBindingsSubjectGroupID = "group_id"
+
+ RoleBindingScopeKindOrganization = "organization"
+ RoleBindingScopeKindCluster = "cluster"
+
+ RoleBindingSubjectKindUser = "user"
+ RoleBindingSubjectKindServiceAccount = "service_account"
+ RoleBindingSubjectKindGroup = "group"
+)
+
+var (
+ supportedScopeKinds = []string{RoleBindingScopeKindOrganization, RoleBindingScopeKindCluster}
+ supportedSubjectKinds = []string{RoleBindingSubjectKindUser, RoleBindingSubjectKindServiceAccount, RoleBindingSubjectKindGroup}
+)
+
+func resourceRoleBindings() *schema.Resource {
+ return &schema.Resource{
+ ReadContext: resourceRoleBindingsRead,
+ CreateContext: resourceRoleBindingsCreate,
+ UpdateContext: resourceRoleBindingsUpdate,
+ DeleteContext: resourceRoleBindingsDelete,
+ Description: "CAST AI organization group resource to manage organization groups",
+ Importer: &schema.ResourceImporter{
+ StateContext: schema.ImportStatePassthroughContext,
+ },
+ Timeouts: &schema.ResourceTimeout{
+ Create: schema.DefaultTimeout(2 * time.Minute),
+ Update: schema.DefaultTimeout(2 * time.Minute),
+ Delete: schema.DefaultTimeout(2 * time.Minute),
+ },
+ Schema: map[string]*schema.Schema{
+ FieldRoleBindingsOrganizationID: {
+ Type: schema.TypeString,
+ Required: true,
+ ForceNew: true,
+ Description: "CAST AI organization ID.",
+ },
+ FieldRoleBindingsName: {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "Name of role binding.",
+ },
+ FieldRoleBindingsDescription: {
+ Type: schema.TypeString,
+ Optional: true,
+ Computed: true,
+ Description: "Description of the role binding.",
+ },
+ FieldRoleBindingsRoleID: {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "ID of role from role binding.",
+ },
+ FieldRoleBindingsScope: {
+ Type: schema.TypeList,
+ Required: true,
+ MaxItems: 1,
+ Description: "Scope of the role binding.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ FieldRoleBindingsScopeKind: {
+ Type: schema.TypeString,
+ Required: true,
+ Description: fmt.Sprintf("Scope of the role binding Supported values include: %s.", strings.Join(supportedScopeKinds, ", ")),
+ ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice(supportedScopeKinds, true)),
+ DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
+ return strings.EqualFold(oldValue, newValue)
+ },
+ },
+ FieldRoleBindingsScopeResourceID: {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "ID of the scope resource.",
+ },
+ },
+ },
+ },
+ FieldRoleBindingsSubjects: {
+ Type: schema.TypeList,
+ Required: true,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ FieldRoleBindingsSubject: {
+ Type: schema.TypeList,
+ Optional: true,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ FieldRoleBindingsSubjectKind: {
+ Type: schema.TypeString,
+ Required: true,
+ Description: fmt.Sprintf("Kind of the subject. Supported values include: %s.", strings.Join(supportedSubjectKinds, ", ")),
+ ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice(supportedSubjectKinds, true)),
+ DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
+ return strings.EqualFold(oldValue, newValue)
+ },
+ },
+ FieldRoleBindingsSubjectUserID: {
+ Type: schema.TypeString,
+ Optional: true,
+ Computed: true,
+ Description: fmt.Sprintf("Optional, required only if `%s` is `%s`.", FieldRoleBindingsSubjectKind, RoleBindingSubjectKindUser),
+ },
+ FieldRoleBindingsSubjectServiceAccountID: {
+ Type: schema.TypeString,
+ Optional: true,
+ Computed: true,
+ Description: fmt.Sprintf("Optional, required only if `%s` is `%s`.", FieldRoleBindingsSubjectKind, RoleBindingSubjectKindServiceAccount),
+ },
+ FieldRoleBindingsSubjectGroupID: {
+ Type: schema.TypeString,
+ Optional: true,
+ Computed: true,
+ Description: fmt.Sprintf("Optional, required only if `%s` is `%s`.", FieldRoleBindingsSubjectKind, RoleBindingSubjectKindGroup),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func resourceRoleBindingsRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ roleBindingID := data.Id()
+ if roleBindingID == "" {
+ return diag.Errorf("role binding ID is not set")
+ }
+
+ organizationID := data.Get(FieldRoleBindingsOrganizationID).(string)
+ if organizationID == "" {
+ var err error
+ organizationID, err = getDefaultOrganizationId(ctx, meta)
+ if err != nil {
+ return diag.FromErr(fmt.Errorf("getting default organization: %w", err))
+ }
+ }
+
+ client := meta.(*ProviderConfig).api
+
+ roleBinding, err := getRoleBinding(client, ctx, organizationID, roleBindingID)
+ if err != nil {
+ return diag.FromErr(fmt.Errorf("getting role binding for read: %w", err))
+ }
+
+ if err := assignRoleBindingData(roleBinding, data); err != nil {
+ return diag.FromErr(fmt.Errorf("assigning role binding data for read: %w", err))
+ }
+
+ return nil
+}
+
+func resourceRoleBindingsCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ organizationID := data.Get(FieldRoleBindingsOrganizationID).(string)
+ if organizationID == "" {
+ var err error
+ organizationID, err = getDefaultOrganizationId(ctx, meta)
+ if err != nil {
+ return diag.FromErr(fmt.Errorf("getting default organization: %w", err))
+ }
+ }
+
+ client := meta.(*ProviderConfig).api
+
+ subjects, err := convertSubjectsToSDK(data)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ scope := convertScopeToSDK(data)
+
+ resp, err := client.RbacServiceAPICreateRoleBindingsWithResponse(ctx, organizationID, sdk.RbacServiceAPICreateRoleBindingsJSONRequestBody{
+ {
+ Definition: sdk.CastaiRbacV1beta1RoleBindingDefinition{
+ RoleId: data.Get(FieldRoleBindingsRoleID).(string),
+ Scope: scope,
+ Subjects: &subjects,
+ },
+ Description: lo.ToPtr(data.Get(FieldRoleBindingsDescription).(string)),
+ Name: data.Get(FieldRoleBindingsName).(string),
+ },
+ })
+
+ if err := sdk.CheckOKResponse(resp, err); err != nil {
+ return diag.FromErr(fmt.Errorf("create role binding: %w", err))
+ }
+
+ if len(*resp.JSON200) == 0 {
+ return diag.FromErr(errors.New("unknown error with creating role binding"))
+ }
+
+ data.SetId(*(*resp.JSON200)[0].Id)
+
+ return resourceRoleBindingsRead(ctx, data, meta)
+}
+
+func resourceRoleBindingsUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ roleBindingID := data.Id()
+ if roleBindingID == "" {
+ return diag.Errorf("role binding ID is not set")
+ }
+
+ organizationID := data.Get(FieldRoleBindingsOrganizationID).(string)
+ if organizationID == "" {
+ var err error
+ organizationID, err = getDefaultOrganizationId(ctx, meta)
+ if err != nil {
+ return diag.FromErr(fmt.Errorf("getting default organization: %w", err))
+ }
+ }
+
+ client := meta.(*ProviderConfig).api
+
+ subjects, err := convertSubjectsToSDK(data)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ scope := convertScopeToSDK(data)
+
+ resp, err := client.RbacServiceAPIUpdateRoleBindingWithResponse(ctx, organizationID, roleBindingID, sdk.RbacServiceAPIUpdateRoleBindingJSONRequestBody{
+ Definition: sdk.CastaiRbacV1beta1RoleBindingDefinition{
+ RoleId: data.Get(FieldRoleBindingsRoleID).(string),
+ Scope: scope,
+ Subjects: &subjects,
+ },
+ Description: lo.ToPtr(data.Get(FieldRoleBindingsDescription).(string)),
+ Name: data.Get(FieldRoleBindingsName).(string),
+ })
+
+ if err := sdk.CheckOKResponse(resp, err); err != nil {
+ return diag.FromErr(fmt.Errorf("update role binding: %w", err))
+ }
+
+ return resourceRoleBindingsRead(ctx, data, meta)
+}
+
+func resourceRoleBindingsDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ roleBindingID := data.Id()
+ if roleBindingID == "" {
+ return diag.Errorf("role binding ID is not set")
+ }
+
+ organizationID := data.Get(FieldRoleBindingsOrganizationID).(string)
+ if organizationID == "" {
+ var err error
+ organizationID, err = getDefaultOrganizationId(ctx, meta)
+ if err != nil {
+ return diag.FromErr(fmt.Errorf("getting default organization: %w", err))
+ }
+ }
+
+ client := meta.(*ProviderConfig).api
+
+ resp, err := client.RbacServiceAPIDeleteRoleBindingWithResponse(ctx, organizationID, roleBindingID)
+ if err := sdk.CheckOKResponse(resp, err); err != nil {
+ return diag.FromErr(fmt.Errorf("destroy role binding: %w", err))
+ }
+
+ return nil
+}
+
+func getRoleBinding(client *sdk.ClientWithResponses, ctx context.Context, organizationID, roleBindingID string) (*sdk.CastaiRbacV1beta1RoleBinding, error) {
+ resp, err := client.RbacServiceAPIGetRoleBindingWithResponse(ctx, organizationID, roleBindingID)
+ if err != nil {
+ return nil, fmt.Errorf("fetching role binding: %w", err)
+ }
+ if resp.StatusCode() == http.StatusNotFound {
+ return nil, fmt.Errorf("role binding %s not found", roleBindingID)
+ }
+ if err := sdk.CheckOKResponse(resp, err); err != nil {
+ return nil, fmt.Errorf("retrieving role binding: %w", err)
+ }
+ if resp.JSON200 == nil {
+ return nil, fmt.Errorf("role binding %s not found", roleBindingID)
+ }
+ return resp.JSON200, nil
+}
+
+func assignRoleBindingData(roleBinding *sdk.CastaiRbacV1beta1RoleBinding, data *schema.ResourceData) error {
+ if err := data.Set(FieldRoleBindingsOrganizationID, roleBinding.OrganizationId); err != nil {
+ return fmt.Errorf("setting organization_id: %w", err)
+ }
+ if err := data.Set(FieldRoleBindingsDescription, roleBinding.Description); err != nil {
+ return fmt.Errorf("setting description: %w", err)
+ }
+ if err := data.Set(FieldRoleBindingsName, roleBinding.Name); err != nil {
+ return fmt.Errorf("setting role binding name: %w", err)
+ }
+ if err := data.Set(FieldRoleBindingsRoleID, roleBinding.Definition.RoleId); err != nil {
+ return fmt.Errorf("setting role binding role id: %w", err)
+ }
+
+ if roleBinding.Definition.Scope.Organization != nil {
+ err := data.Set(FieldRoleBindingsScope, []any{
+ map[string]any{
+ FieldRoleBindingsScopeKind: RoleBindingScopeKindOrganization,
+ FieldRoleBindingsScopeResourceID: roleBinding.Definition.Scope.Organization.Id,
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("parsing scope organization: %w", err)
+ }
+ } else if roleBinding.Definition.Scope.Cluster != nil {
+ err := data.Set(FieldRoleBindingsScope, []any{
+ map[string]any{
+ FieldRoleBindingsScopeKind: RoleBindingScopeKindCluster,
+ FieldRoleBindingsScopeResourceID: roleBinding.Definition.Scope.Cluster.Id,
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("parsing scope cluster: %w", err)
+ }
+ }
+
+ if roleBinding.Definition.Subjects != nil {
+ var subjects []map[string]string
+ for _, subject := range *roleBinding.Definition.Subjects {
+
+ if subject.User != nil {
+ subjects = append(subjects, map[string]string{
+ FieldRoleBindingsSubjectKind: RoleBindingSubjectKindUser,
+ FieldRoleBindingsSubjectUserID: subject.User.Id,
+ })
+ } else if subject.Group != nil {
+ subjects = append(subjects, map[string]string{
+ FieldRoleBindingsSubjectKind: RoleBindingSubjectKindGroup,
+ FieldRoleBindingsSubjectGroupID: subject.Group.Id,
+ })
+ } else if subject.ServiceAccount != nil {
+ subjects = append(subjects, map[string]string{
+ FieldRoleBindingsSubjectKind: RoleBindingSubjectKindServiceAccount,
+ FieldRoleBindingsSubjectServiceAccountID: subject.ServiceAccount.Id,
+ })
+ }
+ }
+ err := data.Set(FieldRoleBindingsSubjects, []any{
+ map[string]any{
+ FieldRoleBindingsSubject: subjects,
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("parsing roleBinding subjects: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func convertScopeToSDK(data *schema.ResourceData) sdk.CastaiRbacV1beta1Scope {
+ scopes := data.Get(FieldRoleBindingsScope).([]any)
+ if len(scopes) == 0 {
+ return sdk.CastaiRbacV1beta1Scope{}
+ }
+
+ scope := scopes[0].(map[string]any)
+
+ switch scope[FieldRoleBindingsScopeKind].(string) {
+ case RoleBindingScopeKindOrganization:
+ return sdk.CastaiRbacV1beta1Scope{
+ Organization: &sdk.CastaiRbacV1beta1OrganizationScope{
+ Id: scope[FieldRoleBindingsScopeResourceID].(string),
+ },
+ }
+ case RoleBindingScopeKindCluster:
+ return sdk.CastaiRbacV1beta1Scope{
+ Cluster: &sdk.CastaiRbacV1beta1ClusterScope{
+ Id: scope[FieldRoleBindingsScopeResourceID].(string),
+ },
+ }
+ default:
+ return sdk.CastaiRbacV1beta1Scope{}
+ }
+}
+
+func convertSubjectsToSDK(data *schema.ResourceData) ([]sdk.CastaiRbacV1beta1Subject, error) {
+ var subjects []sdk.CastaiRbacV1beta1Subject
+
+ for _, dataSubjectsDef := range data.Get(FieldRoleBindingsSubjects).([]any) {
+ for i, dataSubject := range dataSubjectsDef.(map[string]any)[FieldRoleBindingsSubject].([]any) {
+
+ switch dataSubject.(map[string]any)[FieldRoleBindingsSubjectKind].(string) {
+ case RoleBindingSubjectKindUser:
+ if dataSubject.(map[string]any)[FieldRoleBindingsSubjectUserID].(string) == "" {
+ return nil, fmt.Errorf("missing `%s` value for subject no. %d", FieldRoleBindingsSubjectUserID, i)
+ }
+
+ subjects = append(subjects, sdk.CastaiRbacV1beta1Subject{
+ User: &sdk.CastaiRbacV1beta1UserSubject{
+ Id: dataSubject.(map[string]any)[FieldRoleBindingsSubjectUserID].(string),
+ },
+ })
+ case RoleBindingSubjectKindServiceAccount:
+ if dataSubject.(map[string]any)[FieldRoleBindingsSubjectServiceAccountID].(string) == "" {
+ return nil, fmt.Errorf("missing `%s` value for subject no. %d", FieldRoleBindingsSubjectServiceAccountID, i)
+ }
+
+ subjects = append(subjects, sdk.CastaiRbacV1beta1Subject{
+ ServiceAccount: &sdk.CastaiRbacV1beta1ServiceAccountSubject{
+ Id: dataSubject.(map[string]any)[FieldRoleBindingsSubjectServiceAccountID].(string),
+ },
+ })
+ case RoleBindingSubjectKindGroup:
+ if dataSubject.(map[string]any)[FieldRoleBindingsSubjectGroupID].(string) == "" {
+ return nil, fmt.Errorf("missing `%s` value for subject no. %d", FieldRoleBindingsSubjectGroupID, i)
+ }
+
+ subjects = append(subjects, sdk.CastaiRbacV1beta1Subject{
+ Group: &sdk.CastaiRbacV1beta1GroupSubject{
+ Id: dataSubject.(map[string]any)[FieldRoleBindingsSubjectGroupID].(string),
+ },
+ })
+ }
+ }
+ }
+
+ return subjects, nil
+}
diff --git a/castai/resource_role_bindings_test.go b/castai/resource_role_bindings_test.go
new file mode 100644
index 00000000..e56c99bb
--- /dev/null
+++ b/castai/resource_role_bindings_test.go
@@ -0,0 +1,614 @@
+package castai
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/google/uuid"
+ "github.com/hashicorp/go-cty/cty"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+ "github.com/samber/lo"
+ "github.com/stretchr/testify/require"
+
+ "github.com/castai/terraform-provider-castai/castai/sdk"
+ mock_sdk "github.com/castai/terraform-provider-castai/castai/sdk/mock"
+)
+
+func TestRoleBindingsReadContext(t *testing.T) {
+ t.Parallel()
+
+ t.Run("when state is missing role binding ID then return error", func(t *testing.T) {
+ t.Parallel()
+ r := require.New(t)
+
+ ctx := context.Background()
+ provider := &ProviderConfig{}
+
+ stateValue := cty.ObjectVal(map[string]cty.Value{})
+ state := terraform.NewInstanceStateShimmedFromValue(stateValue, 0)
+
+ resource := resourceRoleBindings()
+ data := resource.Data(state)
+
+ result := resource.ReadContext(ctx, data, provider)
+
+ r.NotNil(result)
+ r.True(result.HasError())
+ r.Len(result, 1)
+ r.Equal("role binding ID is not set", result[0].Summary)
+ })
+
+ t.Run("when RbacServiceAPI respond with 404 then return error", func(t *testing.T) {
+ t.Parallel()
+ r := require.New(t)
+ mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t))
+
+ ctx := context.Background()
+ provider := &ProviderConfig{
+ api: &sdk.ClientWithResponses{
+ ClientInterface: mockClient,
+ },
+ }
+
+ organizationID := uuid.NewString()
+ roleBindingID := uuid.NewString()
+
+ body := io.NopCloser(bytes.NewReader([]byte("")))
+
+ mockClient.EXPECT().
+ RbacServiceAPIGetRoleBinding(gomock.Any(), organizationID, roleBindingID).
+ Return(&http.Response{StatusCode: http.StatusNotFound, Body: body, Header: map[string][]string{"Content-Type": {"json"}}}, nil)
+
+ stateValue := cty.ObjectVal(map[string]cty.Value{
+ "organization_id": cty.StringVal(organizationID),
+ })
+ state := terraform.NewInstanceStateShimmedFromValue(stateValue, 0)
+ state.ID = roleBindingID
+
+ resource := resourceRoleBindings()
+ data := resource.Data(state)
+
+ result := resource.ReadContext(ctx, data, provider)
+ r.NotNil(result)
+ r.True(result.HasError())
+ r.Len(result, 1)
+ r.Equal("getting role binding for read: role binding "+roleBindingID+" not found", result[0].Summary)
+ })
+
+ t.Run("when RbacServiceAPI respond with 500 then return error", func(t *testing.T) {
+ t.Parallel()
+ r := require.New(t)
+ mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t))
+
+ ctx := context.Background()
+ provider := &ProviderConfig{
+ api: &sdk.ClientWithResponses{
+ ClientInterface: mockClient,
+ },
+ }
+
+ organizationID := uuid.NewString()
+ roleBindingID := uuid.NewString()
+
+ body := io.NopCloser(bytes.NewReader([]byte("internal error")))
+
+ mockClient.EXPECT().
+ RbacServiceAPIGetRoleBinding(gomock.Any(), organizationID, roleBindingID).
+ Return(&http.Response{StatusCode: http.StatusInternalServerError, Body: body, Header: map[string][]string{"Content-Type": {"json"}}}, nil)
+
+ stateValue := cty.ObjectVal(map[string]cty.Value{
+ "organization_id": cty.StringVal(organizationID),
+ })
+ state := terraform.NewInstanceStateShimmedFromValue(stateValue, 0)
+ state.ID = roleBindingID
+
+ resource := resourceRoleBindings()
+ data := resource.Data(state)
+
+ result := resource.ReadContext(ctx, data, provider)
+ r.NotNil(result)
+ r.True(result.HasError())
+ r.Len(result, 1)
+ r.Equal("getting role binding for read: retrieving role binding: expected status code 200, received: status=500 body=internal error", result[0].Summary)
+ })
+
+ t.Run("when calling RbacServiceAPI throws error then return error", func(t *testing.T) {
+ t.Parallel()
+ r := require.New(t)
+ mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t))
+
+ ctx := context.Background()
+ provider := &ProviderConfig{
+ api: &sdk.ClientWithResponses{
+ ClientInterface: mockClient,
+ },
+ }
+
+ organizationID := uuid.NewString()
+ roleBindingID := uuid.NewString()
+
+ mockClient.EXPECT().
+ RbacServiceAPIGetRoleBinding(gomock.Any(), organizationID, roleBindingID).
+ Return(nil, errors.New("unexpected error"))
+
+ stateValue := cty.ObjectVal(map[string]cty.Value{
+ "organization_id": cty.StringVal(organizationID),
+ })
+ state := terraform.NewInstanceStateShimmedFromValue(stateValue, 0)
+ state.ID = roleBindingID
+
+ resource := resourceRoleBindings()
+ data := resource.Data(state)
+
+ result := resource.ReadContext(ctx, data, provider)
+ r.NotNil(result)
+ r.True(result.HasError())
+ r.Len(result, 1)
+ r.Equal("getting role binding for read: fetching role binding: unexpected error", result[0].Summary)
+ })
+
+ t.Run("when RbacServiceAPI respond with 200 then populate the state", func(t *testing.T) {
+ t.Parallel()
+
+ r := require.New(t)
+ mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t))
+
+ ctx := context.Background()
+ provider := &ProviderConfig{
+ api: &sdk.ClientWithResponses{
+ ClientInterface: mockClient,
+ },
+ }
+
+ organizationID := "4e4cd9eb-82eb-407e-a926-e5fef81cab50"
+ roleBindingID := "a83b7bf2-5a99-45d9-bcac-b969386e751f"
+ roleID := "4df39779-dfb2-48d3-91d8-7ee5bd2bca4b"
+ userID := "671b2ebb-f361-42f0-aa2f-3049de93f8c1"
+ serviceAccountID := "b11f5945-22ca-4101-a86e-d6e37f44a415"
+ groupID := "844d2bf2-870d-42da-a81c-4e19befc78fc"
+
+ body := io.NopCloser(bytes.NewReader([]byte(`{
+ "id": "` + roleBindingID + `",
+ "organizationId": "` + organizationID + `",
+ "name": "role-binding-name",
+ "description": "role-binding-description",
+ "definition": {
+ "roleId": "` + roleID + `",
+ "scope": {
+ "organization": {
+ "id": "` + organizationID + `"
+ }
+ },
+ "subjects": [
+ {
+ "user": {
+ "id": "` + userID + `"
+ }
+ },
+ {
+ "serviceAccount": {
+ "id": "` + serviceAccountID + `"
+ }
+ },
+ {
+ "group": {
+ "id": "` + groupID + `"
+ }
+ }
+ ]
+ }
+}`)))
+
+ mockClient.EXPECT().
+ RbacServiceAPIGetRoleBinding(gomock.Any(), organizationID, roleBindingID).
+ Return(&http.Response{StatusCode: 200, Body: body, Header: map[string][]string{"Content-Type": {"json"}}}, nil)
+
+ stateValue := cty.ObjectVal(map[string]cty.Value{
+ "organization_id": cty.StringVal(organizationID),
+ })
+ state := terraform.NewInstanceStateShimmedFromValue(stateValue, 0)
+ state.ID = roleBindingID
+
+ resource := resourceRoleBindings()
+ data := resource.Data(state)
+
+ result := resource.ReadContext(ctx, data, provider)
+
+ r.Nil(result)
+ r.False(result.HasError())
+ r.Equal(`ID = `+roleBindingID+`
+description = role-binding-description
+name = role-binding-name
+organization_id = `+organizationID+`
+role_id = `+roleID+`
+scope.# = 1
+scope.0.kind = organization
+scope.0.resource_id = `+organizationID+`
+subjects.# = 1
+subjects.0.subject.# = 3
+subjects.0.subject.0.group_id =
+subjects.0.subject.0.kind = user
+subjects.0.subject.0.service_account_id =
+subjects.0.subject.0.user_id = `+userID+`
+subjects.0.subject.1.group_id =
+subjects.0.subject.1.kind = service_account
+subjects.0.subject.1.service_account_id = `+serviceAccountID+`
+subjects.0.subject.1.user_id =
+subjects.0.subject.2.group_id = `+groupID+`
+subjects.0.subject.2.kind = group
+subjects.0.subject.2.service_account_id =
+subjects.0.subject.2.user_id =
+Tainted = false
+`, data.State().String())
+ })
+}
+
+func TestRoleBindingsUpdateContext(t *testing.T) {
+ t.Parallel()
+
+ t.Run("when state is missing role binding ID then return error", func(t *testing.T) {
+ t.Parallel()
+ r := require.New(t)
+
+ ctx := context.Background()
+ provider := &ProviderConfig{}
+
+ stateValue := cty.ObjectVal(map[string]cty.Value{})
+ state := terraform.NewInstanceStateShimmedFromValue(stateValue, 0)
+
+ resource := resourceRoleBindings()
+ data := resource.Data(state)
+
+ result := resource.UpdateContext(ctx, data, provider)
+
+ r.NotNil(result)
+ r.True(result.HasError())
+ r.Len(result, 1)
+ r.Equal("role binding ID is not set", result[0].Summary)
+ })
+
+ t.Run("when RbacServiceAPI UpdateRoleBinding respond with 500 then throw error", func(t *testing.T) {
+ t.Parallel()
+ r := require.New(t)
+ mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t))
+
+ ctx := context.Background()
+ provider := &ProviderConfig{
+ api: &sdk.ClientWithResponses{
+ ClientInterface: mockClient,
+ },
+ }
+
+ organizationID := uuid.NewString()
+ roleBindingID := uuid.NewString()
+
+ mockClient.EXPECT().
+ RbacServiceAPIUpdateRoleBinding(gomock.Any(), organizationID, roleBindingID, gomock.Any()).
+ DoAndReturn(func(ctx context.Context, reqOrgID string, reqRoleBindingID string, req sdk.RbacServiceAPIUpdateRoleBindingJSONRequestBody) (*http.Response, error) {
+ r.Equal(organizationID, reqOrgID)
+ r.Equal(roleBindingID, reqRoleBindingID)
+
+ body := &bytes.Buffer{}
+ err := json.NewEncoder(body).Encode(&sdk.CastaiRbacV1beta1RoleBinding{})
+ r.NoError(err)
+ return &http.Response{StatusCode: http.StatusInternalServerError, Body: io.NopCloser(body), Header: map[string][]string{"Content-Type": {"json"}}}, nil
+ })
+
+ stateValue := cty.ObjectVal(map[string]cty.Value{
+ "organization_id": cty.StringVal(organizationID),
+ })
+ state := terraform.NewInstanceStateShimmedFromValue(stateValue, 0)
+ state.ID = roleBindingID
+
+ resource := resourceRoleBindings()
+ data := resource.Data(state)
+
+ result := resource.UpdateContext(ctx, data, provider)
+
+ r.NotNil(result)
+ r.True(result.HasError())
+ r.Len(result, 1)
+ r.Contains(result[0].Summary, "update role binding: expected status code 200, received: status=500")
+ })
+
+ t.Run("when RbacServiceAPI UpdateRoleBinding respond with 200 then no errors", func(t *testing.T) {
+ t.Parallel()
+ r := require.New(t)
+ mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t))
+
+ ctx := context.Background()
+ provider := &ProviderConfig{
+ api: &sdk.ClientWithResponses{
+ ClientInterface: mockClient,
+ },
+ }
+
+ organizationID := uuid.NewString()
+ roleBindingID := uuid.NewString()
+
+ firstUserID := uuid.NewString()
+ secondUserID := uuid.NewString()
+
+ body := io.NopCloser(bytes.NewReader([]byte(`{
+ "id": "` + roleBindingID + `",
+ "organizationId": "` + organizationID + `",
+ "name": "test group",
+ "description": "test role binding description changed",
+ "definition": {
+ "members": [
+ {
+ "id": "` + firstUserID + `",
+ "email": "test-user-1@test.com"
+ },
+ {
+ "id": "` + secondUserID + `",
+ "email": "test-user-2@test.com"
+ }
+ ]
+ }
+ }`)))
+
+ mockClient.EXPECT().
+ RbacServiceAPIGetRoleBinding(gomock.Any(), organizationID, roleBindingID).
+ Return(&http.Response{StatusCode: http.StatusOK, Body: body, Header: map[string][]string{"Content-Type": {"json"}}}, nil)
+
+ mockClient.EXPECT().
+ RbacServiceAPIUpdateRoleBinding(gomock.Any(), organizationID, roleBindingID, gomock.Any()).
+ DoAndReturn(func(ctx context.Context, reqOrgID string, reqRoleBindingID string, req sdk.RbacServiceAPIUpdateRoleBindingJSONRequestBody) (*http.Response, error) {
+ r.Equal(organizationID, reqOrgID)
+ r.Equal(roleBindingID, reqRoleBindingID)
+
+ body := &bytes.Buffer{}
+ err := json.NewEncoder(body).Encode(&sdk.CastaiRbacV1beta1RoleBinding{})
+ r.NoError(err)
+ return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(body), Header: map[string][]string{"Content-Type": {"json"}}}, nil
+ })
+
+ stateValue := cty.ObjectVal(map[string]cty.Value{
+ "organization_id": cty.StringVal(organizationID),
+ })
+ state := terraform.NewInstanceStateShimmedFromValue(stateValue, 0)
+ state.ID = roleBindingID
+
+ resource := resourceRoleBindings()
+ data := resource.Data(state)
+
+ result := resource.UpdateContext(ctx, data, provider)
+
+ r.Nil(result)
+ r.False(result.HasError())
+ })
+}
+
+func TestRoleBindingsCreateContext(t *testing.T) {
+ t.Parallel()
+
+ t.Run("when RbacServiceAPI CreateRoleBindings respond with 500 then throw error", func(t *testing.T) {
+ t.Parallel()
+ r := require.New(t)
+ mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t))
+
+ ctx := context.Background()
+ provider := &ProviderConfig{
+ api: &sdk.ClientWithResponses{
+ ClientInterface: mockClient,
+ },
+ }
+
+ organizationID := uuid.NewString()
+
+ mockClient.EXPECT().
+ RbacServiceAPICreateRoleBindings(gomock.Any(), organizationID, gomock.Any()).
+ DoAndReturn(func(ctx context.Context, reqOrgID string, req sdk.RbacServiceAPICreateRoleBindingsJSONRequestBody) (*http.Response, error) {
+ r.Equal(organizationID, reqOrgID)
+
+ body := &bytes.Buffer{}
+ err := json.NewEncoder(body).Encode(&sdk.CastaiRbacV1beta1RoleBinding{})
+ r.NoError(err)
+ return &http.Response{StatusCode: http.StatusInternalServerError, Body: io.NopCloser(body), Header: map[string][]string{"Content-Type": {"json"}}}, nil
+ })
+
+ stateValue := cty.ObjectVal(map[string]cty.Value{
+ "organization_id": cty.StringVal(organizationID),
+ })
+ state := terraform.NewInstanceStateShimmedFromValue(stateValue, 0)
+
+ resource := resourceRoleBindings()
+ data := resource.Data(state)
+
+ result := resource.CreateContext(ctx, data, provider)
+
+ r.NotNil(result)
+ r.True(result.HasError())
+ r.Len(result, 1)
+ r.Contains(result[0].Summary, "create role binding: expected status code 200, received: status=500")
+ })
+
+ t.Run("when RbacServiceAPI CreateRoleBindings respond with 200 then assume role binding was created", func(t *testing.T) {
+ t.Parallel()
+ r := require.New(t)
+ mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t))
+
+ ctx := context.Background()
+ provider := &ProviderConfig{
+ api: &sdk.ClientWithResponses{
+ ClientInterface: mockClient,
+ },
+ }
+
+ organizationID := uuid.NewString()
+ roleBindingID := uuid.NewString()
+
+ firstUserID := uuid.NewString()
+ secondUserID := uuid.NewString()
+
+ body := io.NopCloser(bytes.NewReader([]byte(`{
+ "id": "` + roleBindingID + `",
+ "organizationId": "` + organizationID + `",
+ "name": "test role binding",
+ "description": "test role binding description changed",
+ "definition": {
+ "members": [
+ {
+ "id": "` + firstUserID + `",
+ "email": "test-user-1@test.com"
+ },
+ {
+ "id": "` + secondUserID + `",
+ "email": "test-user-2@test.com"
+ }
+ ]
+ }
+ }`)))
+
+ mockClient.EXPECT().
+ RbacServiceAPIGetRoleBinding(gomock.Any(), organizationID, roleBindingID).
+ Return(&http.Response{StatusCode: http.StatusOK, Body: body, Header: map[string][]string{"Content-Type": {"json"}}}, nil)
+
+ mockClient.EXPECT().
+ RbacServiceAPICreateRoleBindings(gomock.Any(), organizationID, gomock.Any()).
+ DoAndReturn(func(ctx context.Context, reqOrgID string, req sdk.RbacServiceAPICreateRoleBindingsJSONRequestBody) (*http.Response, error) {
+ r.Equal(organizationID, reqOrgID)
+
+ body := bytes.NewBuffer([]byte(""))
+ err := json.NewEncoder(body).Encode(&[]sdk.CastaiRbacV1beta1RoleBinding{
+ {
+ Id: lo.ToPtr(roleBindingID),
+ },
+ })
+ r.NoError(err)
+ return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(body), Header: map[string][]string{"Content-Type": {"json"}}}, nil
+ })
+
+ stateValue := cty.ObjectVal(map[string]cty.Value{
+ "organization_id": cty.StringVal(organizationID),
+ })
+ state := terraform.NewInstanceStateShimmedFromValue(stateValue, 0)
+ state.ID = roleBindingID
+
+ resource := resourceRoleBindings()
+ data := resource.Data(state)
+
+ result := resource.CreateContext(ctx, data, provider)
+
+ r.Nil(result)
+ r.False(result.HasError())
+ })
+}
+
+func TestRoleBindingsDeleteContext(t *testing.T) {
+ t.Parallel()
+
+ t.Run("when state is missing role binding ID then return error", func(t *testing.T) {
+ t.Parallel()
+ r := require.New(t)
+
+ ctx := context.Background()
+ provider := &ProviderConfig{}
+
+ stateValue := cty.ObjectVal(map[string]cty.Value{})
+ state := terraform.NewInstanceStateShimmedFromValue(stateValue, 0)
+
+ resource := resourceRoleBindings()
+ data := resource.Data(state)
+
+ result := resource.DeleteContext(ctx, data, provider)
+
+ r.NotNil(result)
+ r.True(result.HasError())
+ r.Len(result, 1)
+ r.Equal("role binding ID is not set", result[0].Summary)
+ })
+
+ t.Run("when RbacServiceAPI DeleteRoleBinding respond with 500 then throw error", func(t *testing.T) {
+ t.Parallel()
+ r := require.New(t)
+ mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t))
+
+ ctx := context.Background()
+ provider := &ProviderConfig{
+ api: &sdk.ClientWithResponses{
+ ClientInterface: mockClient,
+ },
+ }
+
+ organizationID := uuid.NewString()
+ roleBindingID := uuid.NewString()
+
+ mockClient.EXPECT().
+ RbacServiceAPIDeleteRoleBinding(gomock.Any(), organizationID, roleBindingID, gomock.Any()).
+ DoAndReturn(func(ctx context.Context, reqOrgID string, reqRoleBindingID string) (*http.Response, error) {
+ r.Equal(organizationID, reqOrgID)
+ r.Equal(roleBindingID, reqRoleBindingID)
+
+ body := &bytes.Buffer{}
+ err := json.NewEncoder(body).Encode(&sdk.CastaiRbacV1beta1RoleBinding{})
+ r.NoError(err)
+ return &http.Response{StatusCode: http.StatusInternalServerError, Body: io.NopCloser(body), Header: map[string][]string{"Content-Type": {"json"}}}, nil
+ })
+
+ stateValue := cty.ObjectVal(map[string]cty.Value{
+ "organization_id": cty.StringVal(organizationID),
+ })
+ state := terraform.NewInstanceStateShimmedFromValue(stateValue, 0)
+ state.ID = roleBindingID
+
+ resource := resourceRoleBindings()
+ data := resource.Data(state)
+
+ result := resource.DeleteContext(ctx, data, provider)
+
+ r.NotNil(result)
+ r.True(result.HasError())
+ r.Len(result, 1)
+ r.Contains(result[0].Summary, "destroy role binding: expected status code 200, received: status=500")
+ })
+
+ t.Run("when RbacServiceAPI DeleteRoleBinding respond with 200 then no errors", func(t *testing.T) {
+ t.Parallel()
+ r := require.New(t)
+ mockClient := mock_sdk.NewMockClientInterface(gomock.NewController(t))
+
+ ctx := context.Background()
+ provider := &ProviderConfig{
+ api: &sdk.ClientWithResponses{
+ ClientInterface: mockClient,
+ },
+ }
+
+ organizationID := uuid.NewString()
+ roleBindingID := uuid.NewString()
+
+ mockClient.EXPECT().
+ RbacServiceAPIDeleteRoleBinding(gomock.Any(), organizationID, roleBindingID, gomock.Any()).
+ DoAndReturn(func(ctx context.Context, reqOrgID string, reqRoleBindingID string) (*http.Response, error) {
+ r.Equal(organizationID, reqOrgID)
+ r.Equal(roleBindingID, reqRoleBindingID)
+
+ body := &bytes.Buffer{}
+ err := json.NewEncoder(body).Encode(&sdk.CastaiRbacV1beta1RoleBinding{})
+ r.NoError(err)
+ return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(body), Header: map[string][]string{"Content-Type": {"json"}}}, nil
+ })
+
+ stateValue := cty.ObjectVal(map[string]cty.Value{
+ "organization_id": cty.StringVal(organizationID),
+ })
+ state := terraform.NewInstanceStateShimmedFromValue(stateValue, 0)
+ state.ID = roleBindingID
+
+ resource := resourceRoleBindings()
+ data := resource.Data(state)
+
+ result := resource.DeleteContext(ctx, data, provider)
+
+ r.Nil(result)
+ r.False(result.HasError())
+ })
+}
diff --git a/docs/resources/role_bindings.md b/docs/resources/role_bindings.md
new file mode 100644
index 00000000..1c820e26
--- /dev/null
+++ b/docs/resources/role_bindings.md
@@ -0,0 +1,75 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "castai_role_bindings Resource - terraform-provider-castai"
+subcategory: ""
+description: |-
+ CAST AI organization group resource to manage organization groups
+---
+
+# castai_role_bindings (Resource)
+
+CAST AI organization group resource to manage organization groups
+
+
+
+
+## Schema
+
+### Required
+
+- `name` (String) Name of role binding.
+- `organization_id` (String) CAST AI organization ID.
+- `role_id` (String) ID of role from role binding.
+- `scope` (Block List, Min: 1, Max: 1) Scope of the role binding. (see [below for nested schema](#nestedblock--scope))
+- `subjects` (Block List, Min: 1) (see [below for nested schema](#nestedblock--subjects))
+
+### Optional
+
+- `description` (String) Description of the role binding.
+- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts))
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+
+
+### Nested Schema for `scope`
+
+Required:
+
+- `kind` (String) Scope of the role binding Supported values include: organization, cluster.
+- `resource_id` (String) ID of the scope resource.
+
+
+
+### Nested Schema for `subjects`
+
+Optional:
+
+- `subject` (Block List) (see [below for nested schema](#nestedblock--subjects--subject))
+
+
+### Nested Schema for `subjects.subject`
+
+Required:
+
+- `kind` (String) Kind of the subject. Supported values include: user, service_account, group.
+
+Optional:
+
+- `group_id` (String) Optional, required only if `kind` is `group`.
+- `service_account_id` (String) Optional, required only if `kind` is `service_account`.
+- `user_id` (String) Optional, required only if `kind` is `user`.
+
+
+
+
+### Nested Schema for `timeouts`
+
+Optional:
+
+- `create` (String)
+- `delete` (String)
+- `update` (String)
+
+
diff --git a/examples/role_bindings/main.tf b/examples/role_bindings/main.tf
new file mode 100644
index 00000000..672adac0
--- /dev/null
+++ b/examples/role_bindings/main.tf
@@ -0,0 +1,60 @@
+terraform {
+ required_providers {
+ castai = {
+ source = "castai/castai"
+ }
+ }
+ required_version = ">= 0.13"
+}
+
+data "castai_organization" "test" {
+ name = "My test organization name"
+}
+
+resource "castai_role_bindings" "owner_test" {
+ organization_id = data.castai_organization.test.id
+ name = "Role binding owner"
+ description = "Owner access for whole organization."
+
+ role_id = "3e1050c7-6593-4298-94bb-154637911d78" # Role "Owner"
+ scope {
+ kind = "organization"
+ resource_id = data.castai_organization.test.id
+ }
+ subjects {
+ subject {
+ kind = "user"
+ user_id = "21c133e2-a899-4f51-b297-830bc62e51d6" # user x
+ }
+ subject {
+ kind = "user"
+ user_id = "0d1efe35-7ecb-4821-a52d-fd56c9710a64" # user y
+ }
+ subject {
+ kind = "group"
+ group_id = "651734a7-0d0c-49f3-9654-dd92175febaa"
+ }
+ subject {
+ kind = "service_account"
+ service_account_id = "3bf49513-3e9c-4a12-962c-af3bb1a85074"
+ }
+ }
+}
+
+resource "castai_role_bindings" "viewer_test" {
+ organization_id = data.castai_organization.test.id
+ name = "Role binding viewer for cluster 7063d31c-897e-48ef-a322-bdfda6fdbcfb"
+ description = "Viewer access for on of the clusters."
+
+ role_id = "6fc95bd7-6049-4735-80b0-ce5ccde71cb1" # Role "Viewer"
+ scope {
+ kind = "cluster"
+ resource_id = "7063d31c-897e-48ef-a322-bdfda6fdbcfb"
+ }
+ subjects {
+ subject {
+ kind = "user"
+ user_id = "21c133e2-a899-4f51-b297-830bc62e51d6" # user z
+ }
+ }
+}
\ No newline at end of file