diff --git a/ibis-server/app/model/__init__.py b/ibis-server/app/model/__init__.py
index 47b3a4d1e..af4b46964 100644
--- a/ibis-server/app/model/__init__.py
+++ b/ibis-server/app/model/__init__.py
@@ -357,7 +357,7 @@ class GcsFileConnectionInfo(BaseConnectionInfo):
class ValidateDTO(BaseModel):
manifest_str: str = manifest_str_field
- parameters: dict[str, str]
+ parameters: dict
connection_info: ConnectionInfo = connection_info_field
diff --git a/ibis-server/app/model/validator.py b/ibis-server/app/model/validator.py
index 5a3c4e931..30ecbb228 100644
--- a/ibis-server/app/model/validator.py
+++ b/ibis-server/app/model/validator.py
@@ -1,11 +1,18 @@
from __future__ import annotations
+from wren_core import (
+ RowLevelAccessControl,
+ SessionProperty,
+ to_manifest,
+ validate_rlac_rule,
+)
+
from app.mdl.rewriter import Rewriter
from app.model import NotFoundError, UnprocessableEntityError
from app.model.connector import Connector
from app.util import base64_to_dict
-rules = ["column_is_valid", "relationship_is_valid"]
+rules = ["column_is_valid", "relationship_is_valid", "rlac_condition_syntax_is_valid"]
class Validator:
@@ -13,7 +20,7 @@ def __init__(self, connector: Connector, rewriter: Rewriter):
self.connector = connector
self.rewriter = rewriter
- async def validate(self, rule: str, parameters: dict[str, str], manifest_str: str):
+ async def validate(self, rule: str, parameters: dict, manifest_str: str):
if rule not in rules:
raise RuleNotFoundError(rule)
try:
@@ -144,6 +151,45 @@ def format_result(result):
except Exception as e:
raise ValidationError(f"Exception: {type(e)}, message: {e!s}")
+ async def _validate_rlac_condition_syntax_is_valid(
+ self, parameters: dict, manifest_str: str
+ ):
+ if parameters.get("modelName") is None:
+ raise MissingRequiredParameterError("modelName")
+ if parameters.get("requiredProperties") is None:
+ raise MissingRequiredParameterError("requiredProperties")
+ if parameters.get("condition") is None:
+ raise MissingRequiredParameterError("condition")
+
+ model_name = parameters.get("modelName")
+ required_properties = parameters.get("requiredProperties")
+ condition = parameters.get("condition")
+
+ required_properties = [
+ SessionProperty(
+ name=prop["name"],
+ required=bool(prop["required"]),
+ default_expr=prop.get("defaultExpr", None),
+ )
+ for prop in required_properties
+ ]
+
+ rlac = RowLevelAccessControl(
+ name="rlac_validation",
+ required_properties=required_properties,
+ condition=condition,
+ )
+
+ manifest = to_manifest(manifest_str)
+ model = manifest.get_model(model_name)
+ if model is None:
+ raise ValueError(f"Model {model_name} not found in manifest")
+
+ try:
+ validate_rlac_rule(rlac, model)
+ except Exception as e:
+ raise ValidationError(e)
+
def _get_model(self, manifest, model_name):
models = list(filter(lambda m: m["name"] == model_name, manifest["models"]))
if len(models) == 0:
diff --git a/ibis-server/tests/routers/v3/connector/postgres/test_validate.py b/ibis-server/tests/routers/v3/connector/postgres/test_validate.py
index 126095a7c..8cd426cc7 100644
--- a/ibis-server/tests/routers/v3/connector/postgres/test_validate.py
+++ b/ibis-server/tests/routers/v3/connector/postgres/test_validate.py
@@ -119,3 +119,60 @@ async def test_validate_rule_column_is_valid_without_one_parameter(
)
assert response.status_code == 422
assert response.text == "Missing required parameter: `modelName`"
+
+
+async def test_validate_rlac_condition_syntax_is_valid(
+ client, manifest_str, connection_info
+):
+ response = await client.post(
+ url=f"{base_url}/validate/rlac_condition_syntax_is_valid",
+ json={
+ "connectionInfo": connection_info,
+ "manifestStr": manifest_str,
+ "parameters": {
+ "modelName": "orders",
+ "requiredProperties": [
+ {"name": "session_order", "required": "false"},
+ ],
+ "condition": "@session_order = o_orderkey",
+ },
+ },
+ )
+ assert response.status_code == 204
+
+ response = await client.post(
+ url=f"{base_url}/validate/rlac_condition_syntax_is_valid",
+ json={
+ "connectionInfo": connection_info,
+ "manifestStr": manifest_str,
+ "parameters": {
+ "modelName": "orders",
+ "requiredProperties": [
+ {"name": "session_order", "required": False},
+ ],
+ "condition": "@session_order = o_orderkey",
+ },
+ },
+ )
+ assert response.status_code == 204
+
+ response = await client.post(
+ url=f"{base_url}/validate/rlac_condition_syntax_is_valid",
+ json={
+ "connectionInfo": connection_info,
+ "manifestStr": manifest_str,
+ "parameters": {
+ "modelName": "orders",
+ "requiredProperties": [
+ {"name": "session_order", "required": "false"},
+ ],
+ "condition": "@session_not_found = o_orderkey",
+ },
+ },
+ )
+
+ assert response.status_code == 422
+ assert (
+ response.text
+ == "Error during planning: The session property @session_not_found is used, but not found in the session properties"
+ )
diff --git a/wren-core-base/src/mdl/py_method.rs b/wren-core-base/src/mdl/py_method.rs
index ef921aaee..a5e716fa3 100644
--- a/wren-core-base/src/mdl/py_method.rs
+++ b/wren-core-base/src/mdl/py_method.rs
@@ -19,7 +19,7 @@
#[cfg(feature = "python-binding")]
mod manifest_python_impl {
- use crate::mdl::manifest::{Manifest, Model};
+ use crate::mdl::manifest::{Manifest, Model, RowLevelAccessControl, SessionProperty};
use crate::mdl::DataSource;
use pyo3::{pymethods, PyResult};
use std::sync::Arc;
@@ -49,6 +49,16 @@ mod manifest_python_impl {
fn data_source(&self) -> PyResult