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