From 9b5eba62f9322bd5a7cc041a05b9b9b5a7a38093 Mon Sep 17 00:00:00 2001 From: xeniape Date: Tue, 16 Dec 2025 14:01:42 +0100 Subject: [PATCH 1/8] add batchColumnMasks OPA endpoint, add schema definition, adjust operator to use new batched endpoint --- rust/operator-binary/src/authorization/opa.rs | 22 ++--- rust/operator-binary/src/controller.rs | 6 +- .../trino_rules/schema/input.json | 61 ++++++++++++++ .../trino_rules/trino/verification.rego | 83 +++++++++++++++++++ 4 files changed, 158 insertions(+), 14 deletions(-) diff --git a/rust/operator-binary/src/authorization/opa.rs b/rust/operator-binary/src/authorization/opa.rs index 1bd9cd41..c68f31e1 100644 --- a/rust/operator-binary/src/authorization/opa.rs +++ b/rust/operator-binary/src/authorization/opa.rs @@ -23,10 +23,10 @@ pub struct TrinoOpaConfig { /// `http://localhost:8081/v1/data/trino/rowFilters` - if not set, /// no row filtering will be applied pub(crate) row_filters_connection_string: Option, - /// URI for fetching column masks, e.g. - /// `http://localhost:8081/v1/data/trino/columnMask` - if not set, - /// no masking will be applied - pub(crate) column_masking_connection_string: Option, + /// URI for fetching columns masks in batches, e.g. + /// `http://localhost:8081/v1/data/trino/batchColumnMasks` - if not set, + /// column-masking-uri will be used for getting column masks in parallel + pub(crate) batched_column_masking_connection_string: Option, /// Whether to allow permission management (GRANT, DENY, ...) and /// role management operations - OPA will not be queried for any /// such operations, they will be bulk allowed or denied depending @@ -65,12 +65,12 @@ impl TrinoOpaConfig { OpaApiVersion::V1, ) .await?; - let column_masking_connection_string = opa_config + let batched_column_masking_connection_string = opa_config .full_document_url_from_config_map( client, trino, - // Sticking to https://github.com/trinodb/trino/blob/455/plugin/trino-opa/src/test/java/io/trino/plugin/opa/TestOpaAccessControlDataFilteringSystem.java#L47 - Some("columnMask"), + // Sticking to https://github.com/trinodb/trino/blob/455/plugin/trino-opa/src/test/java/io/trino/plugin/opa/TestOpaAccessControlDataFilteringSystem.java#L48 + Some("batchColumnMasks"), OpaApiVersion::V1, ) .await?; @@ -89,7 +89,7 @@ impl TrinoOpaConfig { non_batched_connection_string, batched_connection_string, row_filters_connection_string: Some(row_filters_connection_string), - column_masking_connection_string: Some(column_masking_connection_string), + batched_column_masking_connection_string: Some(batched_column_masking_connection_string), allow_permission_management_operations: true, tls_secret_class, }) @@ -113,10 +113,10 @@ impl TrinoOpaConfig { Some(row_filters_connection_string.clone()), ); } - if let Some(column_masking_connection_string) = &self.column_masking_connection_string { + if let Some(batched_column_masking_connection_string) = &self.batched_column_masking_connection_string { config.insert( - "opa.policy.column-masking-uri".to_string(), - Some(column_masking_connection_string.clone()), + "opa.policy.batch-column-masking-uri".to_string(), + Some(batched_column_masking_connection_string.clone()), ); } if self.allow_permission_management_operations { diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index d8ddea6a..cb2b3aaa 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -2064,8 +2064,8 @@ mod tests { "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/rowFilters" .to_string(), ), - column_masking_connection_string: Some( - "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/columnMask" + batched_column_masking_connection_string: Some( + "http://simple-opa.default.svc.cluster.local:8081/v1/data/my-product/batchColumnMasks" .to_string(), ), allow_permission_management_operations: true, @@ -2167,7 +2167,7 @@ mod tests { assert!(access_control_config.contains("foo.bar=true")); assert!(access_control_config.contains("opa.allow-permission-management-operations=false")); assert!(access_control_config.contains(r#"opa.policy.batched-uri=http\://simple-opa.default.svc.cluster.local\:8081/v1/data/my-product/batch-new"#)); - assert!(access_control_config.contains(r#"opa.policy.column-masking-uri=http\://simple-opa.default.svc.cluster.local\:8081/v1/data/my-product/columnMask"#)); + assert!(access_control_config.contains(r#"opa.policy.batch-column-masking-uri=http\://simple-opa.default.svc.cluster.local\:8081/v1/data/my-product/batchColumnMasks"#)); assert!(access_control_config.contains(r#"opa.policy.row-filters-uri=http\://simple-opa.default.svc.cluster.local\:8081/v1/data/my-product/rowFilters"#)); assert!(access_control_config.contains(r#"opa.policy.uri=http\://simple-opa.default.svc.cluster.local\:8081/v1/data/my-product/allow"#)); } diff --git a/tests/templates/kuttl/opa-authorization/trino_rules/schema/input.json b/tests/templates/kuttl/opa-authorization/trino_rules/schema/input.json index f0149767..27f35f73 100644 --- a/tests/templates/kuttl/opa-authorization/trino_rules/schema/input.json +++ b/tests/templates/kuttl/opa-authorization/trino_rules/schema/input.json @@ -517,6 +517,18 @@ }, "GetColumnMask": { + "type": "object", + "oneOf": [ + { + "$ref": "#/$defs/SingleColumnMask" + }, + { + "$ref": "#/$defs/BatchColumnMasks" + } + ] + }, + + "SingleColumnMask": { "type": "object", "properties": { "operation": { @@ -562,6 +574,55 @@ "required": ["operation", "resource"] }, + "BatchColumnMasks": { + "type": "object", + "properties": { + "operation": { + "const": "GetColumnMask" + }, + "filterResources": { + "type": "array", + "items": { + "type": "object", + "properties": { + "column": { + "type": "object", + "properties": { + "catalogName": { + "type": "string" + }, + "schemaName": { + "type": "string" + }, + "tableName": { + "type": "string" + }, + "columnName": { + "type": "string" + }, + "columnType": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "catalogName", + "schemaName", + "tableName", + "columnName", + "columnType" + ] + } + }, + "additionalProperties": false, + "required": ["column"] + } + } + }, + "additionalProperties": false, + "required": ["operation", "filterResources"] + }, + "GetRowFilters": { "type": "object", "properties": { diff --git a/tests/templates/kuttl/opa-authorization/trino_rules/trino/verification.rego b/tests/templates/kuttl/opa-authorization/trino_rules/trino/verification.rego index 9e001fc1..f5a83a42 100644 --- a/tests/templates/kuttl/opa-authorization/trino_rules/trino/verification.rego +++ b/tests/templates/kuttl/opa-authorization/trino_rules/trino/verification.rego @@ -5,6 +5,7 @@ # - allow # - batch # - columnMask +# - batchColumnMasks # - rowFilters # These rules use the rules and functions in requested_permission.rego # and actual_permissions.rego to calculate the result. @@ -302,6 +303,88 @@ columnMask := column_mask if { column_mask := {"expression": column.mask} } +# METADATA +# description: | +# Entry point for fetching column masks in batch, configured in the +# Trino property `opa.policy.batch-column-masking-uri`. +# +# The input has the following form: +# +# { +# "action": { +# "operation": "GetColumnMasks", +# "filterResources": [{ +# "column": { +# "catalogName": "catalog", +# "schemaName": "schema", +# "tableName": "table", +# "columnName": "column", +# }}, +# {"column": ...}, +# ... +# ], +# }, +# "context": { +# "identity": { +# "groups": ["group1", ...], +# "user": "username", +# }, +# "softwareStack": {"trinoVersion": "455"}, +# } +# } +# +# The batchColumnMask rule queries the column constraints in the +# Trino policies for each of the resources in the "filterResources" +# list of the request and returns a list of viewExpressions, containing +# the column mask if any set and optionally the identity for the mask +# evaluation, and the index of the corresponding resource in the +# "filterResources" list of the request. +# A column mask is an SQL expression, +# e.g. "'XXX-XX-' + substring(credit_card, -4)". +# entrypoint: true +batchColumnMasks contains column_mask if { + input.action.operation == "GetColumnMask" + some index, resource in input.action.filterResources + + column := column_constraints( + resource.column.catalogName, + resource.column.schemaName, + resource.column.tableName, + resource.column.columnName, + ) + + is_string(column.mask) + is_string(column.mask_environment.user) + + column_mask := { + "index": index, + "viewExpression": { + "expression": column.mask, + "identity": column.mask_environment.user, + }, + } +} + +batchColumnMasks contains column_mask if { + input.action.operation == "GetColumnMask" + some index, resource in input.action.filterResources + + column := column_constraints( + resource.column.catalogName, + resource.column.schemaName, + resource.column.tableName, + resource.column.columnName, + ) + + is_string(column.mask) + is_null(column.mask_environment.user) + + column_mask := { + "index": index, + "viewExpression": {"expression": column.mask}, + } +} + # METADATA # description: | # Entry point for fetching row filters, configured in the Trino From 76aeaf582db05750ed707ea4a28ed0cbc82a4021 Mon Sep 17 00:00:00 2001 From: xeniape Date: Tue, 6 Jan 2026 17:07:40 +0100 Subject: [PATCH 2/8] add/adjust tests, add crd field to disable column masking --- deploy/helm/trino-operator/crds/crds.yaml | 3 + rust/operator-binary/src/authorization/opa.rs | 31 ++- rust/operator-binary/src/crd/mod.rs | 15 ++ .../20-install-trino.yaml.j2 | 225 ++++++++++-------- .../trino_rules/trino/verification_test.rego | 83 +++++++ .../kuttl/smoke/09-install-opa.yaml.j2 | 2 + .../kuttl/smoke/10-install-trino.yaml.j2 | 13 + .../kuttl/smoke_aws/09-install-opa.yaml.j2 | 2 + 8 files changed, 260 insertions(+), 114 deletions(-) diff --git a/deploy/helm/trino-operator/crds/crds.yaml b/deploy/helm/trino-operator/crds/crds.yaml index 9f66ef66..e6e96564 100644 --- a/deploy/helm/trino-operator/crds/crds.yaml +++ b/deploy/helm/trino-operator/crds/crds.yaml @@ -71,6 +71,9 @@ spec: Learn more in the [Trino authorization usage guide](https://docs.stackable.tech/home/nightly/trino/usage-guide/security#authorization). nullable: true properties: + disableColumnMasking: + default: false + type: boolean opa: description: |- Configure the OPA stacklet [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) diff --git a/rust/operator-binary/src/authorization/opa.rs b/rust/operator-binary/src/authorization/opa.rs index c68f31e1..12dc34ca 100644 --- a/rust/operator-binary/src/authorization/opa.rs +++ b/rust/operator-binary/src/authorization/opa.rs @@ -65,15 +65,21 @@ impl TrinoOpaConfig { OpaApiVersion::V1, ) .await?; - let batched_column_masking_connection_string = opa_config - .full_document_url_from_config_map( - client, - trino, - // Sticking to https://github.com/trinodb/trino/blob/455/plugin/trino-opa/src/test/java/io/trino/plugin/opa/TestOpaAccessControlDataFilteringSystem.java#L48 - Some("batchColumnMasks"), - OpaApiVersion::V1, - ) - .await?; + + let mut optional_batched_column_masking_connection_string = None; + if trino.column_masking_enabled() { + let batched_column_masking_connection_string = opa_config + .full_document_url_from_config_map( + client, + trino, + // Sticking to https://github.com/trinodb/trino/blob/455/plugin/trino-opa/src/test/java/io/trino/plugin/opa/TestOpaAccessControlDataFilteringSystem.java#L48 + Some("batchColumnMasks"), + OpaApiVersion::V1, + ) + .await?; + optional_batched_column_masking_connection_string = + Some(batched_column_masking_connection_string); + } let tls_secret_class = client .get::( @@ -89,7 +95,8 @@ impl TrinoOpaConfig { non_batched_connection_string, batched_connection_string, row_filters_connection_string: Some(row_filters_connection_string), - batched_column_masking_connection_string: Some(batched_column_masking_connection_string), + batched_column_masking_connection_string: + optional_batched_column_masking_connection_string, allow_permission_management_operations: true, tls_secret_class, }) @@ -113,7 +120,9 @@ impl TrinoOpaConfig { Some(row_filters_connection_string.clone()), ); } - if let Some(batched_column_masking_connection_string) = &self.batched_column_masking_connection_string { + if let Some(batched_column_masking_connection_string) = + &self.batched_column_masking_connection_string + { config.insert( "opa.policy.batch-column-masking-uri".to_string(), Some(batched_column_masking_connection_string.clone()), diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 61e7522a..30b01e36 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -313,6 +313,8 @@ pub mod versioned { #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct TrinoAuthorization { + #[serde(default = "TrinoAuthorization::disabled_column_masking_default")] + pub disable_column_masking: bool, // no doc - it's in the struct. #[serde(default, skip_serializing_if = "Option::is_none")] pub opa: Option, @@ -365,6 +367,12 @@ pub mod versioned { } } +impl v1alpha1::TrinoAuthorization { + pub fn disabled_column_masking_default() -> bool { + false + } +} + impl Default for v1alpha1::TrinoCoordinatorRoleConfig { fn default() -> Self { v1alpha1::TrinoCoordinatorRoleConfig { @@ -877,6 +885,13 @@ impl v1alpha1::TrinoCluster { !spec.cluster_config.authentication.is_empty() } + pub fn column_masking_enabled(&self) -> bool { + match self.spec.cluster_config.authorization.as_ref() { + Some(a) => !a.disable_column_masking, + None => !v1alpha1::TrinoAuthorization::disabled_column_masking_default(), + } + } + pub fn get_opa_config(&self) -> Option<&OpaConfig> { self.spec .cluster_config diff --git a/tests/templates/kuttl/opa-authorization/20-install-trino.yaml.j2 b/tests/templates/kuttl/opa-authorization/20-install-trino.yaml.j2 index be67c0a8..fd82875d 100644 --- a/tests/templates/kuttl/opa-authorization/20-install-trino.yaml.j2 +++ b/tests/templates/kuttl/opa-authorization/20-install-trino.yaml.j2 @@ -1,110 +1,129 @@ --- -apiVersion: trino.stackable.tech/v1alpha1 -kind: TrinoCluster -metadata: - name: trino -spec: - image: +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: | + kubectl apply -n $NAMESPACE -f - < 0 %} - custom: "{{ test_scenario['values']['trino'].split(',')[1] }}" - productVersion: "{{ test_scenario['values']['trino'].split(',')[0] }}" + custom: "{{ test_scenario['values']['trino'].split(',')[1] }}" + productVersion: "{{ test_scenario['values']['trino'].split(',')[0] }}" {% else %} - productVersion: "{{ test_scenario['values']['trino'] }}" + productVersion: "{{ test_scenario['values']['trino'] }}" {% endif %} - pullPolicy: IfNotPresent - clusterConfig: - catalogLabelSelector: - matchLabels: - trino: trino - authentication: - - authenticationClass: trino-users-auth - authorization: - opa: - configMapName: opa - package: trino + pullPolicy: IfNotPresent + clusterConfig: + catalogLabelSelector: + matchLabels: + trino: trino + authentication: + - authenticationClass: trino-users-auth + authorization: +{% if test_scenario['values']['trino'] == "451" %} + disableColumnMasking: true +{% endif %} + opa: + configMapName: opa + package: trino {% if lookup('env', 'VECTOR_AGGREGATOR') %} - vectorAggregatorConfigMapName: vector-aggregator-discovery + vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} - coordinators: - config: - logging: - enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} - roleGroups: - default: - replicas: 1 - config: {} - workers: - config: - gracefulShutdownTimeout: 10s # Let the test run faster - logging: - enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} - roleGroups: - default: - replicas: 1 - config: {} ---- -apiVersion: authentication.stackable.tech/v1alpha1 -kind: AuthenticationClass -metadata: - name: trino-users-auth -spec: - provider: - static: - userCredentialsSecret: + coordinators: + config: + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} +{% if test_scenario['values']['trino'] == "451" %} + configOverrides: + access-control.properties: + opa.policy.column-masking-uri: "https://opa-server.$NAMESPACE.svc.cluster.local:8443/v1/data/trino/columnMask" +{% endif %} + roleGroups: + default: + replicas: 1 + config: {} + workers: + config: + gracefulShutdownTimeout: 10s # Let the test run faster + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} +{% if test_scenario['values']['trino'] == "451" %} + configOverrides: + access-control.properties: + opa.policy.column-masking-uri: "https://opa-server.$NAMESPACE.svc.cluster.local:8443/v1/data/trino/columnMask" +{% endif %} + roleGroups: + default: + replicas: 1 + config: {} + --- + apiVersion: authentication.stackable.tech/v1alpha1 + kind: AuthenticationClass + metadata: + name: trino-users-auth + spec: + provider: + static: + userCredentialsSecret: + name: trino-users + --- + apiVersion: v1 + kind: Secret + metadata: name: trino-users ---- -apiVersion: v1 -kind: Secret -metadata: - name: trino-users -type: kubernetes.io/opaque -stringData: - admin: admin - banned-user: banned-user - group-user: group-user - lakehouse: lakehouse - iceberg: iceberg ---- -apiVersion: trino.stackable.tech/v1alpha1 -kind: TrinoCatalog -metadata: - name: lakehouse - labels: - trino: trino -spec: - connector: - tpch: {} ---- -apiVersion: trino.stackable.tech/v1alpha1 -kind: TrinoCatalog -metadata: - name: tpch - labels: - trino: trino -spec: - connector: - tpch: {} ---- -apiVersion: trino.stackable.tech/v1alpha1 -kind: TrinoCatalog -metadata: - name: tpcds - labels: - trino: trino -spec: - connector: - tpcds: {} ---- -apiVersion: trino.stackable.tech/v1alpha1 -kind: TrinoCatalog -metadata: - name: iceberg - labels: - trino: trino -spec: - connector: - iceberg: - metastore: - configMap: hive # It's fine to reuse the existing HMS for tests. Not recommended for production though, there a dedicated HMS should be used. - s3: - reference: minio + type: kubernetes.io/opaque + stringData: + admin: admin + banned-user: banned-user + group-user: group-user + lakehouse: lakehouse + iceberg: iceberg + --- + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCatalog + metadata: + name: lakehouse + labels: + trino: trino + spec: + connector: + tpch: {} + --- + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCatalog + metadata: + name: tpch + labels: + trino: trino + spec: + connector: + tpch: {} + --- + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCatalog + metadata: + name: tpcds + labels: + trino: trino + spec: + connector: + tpcds: {} + --- + apiVersion: trino.stackable.tech/v1alpha1 + kind: TrinoCatalog + metadata: + name: iceberg + labels: + trino: trino + spec: + connector: + iceberg: + metastore: + configMap: hive # It's fine to reuse the existing HMS for tests. Not recommended for production though, there a dedicated HMS should be used. + s3: + reference: minio diff --git a/tests/templates/kuttl/opa-authorization/trino_rules/trino/verification_test.rego b/tests/templates/kuttl/opa-authorization/trino_rules/trino/verification_test.rego index 007c5868..bb505bb1 100644 --- a/tests/templates/kuttl/opa-authorization/trino_rules/trino/verification_test.rego +++ b/tests/templates/kuttl/opa-authorization/trino_rules/trino/verification_test.rego @@ -436,6 +436,89 @@ test_column_mask_with_no_matching_rule if { with data.trino_policies.policies as policies } +test_batch_column_mask_with_expression_and_optional_identity if { + request := { + "action": { + "operation": "GetColumnMask", + "filterResources": [ + {"column": { + "catalogName": "testcatalog", + "schemaName": "testschema", + "tableName": "testtable", + "columnName": "testcolumn", + }}, + {"column": { + "catalogName": "testcatalog", + "schemaName": "testschema", + "tableName": "testtable", + "columnName": "testcolumn2", + }}, + ], + }, + "context": testcontext, + } + policies := {"tables": [{ + "privileges": ["SELECT"], + "columns": [ + { + "name": "testcolumn", + "mask": "testmask", + "mask_environment": {"user": "testmaskenvironmentuser"}, + }, + { + "name": "testcolumn2", + "mask": "testmask2", + }, + ], + }]} + + column_masks := trino.batchColumnMasks with input as request + with data.trino_policies.policies as policies + + column_masks == { + { + "index": 0, + "viewExpression": { + "expression": "testmask", + "identity": "testmaskenvironmentuser", + }, + }, + { + "index": 1, + "viewExpression": {"expression": "testmask2"}, + }, + } +} + +test_batch_column_mask_with_no_matching_rule if { + request := { + "action": { + "operation": "GetColumnMask", + "filterResources": [ + {"column": { + "catalogName": "testcatalog", + "schemaName": "testschema", + "tableName": "testtable", + "columnName": "testcolumn", + }}, + {"column": { + "catalogName": "testcatalog", + "schemaName": "testschema", + "tableName": "testtable", + "columnName": "testcolumn2", + }}, + ], + }, + "context": testcontext, + } + policies := {} + + column_masks := trino.batchColumnMasks with input as request + with data.trino_policies.policies as policies + + count(column_masks) == 0 +} + test_row_filters_with_expression_and_identity if { request := { "action": { diff --git a/tests/templates/kuttl/smoke/09-install-opa.yaml.j2 b/tests/templates/kuttl/smoke/09-install-opa.yaml.j2 index dca1bebb..6f712b8d 100644 --- a/tests/templates/kuttl/smoke/09-install-opa.yaml.j2 +++ b/tests/templates/kuttl/smoke/09-install-opa.yaml.j2 @@ -57,3 +57,5 @@ data: is_bob() if { input.context.identity.user == "bob" } + + batchColumnMasks = [] diff --git a/tests/templates/kuttl/smoke/10-install-trino.yaml.j2 b/tests/templates/kuttl/smoke/10-install-trino.yaml.j2 index f73c7136..1621cf79 100644 --- a/tests/templates/kuttl/smoke/10-install-trino.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-install-trino.yaml.j2 @@ -19,6 +19,9 @@ spec: authentication: - authenticationClass: trino-users-auth authorization: +{% if test_scenario['values']['trino'] == "451" %} + disableColumnMasking: true +{% endif %} opa: configMapName: opa package: trino @@ -32,6 +35,11 @@ spec: envOverrides: COMMON_VAR: role-value # overridden by role group below ROLE_VAR: role-value # only defined here at role level +{% if test_scenario['values']['trino'] == "451" %} + configOverrides: + access-control.properties: + opa.policy.column-masking-uri: "http://opa-server:8081/v1/data/trino/columnMask" +{% endif %} roleGroups: default: replicas: 1 @@ -47,6 +55,11 @@ spec: envOverrides: COMMON_VAR: role-value # overridden by role group below ROLE_VAR: role-value # only defined here at role level +{% if test_scenario['values']['trino'] == "451" %} + configOverrides: + access-control.properties: + opa.policy.column-masking-uri: "http://opa-server:8081/v1/data/trino/columnMask" +{% endif %} roleGroups: default: replicas: 1 diff --git a/tests/templates/kuttl/smoke_aws/09-install-opa.yaml.j2 b/tests/templates/kuttl/smoke_aws/09-install-opa.yaml.j2 index dca1bebb..6f712b8d 100644 --- a/tests/templates/kuttl/smoke_aws/09-install-opa.yaml.j2 +++ b/tests/templates/kuttl/smoke_aws/09-install-opa.yaml.j2 @@ -57,3 +57,5 @@ data: is_bob() if { input.context.identity.user == "bob" } + + batchColumnMasks = [] From a607d95fe7fca30c0ea4298bd2a5b2e4dc45a39b Mon Sep 17 00:00:00 2001 From: xeniape Date: Wed, 7 Jan 2026 13:44:58 +0100 Subject: [PATCH 3/8] incorporate decision feedback --- deploy/helm/trino-operator/crds/crds.yaml | 6 +-- rust/operator-binary/src/authorization/opa.rs | 48 ++++++++++--------- rust/operator-binary/src/crd/mod.rs | 27 +++++++---- rust/operator-binary/src/main.rs | 2 +- 4 files changed, 47 insertions(+), 36 deletions(-) diff --git a/deploy/helm/trino-operator/crds/crds.yaml b/deploy/helm/trino-operator/crds/crds.yaml index e6e96564..4e16bdbd 100644 --- a/deploy/helm/trino-operator/crds/crds.yaml +++ b/deploy/helm/trino-operator/crds/crds.yaml @@ -71,9 +71,6 @@ spec: Learn more in the [Trino authorization usage guide](https://docs.stackable.tech/home/nightly/trino/usage-guide/security#authorization). nullable: true properties: - disableColumnMasking: - default: false - type: boolean opa: description: |- Configure the OPA stacklet [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) @@ -87,6 +84,9 @@ spec: The [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery) for the OPA stacklet that should be used for authorization requests. type: string + enableColumnMasking: + default: true + type: boolean package: description: The name of the Rego package containing the Rego rules for the product. nullable: true diff --git a/rust/operator-binary/src/authorization/opa.rs b/rust/operator-binary/src/authorization/opa.rs index 12dc34ca..216643d6 100644 --- a/rust/operator-binary/src/authorization/opa.rs +++ b/rust/operator-binary/src/authorization/opa.rs @@ -1,13 +1,11 @@ use std::collections::BTreeMap; use stackable_operator::{ - client::Client, - commons::opa::{OpaApiVersion, OpaConfig}, - k8s_openapi::api::core::v1::ConfigMap, + client::Client, commons::opa::OpaApiVersion, k8s_openapi::api::core::v1::ConfigMap, kube::ResourceExt, }; -use crate::crd::v1alpha1::TrinoCluster; +use crate::crd::v1alpha1; pub const OPA_TLS_VOLUME_NAME: &str = "opa-tls"; @@ -41,13 +39,15 @@ pub struct TrinoOpaConfig { impl TrinoOpaConfig { pub async fn from_opa_config( client: &Client, - trino: &TrinoCluster, - opa_config: &OpaConfig, + trino: &v1alpha1::TrinoCluster, + opa_config: &v1alpha1::TrinoAuthorizationOpaConfig, ) -> Result { let non_batched_connection_string = opa_config + .opa .full_document_url_from_config_map(client, trino, Some("allow"), OpaApiVersion::V1) .await?; let batched_connection_string = opa_config + .opa .full_document_url_from_config_map( client, trino, @@ -57,6 +57,7 @@ impl TrinoOpaConfig { ) .await?; let row_filters_connection_string = opa_config + .opa .full_document_url_from_config_map( client, trino, @@ -66,24 +67,26 @@ impl TrinoOpaConfig { ) .await?; - let mut optional_batched_column_masking_connection_string = None; - if trino.column_masking_enabled() { - let batched_column_masking_connection_string = opa_config - .full_document_url_from_config_map( - client, - trino, - // Sticking to https://github.com/trinodb/trino/blob/455/plugin/trino-opa/src/test/java/io/trino/plugin/opa/TestOpaAccessControlDataFilteringSystem.java#L48 - Some("batchColumnMasks"), - OpaApiVersion::V1, - ) - .await?; - optional_batched_column_masking_connection_string = - Some(batched_column_masking_connection_string); - } + let batched_column_masking_connection_string = if trino.column_masking_enabled() { + Some( + opa_config + .opa + .full_document_url_from_config_map( + client, + trino, + // Sticking to https://github.com/trinodb/trino/blob/455/plugin/trino-opa/src/test/java/io/trino/plugin/opa/TestOpaAccessControlDataFilteringSystem.java#L48 + Some("batchColumnMasks"), + OpaApiVersion::V1, + ) + .await?, + ) + } else { + None + }; let tls_secret_class = client .get::( - &opa_config.config_map_name, + &opa_config.opa.config_map_name, trino.namespace().as_deref().unwrap_or("default"), ) .await @@ -95,8 +98,7 @@ impl TrinoOpaConfig { non_batched_connection_string, batched_connection_string, row_filters_connection_string: Some(row_filters_connection_string), - batched_column_masking_connection_string: - optional_batched_column_masking_connection_string, + batched_column_masking_connection_string, allow_permission_management_operations: true, tls_secret_class, }) diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 30b01e36..2fafb57d 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -313,11 +313,20 @@ pub mod versioned { #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct TrinoAuthorization { - #[serde(default = "TrinoAuthorization::disabled_column_masking_default")] - pub disable_column_masking: bool, // no doc - it's in the struct. #[serde(default, skip_serializing_if = "Option::is_none")] - pub opa: Option, + pub opa: Option, + } + + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct TrinoAuthorizationOpaConfig { + // no doc - it's in the struct. + #[serde(flatten)] + pub opa: OpaConfig, + + #[serde(default = "TrinoAuthorization::enabled_column_masking_default")] + pub enable_column_masking: bool, } #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] @@ -368,8 +377,8 @@ pub mod versioned { } impl v1alpha1::TrinoAuthorization { - pub fn disabled_column_masking_default() -> bool { - false + pub fn enabled_column_masking_default() -> bool { + true } } @@ -886,13 +895,13 @@ impl v1alpha1::TrinoCluster { } pub fn column_masking_enabled(&self) -> bool { - match self.spec.cluster_config.authorization.as_ref() { - Some(a) => !a.disable_column_masking, - None => !v1alpha1::TrinoAuthorization::disabled_column_masking_default(), + match self.get_opa_config() { + Some(a) => a.enable_column_masking, + None => v1alpha1::TrinoAuthorization::enabled_column_masking_default(), } } - pub fn get_opa_config(&self) -> Option<&OpaConfig> { + pub fn get_opa_config(&self) -> Option<&v1alpha1::TrinoAuthorizationOpaConfig> { self.spec .cluster_config .authorization diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 0c49d09e..39a3331b 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -239,7 +239,7 @@ fn references_config_map( match &trino.spec.cluster_config.authorization { Some(trino_authorization) => match &trino_authorization.opa { - Some(opa_config) => opa_config.config_map_name == config_map.name_any(), + Some(opa_config) => opa_config.opa.config_map_name == config_map.name_any(), None => false, }, None => false, From b1addca3f937c2cdfd4c861cbcd61fb779327453 Mon Sep 17 00:00:00 2001 From: xeniape Date: Wed, 7 Jan 2026 20:53:39 +0100 Subject: [PATCH 4/8] adjust tests --- .../kuttl/opa-authorization/20-install-trino.yaml.j2 | 6 +++--- tests/templates/kuttl/smoke/10-install-trino.yaml.j2 | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/templates/kuttl/opa-authorization/20-install-trino.yaml.j2 b/tests/templates/kuttl/opa-authorization/20-install-trino.yaml.j2 index fd82875d..ce42a72a 100644 --- a/tests/templates/kuttl/opa-authorization/20-install-trino.yaml.j2 +++ b/tests/templates/kuttl/opa-authorization/20-install-trino.yaml.j2 @@ -25,12 +25,12 @@ commands: authentication: - authenticationClass: trino-users-auth authorization: -{% if test_scenario['values']['trino'] == "451" %} - disableColumnMasking: true -{% endif %} opa: configMapName: opa package: trino +{% if test_scenario['values']['trino'] == "451" %} + enableColumnMasking: false +{% endif %} {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} diff --git a/tests/templates/kuttl/smoke/10-install-trino.yaml.j2 b/tests/templates/kuttl/smoke/10-install-trino.yaml.j2 index 1621cf79..c3474adc 100644 --- a/tests/templates/kuttl/smoke/10-install-trino.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-install-trino.yaml.j2 @@ -19,12 +19,12 @@ spec: authentication: - authenticationClass: trino-users-auth authorization: -{% if test_scenario['values']['trino'] == "451" %} - disableColumnMasking: true -{% endif %} opa: configMapName: opa package: trino +{% if test_scenario['values']['trino'] == "451" %} + enableColumnMasking: false +{% endif %} {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} From df98f6c6660e73c3ed083f3e4969bfc4e61b6378 Mon Sep 17 00:00:00 2001 From: xeniape Date: Thu, 8 Jan 2026 12:23:40 +0100 Subject: [PATCH 5/8] make `opa` within `authorization` a required enum variant --- deploy/helm/trino-operator/crds/crds.yaml | 4 +++- rust/operator-binary/src/crd/mod.rs | 14 +++++++++----- rust/operator-binary/src/main.rs | 5 ++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/deploy/helm/trino-operator/crds/crds.yaml b/deploy/helm/trino-operator/crds/crds.yaml index 4e16bdbd..fb8004ee 100644 --- a/deploy/helm/trino-operator/crds/crds.yaml +++ b/deploy/helm/trino-operator/crds/crds.yaml @@ -70,6 +70,9 @@ spec: Authorization options for Trino. Learn more in the [Trino authorization usage guide](https://docs.stackable.tech/home/nightly/trino/usage-guide/security#authorization). nullable: true + oneOf: + - required: + - opa properties: opa: description: |- @@ -77,7 +80,6 @@ spec: and the name of the Rego package containing your authorization rules. Consult the [OPA authorization documentation](https://docs.stackable.tech/home/nightly/concepts/opa) to learn how to deploy Rego authorization rules with OPA. - nullable: true properties: configMapName: description: |- diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 2fafb57d..474b36f0 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -312,10 +312,12 @@ pub mod versioned { #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] - pub struct TrinoAuthorization { - // no doc - it's in the struct. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub opa: Option, + pub enum TrinoAuthorization { + Opa { + // no doc - it's in the struct. + #[serde(default, flatten)] + config: TrinoAuthorizationOpaConfig, + }, } #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] @@ -906,7 +908,9 @@ impl v1alpha1::TrinoCluster { .cluster_config .authorization .as_ref() - .and_then(|a| a.opa.as_ref()) + .and_then(|a| match a { + v1alpha1::TrinoAuthorization::Opa { config } => Some(config), + }) } /// Return user provided server TLS settings diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 39a3331b..fa2bb0f7 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -238,9 +238,8 @@ fn references_config_map( }; match &trino.spec.cluster_config.authorization { - Some(trino_authorization) => match &trino_authorization.opa { - Some(opa_config) => opa_config.opa.config_map_name == config_map.name_any(), - None => false, + Some(trino_authorization) => match &trino_authorization { + v1alpha1::TrinoAuthorization::Opa { config } => config.opa.config_map_name == config_map.name_any(), }, None => false, } From 1320aef2804fb39639b0c51eab0af2227a6b3e64 Mon Sep 17 00:00:00 2001 From: xeniape Date: Mon, 12 Jan 2026 08:40:24 +0100 Subject: [PATCH 6/8] pre-commit fixes --- rust/operator-binary/src/crd/mod.rs | 4 ++-- rust/operator-binary/src/main.rs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 474b36f0..6f2012d2 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -908,8 +908,8 @@ impl v1alpha1::TrinoCluster { .cluster_config .authorization .as_ref() - .and_then(|a| match a { - v1alpha1::TrinoAuthorization::Opa { config } => Some(config), + .map(|a| match a { + v1alpha1::TrinoAuthorization::Opa { config } => config, }) } diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index fa2bb0f7..4f1536fa 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -239,7 +239,9 @@ fn references_config_map( match &trino.spec.cluster_config.authorization { Some(trino_authorization) => match &trino_authorization { - v1alpha1::TrinoAuthorization::Opa { config } => config.opa.config_map_name == config_map.name_any(), + v1alpha1::TrinoAuthorization::Opa { config } => { + config.opa.config_map_name == config_map.name_any() + } }, None => false, } From 2d4c4ad3f4a1c3f4fefd563e4b0226603ee077c0 Mon Sep 17 00:00:00 2001 From: xeniape Date: Mon, 12 Jan 2026 13:14:04 +0100 Subject: [PATCH 7/8] refactor code, add documenation --- deploy/helm/trino-operator/crds/crds.yaml | 1 + .../trino/pages/reference/security.adoc | 39 +++++++++++++++++++ .../trino/pages/usage-guide/overrides.adoc | 5 +++ docs/modules/trino/partials/nav.adoc | 1 + rust/operator-binary/src/crd/mod.rs | 7 ++-- 5 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 docs/modules/trino/pages/reference/security.adoc diff --git a/deploy/helm/trino-operator/crds/crds.yaml b/deploy/helm/trino-operator/crds/crds.yaml index fb8004ee..3b49262d 100644 --- a/deploy/helm/trino-operator/crds/crds.yaml +++ b/deploy/helm/trino-operator/crds/crds.yaml @@ -88,6 +88,7 @@ spec: type: string enableColumnMasking: default: true + description: Set the OPA batched column masking uri for Trino queries or not. Defaults to true. type: boolean package: description: The name of the Rego package containing the Rego rules for the product. diff --git a/docs/modules/trino/pages/reference/security.adoc b/docs/modules/trino/pages/reference/security.adoc new file mode 100644 index 00000000..61e9d8f1 --- /dev/null +++ b/docs/modules/trino/pages/reference/security.adoc @@ -0,0 +1,39 @@ += Security + +== Authorization + +=== OPA + +==== Column masking + +===== CRD configuration + +[source,yaml] +---- +apiVersion: trino.stackable.tech/v1alpha1 +kind: TrinoCluster # <1> +spec: + clusterConfig: + authorization: + opa: + enableColumnMasking: true # default +---- + +<1> Redundant fields for column masking reference configuration are omitted + +===== Result + +In `access-control.properties` the following value is set, when `enableColumnMasking` is set to `true`: + +[.wrap,source] +---- +opa.policy.batch-column-masking-uri=/v1/data//batchColumnMasks # <1> <2> +---- + +<1> `` is read from the OPA discovery ConfigMap +<2> `` is read from `spec.clusterConfig.authorization.opa.package` if set, otherwise defaults to the TrinoCluster name + +===== Considerations + +The default setting for `enableColumnMasking` assumes a `batchColumnMasks` rule is defined in the Rego rules for the TrinoCluster. +If there is no such rule defined, Trino queries, using that column masking endpoint, will fail. diff --git a/docs/modules/trino/pages/usage-guide/overrides.adoc b/docs/modules/trino/pages/usage-guide/overrides.adoc index d808878d..6bc7d5a9 100644 --- a/docs/modules/trino/pages/usage-guide/overrides.adoc +++ b/docs/modules/trino/pages/usage-guide/overrides.adoc @@ -27,8 +27,13 @@ Confiuration overrides are applied like so: Configuration overrides can be applied to: +* `access-control.properties` * `config.properties` * `node.properties` +* `password-authenticator.properties` +* `security.properties` +* `exchange-manager.properties` +* `spooling-manager.properties` === Configuration overrides in the TrinoCatalog diff --git a/docs/modules/trino/partials/nav.adoc b/docs/modules/trino/partials/nav.adoc index 02dbac77..453c0fa0 100644 --- a/docs/modules/trino/partials/nav.adoc +++ b/docs/modules/trino/partials/nav.adoc @@ -33,5 +33,6 @@ ** xref:trino:reference/crds.adoc[] *** {crd-docs}/trino.stackable.tech/trinocluster/v1alpha1/[TrinoCluster {external-link-icon}^] *** {crd-docs}/trino.stackable.tech/trinocatalog/v1alpha1/[TrinoCatalog {external-link-icon}^] +** xref:trino:reference/security.adoc[] ** xref:trino:reference/commandline-parameters.adoc[] ** xref:trino:reference/environment-variables.adoc[] diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 6f2012d2..83a00f2b 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -327,7 +327,8 @@ pub mod versioned { #[serde(flatten)] pub opa: OpaConfig, - #[serde(default = "TrinoAuthorization::enabled_column_masking_default")] + /// Set the OPA batched column masking uri for Trino queries or not. Defaults to true. + #[serde(default = "TrinoAuthorizationOpaConfig::enabled_column_masking_default")] pub enable_column_masking: bool, } @@ -378,7 +379,7 @@ pub mod versioned { } } -impl v1alpha1::TrinoAuthorization { +impl v1alpha1::TrinoAuthorizationOpaConfig { pub fn enabled_column_masking_default() -> bool { true } @@ -899,7 +900,7 @@ impl v1alpha1::TrinoCluster { pub fn column_masking_enabled(&self) -> bool { match self.get_opa_config() { Some(a) => a.enable_column_masking, - None => v1alpha1::TrinoAuthorization::enabled_column_masking_default(), + None => v1alpha1::TrinoAuthorizationOpaConfig::enabled_column_masking_default(), } } From 06b2878bae10d8eed4e5c21f2ae1920fec839504 Mon Sep 17 00:00:00 2001 From: xeniape Date: Mon, 12 Jan 2026 14:03:55 +0100 Subject: [PATCH 8/8] add changelog entries --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb07b2d..c2c014de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Add `enabledColumnMasking` field to `opa` configuration in `authorization` ([#827]). +- Support batched column masks in Rego rules ([#827]). + +### Changed + +- BREAKING: The field `opa` in `authorization` is now a mandatory enum variant instead of being optional ([#827]). +- BREAKING: The operator no longer sets `opa.policy.column-masking-uri` in `access-control.properties` but + `opa.policy.batch-column-masking-uri` instead, allowing Trino to fetch multiple column masks in a single request ([#827]). + +[#827]: https://github.com/stackabletech/trino-operator/pull/827 + ## [25.11.0] - 2025-11-07 ## [25.11.0-rc1] - 2025-11-06