diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c98ca46..86f70f17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Ignore `http.StatusBadRequest` on `ClickhouseGrant` deletion - Retry conflict error when k8s object saved to the storage - Fix `ClickhouseGrant` invalid remote and local privileges comparison +- Fix `ClickhouseGrant`: escapes role name to grant ## v0.22.0 - 2024-07-02 diff --git a/api/v1alpha1/clickhousegrant_types.go b/api/v1alpha1/clickhousegrant_types.go index f1515a87..73a76142 100644 --- a/api/v1alpha1/clickhousegrant_types.go +++ b/api/v1alpha1/clickhousegrant_types.go @@ -237,7 +237,7 @@ func constructPrivilegesPart(g PrivilegeGrant) string { } func (g RoleGrant) ConstructParts(t chUtils.StatementType) (string, string, string) { - rolesPart := strings.Join(g.Roles, ", ") + rolesPart := strings.Join(escapeList(g.Roles), ", ") granteesPart := constructGranteePart(g.Grantees) options := "" if g.WithAdminOption && t == chUtils.GRANT { @@ -315,3 +315,11 @@ func escape(identifier string) string { replacer := strings.NewReplacer("`", "\\`", "\\", "\\\\") return "`" + replacer.Replace(identifier) + "`" } + +func escapeList(list []string) []string { + result := make([]string, len(list)) + for i, v := range list { + result[i] = escape(v) + } + return result +} diff --git a/clickhouserole.yaml b/clickhouserole.yaml new file mode 100644 index 00000000..e740e67b --- /dev/null +++ b/clickhouserole.yaml @@ -0,0 +1,63 @@ +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseGrant +metadata: + name: test-grant +spec: + authSecretRef: + name: aiven-token + key: token + + project: aiven-ci-kubernetes-operator + serviceName: clickhouse-227c6ab5 + + privilegeGrants: + - database: clickhouse-db + grantees: + - role: writer + privileges: + - INSERT + - SELECT + - CREATE TABLE + - CREATE VIEW + roleGrants: + - grantees: + - user: clickhouse-user + roles: + - writer +--- +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseDatabase +metadata: + name: clickhouse-db +spec: + authSecretRef: + name: aiven-token + key: token + + project: aiven-ci-kubernetes-operator + serviceName: clickhouse-227c6ab5 +--- +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseUser +metadata: + name: clickhouse-user +spec: + authSecretRef: + name: aiven-token + key: token + + project: aiven-ci-kubernetes-operator + serviceName: clickhouse-227c6ab5 +--- +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseRole +metadata: + name: writer +spec: + authSecretRef: + name: aiven-token + key: token + + project: aiven-ci-kubernetes-operator + serviceName: clickhouse-227c6ab5 + role: writer diff --git a/docs/docs/api-reference/clickhousegrant.md b/docs/docs/api-reference/clickhousegrant.md index d4d1c9bc..58e9deb6 100644 --- a/docs/docs/api-reference/clickhousegrant.md +++ b/docs/docs/api-reference/clickhousegrant.md @@ -2,9 +2,9 @@ title: "ClickhouseGrant" --- -## Usage example +## Usage examples -??? example +??? example "example_2" ```yaml apiVersion: aiven.io/v1alpha1 kind: ClickhouseGrant @@ -46,6 +46,93 @@ title: "ClickhouseGrant" - role: my-role ``` +??? example + ```yaml + apiVersion: aiven.io/v1alpha1 + kind: ClickhouseGrant + metadata: + name: my-clickhouse-grant + spec: + authSecretRef: + name: aiven-token + key: token + + project: my-aiven-project + serviceName: my-clickhouse + privilegeGrants: + - grantees: + - role: my-clickhouse-role + privileges: + - INSERT + - SELECT + - CREATE TABLE + - CREATE VIEW + database: my-clickhouse-db + roleGrants: + - grantees: + - user: my-clickhouse-user + roles: + - my-clickhouse-role + + --- + + apiVersion: aiven.io/v1alpha1 + kind: Clickhouse + metadata: + name: my-clickhouse + spec: + authSecretRef: + name: aiven-token + key: token + + project: my-aiven-project + cloudName: google-europe-west1 + plan: startup-16 + + --- + + apiVersion: aiven.io/v1alpha1 + kind: ClickhouseDatabase + metadata: + name: my-clickhouse-db + spec: + authSecretRef: + name: aiven-token + key: token + + project: my-aiven-project + serviceName: my-clickhouse + + --- + + apiVersion: aiven.io/v1alpha1 + kind: ClickhouseUser + metadata: + name: my-clickhouse-user + spec: + authSecretRef: + name: aiven-token + key: token + + project: my-aiven-project + serviceName: my-clickhouse + + --- + + apiVersion: aiven.io/v1alpha1 + kind: ClickhouseRole + metadata: + name: my-clickhouse-role + spec: + authSecretRef: + name: aiven-token + key: token + + project: my-aiven-project + serviceName: my-clickhouse + role: my-clickhouse-role + ``` + !!! info To create this resource, a `Secret` containing Aiven token must be [created](/aiven-operator/authentication.html) first. diff --git a/docs/docs/api-reference/examples/clickhousegrant.example_2.yaml b/docs/docs/api-reference/examples/clickhousegrant.example_2.yaml new file mode 100644 index 00000000..9fc67a1c --- /dev/null +++ b/docs/docs/api-reference/examples/clickhousegrant.example_2.yaml @@ -0,0 +1,40 @@ + +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseGrant +metadata: + name: demo-ch-grant +spec: + authSecretRef: + name: aiven-token + key: token + + project: my-aiven-project + serviceName: my-clickhouse + + privilegeGrants: + - grantees: + - user: user1 + - user: my-clickhouse-user-🦄 + privileges: + - SELECT + - INSERT + database: my-db + # If table is omitted, the privileges are granted on all tables in the database + # If columns is omitted, the privileges are granted on all columns in the table + - grantees: + - role: my-role + privileges: + - SELECT + database: my-db + table: my-table + columns: + - col1 + - col2 + + roleGrants: + - roles: + - other-role + grantees: + - user: my-user + - role: my-role + diff --git a/docs/docs/api-reference/examples/clickhousegrant.yaml b/docs/docs/api-reference/examples/clickhousegrant.yaml index 9fc67a1c..ec150873 100644 --- a/docs/docs/api-reference/examples/clickhousegrant.yaml +++ b/docs/docs/api-reference/examples/clickhousegrant.yaml @@ -1,40 +1,84 @@ - apiVersion: aiven.io/v1alpha1 kind: ClickhouseGrant metadata: - name: demo-ch-grant + name: my-clickhouse-grant spec: authSecretRef: name: aiven-token key: token - project: my-aiven-project + project: aiven-project-name serviceName: my-clickhouse privilegeGrants: - grantees: - - user: user1 - - user: my-clickhouse-user-🦄 + - role: my-clickhouse-role privileges: - - SELECT - INSERT - database: my-db - # If table is omitted, the privileges are granted on all tables in the database - # If columns is omitted, the privileges are granted on all columns in the table - - grantees: - - role: my-role - privileges: - SELECT - database: my-db - table: my-table - columns: - - col1 - - col2 - + - CREATE TABLE + - CREATE VIEW + database: my-clickhouse-db roleGrants: - - roles: - - other-role - grantees: - - user: my-user - - role: my-role + - grantees: + - user: my-clickhouse-user + roles: + - my-clickhouse-role + +--- + +apiVersion: aiven.io/v1alpha1 +kind: Clickhouse +metadata: + name: my-clickhouse +spec: + authSecretRef: + name: aiven-token + key: token + + project: aiven-project-name + cloudName: google-europe-west1 + plan: startup-16 + +--- + +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseDatabase +metadata: + name: my-clickhouse-db +spec: + authSecretRef: + name: aiven-token + key: token + + project: aiven-project-name + serviceName: my-clickhouse + +--- + +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseUser +metadata: + name: my-clickhouse-user +spec: + authSecretRef: + name: aiven-token + key: token + project: aiven-project-name + serviceName: my-clickhouse + +--- + +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseRole +metadata: + name: my-clickhouse-role +spec: + authSecretRef: + name: aiven-token + key: token + + project: aiven-project-name + serviceName: my-clickhouse + role: my-clickhouse-role diff --git a/tests/clickhousegrant_test.go b/tests/clickhousegrant_test.go index 853b050e..538df03a 100644 --- a/tests/clickhousegrant_test.go +++ b/tests/clickhousegrant_test.go @@ -17,7 +17,7 @@ import ( chUtils "github.com/aiven/aiven-operator/utils/clickhouse" ) -func chConnFromSecret(ctx context.Context, secret *corev1.Secret) (clickhouse.Conn, error) { +func chConnFromSecret(secret *corev1.Secret) (clickhouse.Conn, error) { c, err := clickhouse.Open(&clickhouse.Options{ Addr: []string{fmt.Sprintf("%s:%s", string(secret.Data["HOST"]), string(secret.Data["PORT"]))}, Auth: clickhouse.Auth{ @@ -141,18 +141,16 @@ func TestClickhouseGrant(t *testing.T) { // Waits kube objects ch := new(v1alpha1.Clickhouse) - // user := new(v1alpha1.ClickhouseUser) db := new(v1alpha1.ClickhouseDatabase) grant := new(v1alpha1.ClickhouseGrant) require.NoError(t, s.GetRunning(ch, chName)) - // require.NoError(t, s.GetRunning(user, userName)) require.NoError(t, s.GetRunning(db, dbName)) // Constructs connection to ClickHouse from service secret secret, err := s.GetSecret(ch.GetName()) require.NoError(t, err, "Failed to get secret") - conn, err := chConnFromSecret(ctx, secret) + conn, err := chConnFromSecret(secret) require.NoError(t, err, "failed to connect to ClickHouse") // THEN @@ -331,3 +329,86 @@ var expectedRoleGrants = []ClickhouseRoleGrant{ } func ptr(s string) *string { return &s } + +func fromPtr[T any](v *T) T { + if v == nil { + var empty T + return empty + } + return *v +} + +func TestClickhouseGrantExample(t *testing.T) { + t.Parallel() + defer recoverPanic(t) + + // GIVEN + ctx, cancel := testCtx() + defer cancel() + + chName := randName("clickhouse") + dbName := randName("clickhouse-db") + userName := randName("clickhouse-user") + grantName := randName("clickhouse-grant") + roleName := randName("clickhouse-role") + + yml, err := loadExampleYaml("clickhousegrant.yaml", map[string]string{ + "aiven-project-name": cfg.Project, + "my-clickhouse": chName, + "my-clickhouse-db": dbName, + "my-clickhouse-user": userName, + "my-clickhouse-grant": grantName, + "my-clickhouse-role": roleName, + }) + require.NoError(t, err) + s := NewSession(ctx, k8sClient, cfg.Project) + + // Cleans test afterward + defer s.Destroy() + + // WHEN + // Applies given manifest + require.NoError(t, s.Apply(yml)) + + ch := new(v1alpha1.Clickhouse) + require.NoError(t, s.GetRunning(ch, chName)) + + user := new(v1alpha1.ClickhouseUser) + require.NoError(t, s.GetRunning(user, userName)) + + db := new(v1alpha1.ClickhouseDatabase) + require.NoError(t, s.GetRunning(db, dbName)) + + role := new(v1alpha1.ClickhouseRole) + require.NoError(t, s.GetRunning(role, roleName)) + + grant := new(v1alpha1.ClickhouseGrant) + require.NoError(t, s.GetRunning(grant, grantName)) + + // Creates connection + secret, err := s.GetSecret(ch.GetName()) + require.NoError(t, err, "Failed to get secret") + + conn, err := chConnFromSecret(secret) + require.NoError(t, err, "failed to connect to ClickHouse") + + results, err := queryAndCollectResults[ClickhouseGrant](ctx, conn, chUtils.QueryNonAivenPrivileges) + require.NoError(t, err) + + // Privileges validation + expected := map[string]bool{ + fmt.Sprintf("%s/%s/INSERT", dbName, roleName): true, + fmt.Sprintf("%s/%s/SELECT", dbName, roleName): true, + fmt.Sprintf("%s/%s/CREATE TABLE", dbName, roleName): true, + fmt.Sprintf("%s/%s/CREATE VIEW", dbName, roleName): true, + } + + // Finds and removes grants from the expected list + for _, r := range results { + key := fmt.Sprintf("%s/%s/%s", fromPtr(r.Database), fromPtr(r.RoleName), r.AccessType) + delete(expected, key) + } + + // Nothing left == all found + assert.Empty(t, expected) +}