You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by be...@apache.org on 2025/12/17 22:29:30 UTC

(superset) 02/03: RLS and CLS working

This is an automated email from the ASF dual-hosted git repository.

beto pushed a commit to branch new-dar
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 2e7df4614c8ccc7dbcd3c57888f3ea8f5769c585
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Wed Dec 17 17:08:58 2025 -0500

    RLS and CLS working
---
 superset/data_access_rules/api.py   |  4 ++--
 superset/data_access_rules/utils.py | 47 +++++++++++++++++++++++++++++++++----
 superset/sql/parse.py               |  3 +++
 3 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/superset/data_access_rules/api.py b/superset/data_access_rules/api.py
index c7c9e27f5b..fcd54474fb 100644
--- a/superset/data_access_rules/api.py
+++ b/superset/data_access_rules/api.py
@@ -31,7 +31,6 @@ from marshmallow import ValidationError
 from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
 from superset.data_access_rules.models import DataAccessRule
 from superset.data_access_rules.schemas import (
-    DataAccessRuleListSchema,
     DataAccessRulePostSchema,
     DataAccessRulePutSchema,
     DataAccessRuleShowSchema,
@@ -109,7 +108,8 @@ class DataAccessRulesRestApi(BaseSupersetModelRestApi):
 
     add_model_schema = DataAccessRulePostSchema()
     edit_model_schema = DataAccessRulePutSchema()
-    list_model_schema = DataAccessRuleListSchema()
+    # Don't use custom list_model_schema - let Flask-AppBuilder handle
+    # nested relationships via list_columns dot notation
     show_model_schema = DataAccessRuleShowSchema()
 
     openapi_spec_methods = {
diff --git a/superset/data_access_rules/utils.py b/superset/data_access_rules/utils.py
index 31689c8b4f..90582b7291 100644
--- a/superset/data_access_rules/utils.py
+++ b/superset/data_access_rules/utils.py
@@ -551,14 +551,51 @@ def apply_data_access_rules(
         if table_cls:
             cls_rules[qualified_table] = table_cls
 
-    # Apply RLS if we have predicates
+    # Apply CLS first (before RLS) so that hidden columns are removed
+    # before RLS wraps the query in a subquery
+    if cls_rules:
+        # Build schema dict for sqlglot's qualify() to expand SELECT *
+        # sqlglot expects nested format: {catalog: {schema: {table: {col: type}}}}
+        # or {schema: {table: {col: type}}} without catalog
+        table_schemas: dict[str, Any] = {}
+        for table in cls_rules.keys():
+            try:
+                columns = database.get_columns(table)
+                col_types = {
+                    col["column_name"]: str(col.get("type", "VARCHAR"))
+                    for col in columns
+                }
+
+                # Build nested structure for sqlglot
+                if table.catalog:
+                    if table.catalog not in table_schemas:
+                        table_schemas[table.catalog] = {}
+                    if table.schema:
+                        if table.schema not in table_schemas[table.catalog]:
+                            table_schemas[table.catalog][table.schema] = {}
+                        table_schemas[table.catalog][table.schema][table.table] = col_types
+                    else:
+                        table_schemas[table.catalog][table.table] = col_types
+                elif table.schema:
+                    if table.schema not in table_schemas:
+                        table_schemas[table.schema] = {}
+                    table_schemas[table.schema][table.table] = col_types
+                else:
+                    table_schemas[table.table] = col_types
+            except Exception as ex:
+                logger.warning(
+                    "Could not fetch schema for table %s: %s",
+                    table,
+                    ex,
+                )
+
+        parsed_statement.apply_cls(cls_rules, schema=table_schemas if table_schemas else None)
+
+    # Apply RLS after CLS - RLS wraps the query in a subquery with SELECT *
+    # which will pick up the already-transformed columns from CLS
     if rls_predicates:
         parsed_statement.apply_rls(catalog, schema, rls_predicates, method)
 
-    # Apply CLS if we have rules
-    if cls_rules:
-        parsed_statement.apply_cls(cls_rules)
-
 
 def get_allowed_tables(
     database_name: str,
diff --git a/superset/sql/parse.py b/superset/sql/parse.py
index 3d4f55010b..c674342820 100644
--- a/superset/sql/parse.py
+++ b/superset/sql/parse.py
@@ -1536,11 +1536,14 @@ class SQLStatement(BaseSQLStatement[exp.Expression]):
         # Without schema: qualifies single-table queries, partial for JOINs.
         from sqlglot.optimizer.qualify import qualify
 
+        # Only expand stars if schema is provided (from DAR with feature flag enabled)
+        # to avoid potential errors in other contexts
         self._parsed = qualify(
             self._parsed,
             schema=schema,
             dialect=self._dialect,
             validate_qualify_columns=False,
+            expand_stars=bool(schema),
         )
 
         transformer = CLSTransformer(rules, self._dialect)