Skip to content

SQL Policy

SQL policy is an extension framework for enforcing request-scoped SQL read controls. It is useful when an API request should only access rows or tables within the caller's business scope, such as a tenant, market, region, or store list.

The open-source agent provides the framework boundary and runtime hook points. It does not ship a concrete policy engine. Deployments that need enforcement should provide their own SQL policy package.

Framework Boundary

The open-source agent owns these responsibilities:

  • Loading agent.sql_policy from agent configuration.
  • Loading the configured provider class from agent.sql_policy.provider.
  • Passing the full raw agent.sql_policy mapping to the provider.
  • Parsing request principal fields into AppContext.principal.
  • Running a generic API pre-check for required principal.* values.
  • Calling the provider before read queries reach the database.
  • Revalidating SQL after a provider rewrite.
  • Returning policy denial reasons to the tool/model layer.

The provider owns these responsibilities:

  • Defining the policy schema under agent.sql_policy.
  • Validating provider-specific policy fields.
  • Matching policies to datasources, tables, columns, or other business concepts.
  • Resolving values from the request principal.
  • Rewriting read SQL or denying the query.
  • Returning clear denial reasons that tell the model or caller what is missing.

Runtime Flow

When SQL policy is enabled, request handling follows this flow:

  1. The API auth provider creates an AppContext.
  2. Request-scoped attributes are stored on AppContext.principal.
  3. Agent configuration is loaded, including agent.sql_policy.
  4. If the raw policy config references value_from: principal.<path>, the chat API checks that each referenced principal path is present before the agent starts.
  5. The agent runs normally until a database read tool is called.
  6. DBFuncTool.read_query validates that the original SQL is read-only.
  7. The configured provider is loaded and called through enforce_read(...).
  8. If the provider denies the query, the tool returns the provider's reason and does not execute SQL.
  9. If the provider returns rewritten SQL, the agent revalidates that rewritten SQL is still read-only.
  10. The validated SQL is executed against the target datasource.

The pre-check in step 4 is intentionally generic. It scans the raw policy mapping for value_from strings that start with principal.. It does not assume a specific policy type, column name, or business field.

Configuration Contract

The open-source agent only interprets these fields:

agent:
  sql_policy:
    enabled: true
    provider: my_company.sql_policies:SqlPolicyProvider

enabled turns on the framework. provider must be a Python class path in module:Class format.

All other fields under agent.sql_policy are passed through unchanged as SqlPolicyConfig.raw. Your provider can define any schema it needs:

agent:
  sql_policy:
    enabled: true
    provider: my_company.sql_policies:SqlPolicyProvider
    policies:
      - name: tenant_scope
        type: row_filter
        applies_to:
          datasources: ["warehouse"]
          tables: ["orders"]
        condition:
          column: tenant_id
          operator: eq
          value_from: principal.tenant.id
        enforcement:
          on_read: filter
          on_unhandled: deny

In this example, the open-source agent uses enabled, provider, and the principal.tenant.id reference for pre-checking. The provider decides what policies, type, applies_to, condition, and enforcement mean.

Provider Interface

A SQL policy provider is a normal Python package installed in the same environment as datus-api. The provider class must accept a SqlPolicyConfig argument and implement enforce_read(...).

from typing import Any, Dict, Optional

from datus.tools.sql_policy import SqlPolicyConfig, EnforcementResult


class SqlPolicyProvider:
    def __init__(self, config: Optional[SqlPolicyConfig] = None) -> None:
        self.config = config or SqlPolicyConfig()
        self.policies = self.config.raw.get("policies", []) or []
        self._validate_policy_config()

    def enforce_read(
        self,
        sql: str,
        *,
        datasource: str,
        dialect: str,
        principal: Optional[Dict[str, Any]],
    ) -> EnforcementResult:
        principal = principal or {}

        # Implement policy selection and enforcement here:
        # - parse SQL and identify referenced tables
        # - match policies for the datasource and tables
        # - resolve configured values from principal
        # - return a rewritten read query or deny with a clear reason
        return EnforcementResult(allowed=True, sql=sql)

    def _validate_policy_config(self) -> None:
        # Raise an exception if required provider-specific config is invalid.
        pass

The result controls what happens next:

return EnforcementResult(
    allowed=True,
    sql=rewritten_sql,
    applied_policies=["tenant_scope"],
)
return EnforcementResult(
    allowed=False,
    reason="Missing required principal path: principal.tenant.id",
)

Use a SQL parser or database-safe query builder when rewriting SQL. Avoid string concatenation for policy predicates.

Request Principal

The principal is the request-scoped input that policy providers use for caller attributes. For the default API auth provider, principal fields are read from the X-Datus-Principal header as a JSON object:

X-Datus-Principal: {"tenant":{"id":"tenant_001"},"market_codes":["MKT300","MKT301"]}

The provider receives the parsed object:

{
    "tenant": {"id": "tenant_001"},
    "market_codes": ["MKT300", "MKT301"],
}

Policy config can reference nested fields with principal.<path>:

condition:
  column: tenant_id
  operator: eq
  value_from: principal.tenant.id

The API pre-check treats missing keys, null, empty strings, and empty arrays as missing values.

X-Datus-User-Id is separate from the SQL policy principal. It identifies a caller for session isolation and is not copied into AppContext.principal.

Provider Contract

Item Contract
agent.sql_policy.enabled Enables the SQL policy framework.
agent.sql_policy.provider Python class path in module:Class format. Required when enabled.
SqlPolicyConfig.raw Full raw agent.sql_policy mapping passed to the provider.
enforce_read(sql, datasource, dialect, principal) Called before read SQL is executed.
EnforcementResult.allowed=True The query may continue. sql can contain the original or rewritten SQL.
EnforcementResult.allowed=False The query is denied. reason is returned to the tool/model layer.
applied_policies Optional policy names for logging and diagnostics.
value_from: principal.* Optional convention used by the API pre-check to detect missing principal fields.

Error Behavior

If SQL policy is enabled but no provider is configured, enforcement fails before SQL is executed.

If the configured provider class cannot be imported, initialized, or does not implement a callable enforce_read, enforcement fails with a SQL policy provider error.

If policy config references a missing principal.* value, the chat API fails before the agent starts:

SQL_POLICY_PRINCIPAL_REQUIRED

The error message includes the missing principal path, for example principal.tenant.id.

If the provider denies a query, SQL is not executed and the tool returns the provider's reason.

If the provider rewrites SQL into a non-read statement or a multi-statement query, the read-query validator rejects it before execution.

Limits

  • The open-source agent does not include a built-in row-filter or SQL-injection review provider.
  • The framework does not define a required policy schema beyond enabled, provider, and the optional principal.* pre-check convention.
  • CLI requests do not have HTTP headers. Request-scoped API principal input is available through the API auth context. For CLI or custom deployments, populate AppContext.principal through the relevant auth or runtime integration.
  • Do not put user_id inside X-Datus-Principal; user_id is reserved for X-Datus-User-Id.