Skip to content

Commit 5a43eda

Browse files
authored
add role grant resource (#232)
1 parent b5d8bf1 commit 5a43eda

File tree

8 files changed

+1608
-52
lines changed

8 files changed

+1608
-52
lines changed

examples/rbac/main.tf

+15-4
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,21 @@ resource "clickhouse_role" "writer" {
7979
name = "writer"
8080
}
8181

82-
output "service_endpoints" {
83-
value = clickhouse_service.service.endpoints
82+
resource "clickhouse_grant_role" "writer_to_john" {
83+
service_id = clickhouse_service.service.id
84+
role_name = clickhouse_role.writer.name
85+
grantee_user_name = clickhouse_user.john.name
86+
admin_option = false
8487
}
8588

86-
output "service_iam" {
87-
value = clickhouse_service.service.iam_role
89+
resource "clickhouse_role" "manager" {
90+
service_id = clickhouse_service.service.id
91+
name = "manager"
92+
}
93+
94+
resource "clickhouse_grant_role" "writer_to_manager" {
95+
service_id = clickhouse_service.service.id
96+
role_name = clickhouse_role.writer.name
97+
grantee_role_name = clickhouse_role.manager.name
98+
admin_option = false
8899
}

pkg/internal/api/client_mock.go

+1,242-48
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/internal/api/db_grant_role.go

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/huandu/go-sqlbuilder"
9+
10+
sqlutil "github.com/ClickHouse/terraform-provider-clickhouse/pkg/internal/sql"
11+
)
12+
13+
type GrantRole struct {
14+
RoleName string `json:"granted_role_name"`
15+
GranteeUserName *string `json:"user_name"`
16+
GranteeRoleName *string `json:"role_name"`
17+
AdminOption bool `json:"with_admin_option"`
18+
}
19+
20+
func (c *ClientImpl) GrantRole(ctx context.Context, serviceID string, grantRole GrantRole) (*GrantRole, error) {
21+
format := "GRANT `$?` TO `$?`"
22+
if grantRole.AdminOption {
23+
format = fmt.Sprintf("%s WITH ADMIN OPTION", format)
24+
}
25+
args := []interface{}{
26+
sqlbuilder.Raw(sqlutil.EscapeBacktick(grantRole.RoleName)),
27+
}
28+
29+
if grantRole.GranteeUserName != nil {
30+
args = append(args, sqlbuilder.Raw(sqlutil.EscapeBacktick(*grantRole.GranteeUserName)))
31+
} else if grantRole.GranteeRoleName != nil {
32+
args = append(args, sqlbuilder.Raw(sqlutil.EscapeBacktick(*grantRole.GranteeRoleName)))
33+
} else {
34+
return nil, fmt.Errorf("either GranteeUserName or GranteeRoleName must be set")
35+
}
36+
37+
sb := sqlbuilder.Build(format, args...)
38+
39+
sql, args := sb.Build()
40+
41+
_, err := c.runQuery(ctx, serviceID, sql, args...)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
createdGrant, err := c.GetGrantRole(ctx, serviceID, grantRole.RoleName, grantRole.GranteeUserName, grantRole.GranteeRoleName)
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
return createdGrant, nil
52+
}
53+
54+
func (c *ClientImpl) GetGrantRole(ctx context.Context, serviceID string, grantedRoleName string, granteeUserName *string, granteeRoleName *string) (*GrantRole, error) {
55+
var fieldName, fieldValue string
56+
if granteeUserName != nil {
57+
fieldName = "user_name"
58+
fieldValue = *granteeUserName
59+
} else if granteeRoleName != nil {
60+
fieldName = "role_name"
61+
fieldValue = *granteeRoleName
62+
} else {
63+
return nil, fmt.Errorf("either GranteeUserName or GranteeRoleName must be set")
64+
}
65+
66+
format := fmt.Sprintf("SELECT granted_role_name,user_name,role_name,toBool(with_admin_option) as with_admin_option FROM system.role_grants WHERE granted_role_name = ${granted_role_name} and %s = ${field_value}", fieldName)
67+
args := []interface{}{
68+
sqlbuilder.Named("granted_role_name", grantedRoleName),
69+
sqlbuilder.Named("field_value", fieldValue),
70+
}
71+
72+
sb := sqlbuilder.Build(format, args...)
73+
74+
sql, args := sb.Build()
75+
76+
data, err := c.runQuery(ctx, serviceID, sql, args...)
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
if len(data) == 0 {
82+
// Grant not found
83+
return nil, nil
84+
}
85+
86+
grant := GrantRole{}
87+
88+
err = json.Unmarshal(data, &grant)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
if grant.GranteeUserName != nil && *grant.GranteeUserName == "" {
94+
grant.GranteeUserName = nil
95+
}
96+
if grant.GranteeRoleName != nil && *grant.GranteeRoleName == "" {
97+
grant.GranteeRoleName = nil
98+
}
99+
100+
return &grant, nil
101+
}
102+
103+
func (c *ClientImpl) RevokeGrantRole(ctx context.Context, serviceID string, grantedRoleName string, granteeUserName *string, granteeRoleName *string) error {
104+
format := "REVOKE `$?` FROM `$?`"
105+
args := []interface{}{
106+
sqlbuilder.Raw(sqlutil.EscapeBacktick(grantedRoleName)),
107+
}
108+
109+
if granteeUserName != nil {
110+
args = append(args, sqlbuilder.Raw(sqlutil.EscapeBacktick(*granteeUserName)))
111+
} else if granteeRoleName != nil {
112+
args = append(args, sqlbuilder.Raw(sqlutil.EscapeBacktick(*granteeRoleName)))
113+
} else {
114+
return fmt.Errorf("either GranteeUserName or GranteeRoleName must be set")
115+
}
116+
117+
sb := sqlbuilder.Build(format, args...)
118+
119+
sql, args := sb.Build()
120+
121+
_, err := c.runQuery(ctx, serviceID, sql, args...)
122+
if err != nil {
123+
return err
124+
}
125+
126+
return nil
127+
}

pkg/internal/api/interface.go

+4
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,8 @@ type Client interface {
3838
CreateRole(ctx context.Context, serviceId string, role Role) (*Role, error)
3939
GetRole(ctx context.Context, serviceID string, name string) (*Role, error)
4040
DeleteRole(ctx context.Context, serviceID string, name string) error
41+
42+
GrantRole(ctx context.Context, serviceId string, grantRole GrantRole) (*GrantRole, error)
43+
GetGrantRole(ctx context.Context, serviceID string, grantedRoleName string, granteeUserName *string, granteeRoleName *string) (*GrantRole, error)
44+
RevokeGrantRole(ctx context.Context, serviceID string, grantedRoleName string, granteeUserName *string, granteeRoleName *string) error
4145
}
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
You can use the `clickhouse_grant_role` resource to grant a `clickhouse_role` to either a `clickhouse_user` or to another `clickhouse_role`.
2+
3+
Known limitations:
4+
5+
- It's not possible to grant the same `clickhouse_role` to both a `clickhouse_user` and a `clickhouse_role` using a single `clickhouse_grant_role` stanza. You can do that using two different stanzas, one with `grantee_user_name` and the other with `grantee_role_name` fields set.
6+
- Importing `clickhouse_grant_role` resources into terraform is not supported.

pkg/resource/grant_role.go

+198
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
//go:build alpha
2+
3+
package resource
4+
5+
import (
6+
"context"
7+
_ "embed"
8+
9+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
10+
"github.com/hashicorp/terraform-plugin-framework/path"
11+
"github.com/hashicorp/terraform-plugin-framework/resource"
12+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
13+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
14+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
15+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
16+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
17+
"github.com/hashicorp/terraform-plugin-framework/types"
18+
19+
"github.com/ClickHouse/terraform-provider-clickhouse/pkg/internal/api"
20+
"github.com/ClickHouse/terraform-provider-clickhouse/pkg/resource/models"
21+
)
22+
23+
//go:embed descriptions/grant_role.md
24+
var grantResourceDescription string
25+
26+
var (
27+
_ resource.Resource = &GrantRoleResource{}
28+
_ resource.ResourceWithConfigure = &GrantRoleResource{}
29+
)
30+
31+
func NewGrantRoleResource() resource.Resource {
32+
return &GrantRoleResource{}
33+
}
34+
35+
type GrantRoleResource struct {
36+
client api.Client
37+
}
38+
39+
func (r *GrantRoleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
40+
resp.TypeName = req.ProviderTypeName + "_grant_role"
41+
}
42+
43+
func (r *GrantRoleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
44+
resp.Schema = schema.Schema{
45+
Attributes: map[string]schema.Attribute{
46+
"service_id": schema.StringAttribute{
47+
Description: "ClickHouse Service ID",
48+
Required: true,
49+
PlanModifiers: []planmodifier.String{
50+
stringplanmodifier.RequiresReplace(),
51+
},
52+
},
53+
"role_name": schema.StringAttribute{
54+
Required: true,
55+
Description: "Name of the role to be granted",
56+
PlanModifiers: []planmodifier.String{
57+
stringplanmodifier.RequiresReplace(),
58+
},
59+
},
60+
"grantee_user_name": schema.StringAttribute{
61+
Optional: true,
62+
Description: "Name of the `user` to grant `role_name` to.",
63+
PlanModifiers: []planmodifier.String{
64+
stringplanmodifier.RequiresReplace(),
65+
},
66+
Validators: []validator.String{
67+
stringvalidator.ConflictsWith(path.Expressions{path.MatchRoot("grantee_role_name")}...),
68+
stringvalidator.AtLeastOneOf(path.Expressions{
69+
path.MatchRoot("grantee_user_name"),
70+
path.MatchRoot("grantee_role_name"),
71+
}...),
72+
},
73+
},
74+
"grantee_role_name": schema.StringAttribute{
75+
Optional: true,
76+
Description: "Name of the `role` to grant `role_name` to.",
77+
PlanModifiers: []planmodifier.String{
78+
stringplanmodifier.RequiresReplace(),
79+
},
80+
Validators: []validator.String{
81+
stringvalidator.ConflictsWith(path.Expressions{path.MatchRoot("grantee_user_name")}...),
82+
stringvalidator.AtLeastOneOf(path.Expressions{
83+
path.MatchRoot("grantee_user_name"),
84+
path.MatchRoot("grantee_role_name"),
85+
}...),
86+
},
87+
},
88+
"admin_option": schema.BoolAttribute{
89+
Optional: true,
90+
Computed: true,
91+
Description: "If true, the grantee will be able to grant `role_name` to other `users` or `roles`.",
92+
PlanModifiers: []planmodifier.Bool{
93+
boolplanmodifier.RequiresReplace(),
94+
},
95+
},
96+
},
97+
MarkdownDescription: grantResourceDescription,
98+
}
99+
}
100+
101+
func (r *GrantRoleResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) {
102+
if req.ProviderData == nil {
103+
return
104+
}
105+
106+
r.client = req.ProviderData.(api.Client)
107+
}
108+
109+
func (r *GrantRoleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
110+
var plan models.GrantRole
111+
diags := req.Plan.Get(ctx, &plan)
112+
resp.Diagnostics.Append(diags...)
113+
if resp.Diagnostics.HasError() {
114+
return
115+
}
116+
117+
grant := api.GrantRole{
118+
RoleName: plan.RoleName.ValueString(),
119+
GranteeUserName: plan.GranteeUserName.ValueStringPointer(),
120+
GranteeRoleName: plan.GranteeRoleName.ValueStringPointer(),
121+
AdminOption: plan.AdminOption.ValueBool(),
122+
}
123+
124+
createdGrant, err := r.client.GrantRole(ctx, plan.ServiceID.ValueString(), grant)
125+
if err != nil {
126+
resp.Diagnostics.AddError(
127+
"Error Creating ClickHouse Role Grant",
128+
"Could not create role grant, unexpected error: "+err.Error(),
129+
)
130+
return
131+
}
132+
133+
state := models.GrantRole{
134+
ServiceID: plan.ServiceID,
135+
RoleName: types.StringValue(createdGrant.RoleName),
136+
GranteeUserName: types.StringPointerValue(createdGrant.GranteeUserName),
137+
GranteeRoleName: types.StringPointerValue(createdGrant.GranteeRoleName),
138+
AdminOption: types.BoolValue(createdGrant.AdminOption),
139+
}
140+
141+
diags = resp.State.Set(ctx, state)
142+
resp.Diagnostics.Append(diags...)
143+
if resp.Diagnostics.HasError() {
144+
return
145+
}
146+
}
147+
148+
func (r *GrantRoleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
149+
var state models.GrantRole
150+
diags := req.State.Get(ctx, &state)
151+
resp.Diagnostics.Append(diags...)
152+
if resp.Diagnostics.HasError() {
153+
return
154+
}
155+
156+
grant, err := r.client.GetGrantRole(ctx, state.ServiceID.ValueString(), state.RoleName.ValueString(), state.GranteeUserName.ValueStringPointer(), state.GranteeRoleName.ValueStringPointer())
157+
if err != nil {
158+
resp.Diagnostics.AddError(
159+
"Error Reading ClickHouse Role Grant",
160+
"Could not read role grant, unexpected error: "+err.Error(),
161+
)
162+
return
163+
}
164+
165+
if grant != nil {
166+
state.RoleName = types.StringValue(grant.RoleName)
167+
state.GranteeUserName = types.StringPointerValue(grant.GranteeUserName)
168+
state.GranteeRoleName = types.StringPointerValue(grant.GranteeRoleName)
169+
state.AdminOption = types.BoolValue(grant.AdminOption)
170+
171+
diags = resp.State.Set(ctx, &state)
172+
resp.Diagnostics.Append(diags...)
173+
} else {
174+
resp.State.RemoveResource(ctx)
175+
}
176+
}
177+
178+
func (r *GrantRoleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
179+
panic("Update of grant resource is not supported")
180+
}
181+
182+
func (r *GrantRoleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
183+
var state models.GrantRole
184+
diags := req.State.Get(ctx, &state)
185+
resp.Diagnostics.Append(diags...)
186+
if resp.Diagnostics.HasError() {
187+
return
188+
}
189+
190+
err := r.client.RevokeGrantRole(ctx, state.ServiceID.ValueString(), state.RoleName.ValueString(), state.GranteeUserName.ValueStringPointer(), state.GranteeRoleName.ValueStringPointer())
191+
if err != nil {
192+
resp.Diagnostics.AddError(
193+
"Error Deleting ClickHouse Role Grant",
194+
"Could not delete role grant, unexpected error: "+err.Error(),
195+
)
196+
return
197+
}
198+
}

pkg/resource/models/grant_role.go

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//go:build alpha
2+
3+
package models
4+
5+
import (
6+
"github.com/hashicorp/terraform-plugin-framework/types"
7+
)
8+
9+
type GrantRole struct {
10+
ServiceID types.String `tfsdk:"service_id"`
11+
RoleName types.String `tfsdk:"role_name"`
12+
GranteeUserName types.String `tfsdk:"grantee_user_name"`
13+
GranteeRoleName types.String `tfsdk:"grantee_role_name"`
14+
AdminOption types.Bool `tfsdk:"admin_option"`
15+
}

pkg/resource/register_debug.go

+1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ func GetResourceFactories() []func() upstreamresource.Resource {
1414
NewClickPipeResource,
1515
NewUserResource,
1616
NewRoleResource,
17+
NewGrantRoleResource,
1718
}
1819
}

0 commit comments

Comments
 (0)