Skip to content

Commit 0570395

Browse files
authored
feat(rbac): support RBAC resource restriction on rolebinding (#133)
### Motivation Support RBAC resource restriction on rolebinding ### Examples ```hcl resource "streamnative_rolebinding" "rb_resource_name_restriction" { name = "rb_resource_name_restriction" organization = "o-y8z75" cluster_role_name = "topic-producer" service_account_names = ["sv-1"] resource_name_restriction { common_instance = "instance-1" common_cluster = "cluster-1" common_tenant = "tenant-1" common_namespace = "ns-1" common_topic = "allPartition('topic-1')" pulsar_topic_domain = "persistent" } } data "streamnative_rolebinding" "rb_resource_name_restriction" { depends_on = [streamnative_rolebinding.rb_resource_name_restriction] name = "rb_resource_name_restriction" organization = "o-y8z75" } ```
1 parent b604f0f commit 0570395

File tree

7 files changed

+314
-11
lines changed

7 files changed

+314
-11
lines changed

cloud/data_source_rolebinding.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package cloud
33
import (
44
"context"
55
"fmt"
6-
"github.com/streamnative/cloud-api-server/pkg/apis/cloud/v1alpha1"
6+
77
"strings"
88

9+
"github.com/streamnative/cloud-api-server/pkg/apis/cloud/v1alpha1"
10+
"github.com/streamnative/terraform-provider-streamnative/cloud/rbac"
11+
912
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1013
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1114
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -67,10 +70,18 @@ func dataSourceRoleBinding() *schema.Resource {
6770
Type: schema.TypeString,
6871
},
6972
},
73+
"resource_name_restriction": {
74+
Type: schema.TypeList,
75+
Computed: true,
76+
Elem: &schema.Resource{
77+
Schema: rbac.GenerateDataRoleBinding(),
78+
},
79+
},
7080
"condition_resource_names": {
7181
Type: schema.TypeList,
7282
Computed: true,
7383
Description: descriptions["rolebinding_condition_resource_names"],
84+
Deprecated: "condition_resource_names has deprecated, please use resource_name_restriction instead.",
7485
Elem: &schema.Resource{
7586
Schema: map[string]*schema.Schema{
7687
"organization": {
@@ -187,6 +198,14 @@ func DataSourceRoleBindingRead(ctx context.Context, d *schema.ResourceData, meta
187198
return diag.FromErr(fmt.Errorf("ERROR_SET_CONDITION: %w", err))
188199
}
189200

201+
if roleBinding.Spec.ResourceNameRestriction != nil {
202+
if rawData, updated := rbac.ParseToRaw(roleBinding.Spec.ResourceNameRestriction); updated {
203+
if err = d.Set("resource_name_restriction", []interface{}{rawData}); err != nil {
204+
return diag.FromErr(fmt.Errorf("ERROR_SET_RESOURCE_NAME_RESTRICTION: %w", err))
205+
}
206+
}
207+
}
208+
190209
if len(roleBinding.Status.Conditions) >= 1 {
191210
for _, condition := range roleBinding.Status.Conditions {
192211
if condition.Type == "Ready" && condition.Status == "True" {
@@ -196,6 +215,7 @@ func DataSourceRoleBindingRead(ctx context.Context, d *schema.ResourceData, meta
196215
}
197216
}
198217
}
218+
199219
d.SetId(fmt.Sprintf("%s/%s", roleBinding.Namespace, roleBinding.Name))
200220
return nil
201221
}

cloud/rbac/rbac.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package rbac
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"strings"
7+
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9+
cloudv1alpha1 "github.com/streamnative/cloud-api-server/pkg/apis/cloud/v1alpha1"
10+
)
11+
12+
const resourceNotSet = "__RESOURCE_UNSET__"
13+
14+
type iteratorProcessor func(reflect.Value, string) error
15+
16+
func iterateStructWithProcessor(s reflect.Value, prefix string, processor iteratorProcessor) error {
17+
if s.Kind() != reflect.Struct {
18+
return fmt.Errorf("expected a struct reflect.Value, got %s", s.Kind())
19+
}
20+
sType := s.Type()
21+
for i := 0; i < sType.NumField(); i++ {
22+
field := sType.Field(i)
23+
fieldValue := s.Field(i)
24+
flagName := strings.ToLower(field.Name)
25+
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
26+
parts := strings.Split(jsonTag, ",")
27+
if parts[0] != "" {
28+
flagName = parts[0]
29+
}
30+
}
31+
fullFlagName := prefix + flagName
32+
if fieldValue.Kind() == reflect.Ptr {
33+
if fieldValue.IsNil() {
34+
elem := fieldValue.Type().Elem()
35+
if elem.Kind() == reflect.Struct {
36+
fieldValue.Set(reflect.New(fieldValue.Type().Elem()))
37+
}
38+
}
39+
switch fieldValue.Type().Elem().Kind() {
40+
case reflect.Struct:
41+
if err := iterateStructWithProcessor(fieldValue.Elem(), fullFlagName+"_", processor); err != nil {
42+
return err
43+
}
44+
case reflect.String:
45+
break
46+
default:
47+
return fmt.Errorf("expected a struct pointer to a struct pointer, got %s", fieldValue.Elem().Kind())
48+
}
49+
if err := processor(fieldValue, strings.ToLower(fullFlagName)); err != nil {
50+
return err
51+
}
52+
} else {
53+
return fmt.Errorf("unsupported field type (not a pointer) for %s: %s", strings.ToLower(fullFlagName), fieldValue.Kind())
54+
}
55+
}
56+
return nil
57+
}
58+
59+
func ParseToResourceNameRestriction(rawData map[string]interface{}) (*cloudv1alpha1.ResourceNameRestriction, bool) {
60+
restriction := &cloudv1alpha1.ResourceNameRestriction{}
61+
updated := false
62+
if err := iterateStructWithProcessor(reflect.ValueOf(restriction).Elem(), "", func(fieldValue reflect.Value, fullName string) error {
63+
if value, exist := rawData[fullName]; exist {
64+
if reflect.TypeOf(value).Kind() == reflect.String {
65+
vstr := value.(string)
66+
if vstr != resourceNotSet {
67+
updated = true
68+
if fieldValue.IsNil() {
69+
fieldValue.Set(reflect.New(fieldValue.Type().Elem()))
70+
}
71+
fieldValue.Elem().SetString(vstr)
72+
}
73+
}
74+
}
75+
pointedToValue := fieldValue.Elem()
76+
if pointedToValue.Kind() == reflect.Struct && pointedToValue.IsZero() {
77+
fieldValue.Set(reflect.Zero(fieldValue.Type()))
78+
}
79+
return nil
80+
}); err != nil {
81+
panic(err)
82+
}
83+
return restriction, updated
84+
}
85+
86+
func ParseToRaw(restriction *cloudv1alpha1.ResourceNameRestriction) (map[string]interface{}, bool) {
87+
dc := restriction.DeepCopy()
88+
m := make(map[string]interface{})
89+
updated := false
90+
if err := iterateStructWithProcessor(reflect.ValueOf(dc).Elem(), "", func(fieldValue reflect.Value, fullName string) error {
91+
if (fieldValue.Kind() == reflect.Ptr && !fieldValue.IsNil()) && fieldValue.Elem().Kind() == reflect.String {
92+
updated = true
93+
m[fullName] = fieldValue.Elem().String()
94+
}
95+
return nil
96+
}); err != nil {
97+
panic(err)
98+
}
99+
return m, updated
100+
}
101+
102+
func GenerateResourceRoleBinding() map[string]*schema.Schema {
103+
schemas := make(map[string]*schema.Schema)
104+
restriction := &cloudv1alpha1.ResourceNameRestriction{}
105+
if err := iterateStructWithProcessor(reflect.ValueOf(restriction).Elem(), "", func(fieldValue reflect.Value, fullName string) error {
106+
pointedToValue := fieldValue.Type().Elem()
107+
if pointedToValue.Kind() == reflect.String {
108+
schemas[fullName] = &schema.Schema{
109+
Type: schema.TypeString,
110+
Optional: true,
111+
Default: resourceNotSet,
112+
}
113+
}
114+
return nil
115+
}); err != nil {
116+
panic(err)
117+
}
118+
return schemas
119+
}
120+
121+
func GenerateDataRoleBinding() map[string]*schema.Schema {
122+
schemas := make(map[string]*schema.Schema)
123+
restriction := &cloudv1alpha1.ResourceNameRestriction{}
124+
if err := iterateStructWithProcessor(reflect.ValueOf(restriction).Elem(), "", func(fieldValue reflect.Value, fullName string) error {
125+
pointedToValue := fieldValue.Type().Elem()
126+
if pointedToValue.Kind() == reflect.String {
127+
schemas[fullName] = &schema.Schema{
128+
Type: schema.TypeString,
129+
Computed: true,
130+
}
131+
}
132+
return nil
133+
}); err != nil {
134+
panic(err)
135+
}
136+
return schemas
137+
}

cloud/rbac/rbac_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package rbac
2+
3+
import (
4+
"testing"
5+
6+
"github.com/streamnative/cloud-api-server/pkg/apis/cloud/v1alpha1"
7+
"github.com/stretchr/testify/assert"
8+
"k8s.io/utils/ptr"
9+
)
10+
11+
func TestParser(t *testing.T) {
12+
restriction := &v1alpha1.ResourceNameRestriction{
13+
Common: &v1alpha1.CommonAttributes{
14+
Organization: ptr.To("org-1"),
15+
Instance: ptr.To("ins-1"),
16+
Cluster: ptr.To("cluster-1"),
17+
Tenant: ptr.To("tenant-1"),
18+
Namespace: ptr.To("namespace-1"),
19+
Topic: ptr.To("topic-1"),
20+
},
21+
Pulsar: &v1alpha1.PulsarAttributes{
22+
Topic: &v1alpha1.PulsarTopicAttributes{
23+
Domain: ptr.To("domain-1"),
24+
},
25+
Subscription: &v1alpha1.PulsarSubscriptionAttributes{
26+
Name: ptr.To("subscription-1"),
27+
},
28+
},
29+
Cloud: &v1alpha1.CloudAttributes{
30+
Apikey: &v1alpha1.CloudApiKeyAttributes{
31+
Name: ptr.To("api-key-1"),
32+
},
33+
},
34+
}
35+
36+
raw, updated := ParseToRaw(restriction)
37+
assert.True(t, updated)
38+
assert.NotNil(t, raw)
39+
parsedRestriction, updated := ParseToResourceNameRestriction(raw)
40+
assert.True(t, updated)
41+
assert.NotNil(t, parsedRestriction)
42+
assert.EqualValues(t, restriction, parsedRestriction)
43+
}
44+
45+
func TestParserIgnoreUnset(t *testing.T) {
46+
raw := map[string]interface{}{
47+
"common_organization": "org-1",
48+
"common_instance": "ins-1",
49+
"common_cluster": "cluster-1",
50+
"cloud_apikey_name": resourceNotSet,
51+
"pulsar_subscription_name": resourceNotSet,
52+
}
53+
parsedRestriction, updated := ParseToResourceNameRestriction(raw)
54+
assert.True(t, updated)
55+
assert.EqualValues(t, &v1alpha1.ResourceNameRestriction{
56+
Common: &v1alpha1.CommonAttributes{
57+
Organization: ptr.To("org-1"),
58+
Instance: ptr.To("ins-1"),
59+
Cluster: ptr.To("cluster-1"),
60+
},
61+
}, parsedRestriction)
62+
}

cloud/resource_rolebinding.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ package cloud
33
import (
44
"context"
55
"fmt"
6-
"github.com/streamnative/cloud-api-server/pkg/apis/cloud/v1alpha1"
76
"strings"
87
"time"
98

109
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1110
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
1211
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12+
"github.com/streamnative/cloud-api-server/pkg/apis/cloud/v1alpha1"
13+
"github.com/streamnative/terraform-provider-streamnative/cloud/rbac"
1314
apierrors "k8s.io/apimachinery/pkg/api/errors"
1415
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1516
)
@@ -91,11 +92,20 @@ func resourceRoleBinding() *schema.Resource {
9192
Type: schema.TypeString,
9293
},
9394
},
95+
"resource_name_restriction": {
96+
Type: schema.TypeList,
97+
Optional: true,
98+
MaxItems: 1,
99+
Elem: &schema.Resource{
100+
Schema: rbac.GenerateResourceRoleBinding(),
101+
},
102+
},
94103
"condition_resource_names": {
95104
ConflictsWith: []string{"condition_cel"},
96105
Type: schema.TypeList,
97106
Optional: true,
98107
Description: descriptions["rolebinding_condition_resource_names"],
108+
Deprecated: "condition_resource_names has deprecated, please use resource_name_restriction instead.",
99109
Elem: &schema.Resource{
100110
Schema: map[string]*schema.Schema{
101111
"organization": {
@@ -168,6 +178,7 @@ func resourceRoleBindingCreate(ctx context.Context, d *schema.ResourceData, m in
168178
predefinedRoleName := d.Get("cluster_role_name").(string)
169179
serviceAccountNames := d.Get("service_account_names").([]interface{})
170180
userNames := d.Get("user_names").([]interface{})
181+
resourceNameRestriction := d.Get("resource_name_restriction").([]interface{})
171182

172183
clientSet, err := getClientSet(getFactoryFromMeta(m))
173184
if err != nil {
@@ -214,6 +225,12 @@ func resourceRoleBindingCreate(ctx context.Context, d *schema.ResourceData, m in
214225
}
215226
}
216227

228+
if resourceNameRestriction != nil && len(resourceNameRestriction) > 0 {
229+
if restriction, updated := rbac.ParseToResourceNameRestriction(resourceNameRestriction[0].(map[string]interface{})); updated {
230+
rb.Spec.ResourceNameRestriction = restriction
231+
}
232+
}
233+
217234
conditionSet(namespace, d, rb)
218235

219236
if _, err := clientSet.CloudV1alpha1().RoleBindings(namespace).Create(ctx, rb, metav1.CreateOptions{

docs/data-sources/rolebinding.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ description: |-
2424

2525
- `cluster_role_name` (String) The predefined role name
2626
- `condition_cel` (String) The conditional role binding CEL(Common Expression Language) expression
27-
- `condition_resource_names` (List of Object) The list of conditional role binding resource names (see [below for nested schema](#nestedatt--condition_resource_names))
27+
- `condition_resource_names` (List of Object, Deprecated) The list of conditional role binding resource names (see [below for nested schema](#nestedatt--condition_resource_names))
2828
- `id` (String) The ID of this resource.
2929
- `ready` (Boolean) The RoleBinding is ready, it will be set to 'True' after the cluster is ready
30+
- `resource_name_restriction` (List of Object) (see [below for nested schema](#nestedatt--resource_name_restriction))
3031
- `service_account_names` (List of String) The list of service accounts that are role binding names
3132
- `user_names` (List of String) The list of users that are role binding names
3233

@@ -45,3 +46,24 @@ Read-Only:
4546
- `tenant` (String)
4647
- `topic_domain` (String)
4748
- `topic_name` (String)
49+
50+
51+
<a id="nestedatt--resource_name_restriction"></a>
52+
### Nested Schema for `resource_name_restriction`
53+
54+
Read-Only:
55+
56+
- `cloud_apikey_name` (String)
57+
- `cloud_secret_name` (String)
58+
- `cloud_serviceaccount_name` (String)
59+
- `common_cluster` (String)
60+
- `common_instance` (String)
61+
- `common_namespace` (String)
62+
- `common_organization` (String)
63+
- `common_tenant` (String)
64+
- `common_topic` (String)
65+
- `kafka_consumergroup_name` (String)
66+
- `kafka_transaction_id` (String)
67+
- `pulsar_subscription_name` (String)
68+
- `pulsar_topic_domain` (String)
69+
- `schema_subject` (String)

docs/resources/rolebinding.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ description: |-
2424

2525
- `cluster_role_name` (String) The predefined role name
2626
- `condition_cel` (String) The conditional role binding CEL(Common Expression Language) expression
27-
- `condition_resource_names` (Block List) The list of conditional role binding resource names (see [below for nested schema](#nestedblock--condition_resource_names))
27+
- `condition_resource_names` (Block List, Deprecated) The list of conditional role binding resource names (see [below for nested schema](#nestedblock--condition_resource_names))
28+
- `resource_name_restriction` (Block List, Max: 1) (see [below for nested schema](#nestedblock--resource_name_restriction))
2829
- `service_account_names` (List of String) The list of service accounts that are role binding names
2930
- `user_names` (List of String) The list of users that are role binding names
3031

@@ -48,3 +49,24 @@ Optional:
4849
- `tenant` (String) The conditional role binding resource name - tenant
4950
- `topic_domain` (String) The conditional role binding resource name - topic domain(persistent/non-persistent)
5051
- `topic_name` (String) The conditional role binding resource name - topic name
52+
53+
54+
<a id="nestedblock--resource_name_restriction"></a>
55+
### Nested Schema for `resource_name_restriction`
56+
57+
Optional:
58+
59+
- `cloud_apikey_name` (String)
60+
- `cloud_secret_name` (String)
61+
- `cloud_serviceaccount_name` (String)
62+
- `common_cluster` (String)
63+
- `common_instance` (String)
64+
- `common_namespace` (String)
65+
- `common_organization` (String)
66+
- `common_tenant` (String)
67+
- `common_topic` (String)
68+
- `kafka_consumergroup_name` (String)
69+
- `kafka_transaction_id` (String)
70+
- `pulsar_subscription_name` (String)
71+
- `pulsar_topic_domain` (String)
72+
- `schema_subject` (String)

0 commit comments

Comments
 (0)