MATIH Platform is in active MVP development. Documentation reflects current implementation status.
9. Query Engine & SQL
Row Level Security

Row-Level Security

Row-Level Security (RLS) ensures that each user sees only the data rows they are authorized to access. The Query Engine's RlsFilterService intercepts every SQL query, extracts table references, evaluates security policies for each table against the user's roles and attributes, and injects WHERE clause predicates that restrict row visibility. This process is transparent to the user -- the query they write is automatically augmented with security filters before execution.


RLS Architecture

The RLS system spans two services:

+------------------+         Policy Eval        +-------------------+
|  Query Engine    | --------------------------> | Governance Service|
|  RlsFilterService|                             | (OPA + Polaris)   |
+--------+---------+                             +-------------------+
         |
    SQL Transformation
         |
+--------v---------+
|  Modified SQL    |
|  with WHERE      |
|  predicates      |
+------------------+
ComponentServiceResponsibility
RlsFilterServiceQuery EngineSQL parsing, table extraction, filter injection
RlsServiceClientQuery EngineHTTP client to governance service
OPA Policy EngineGovernance ServicePolicy evaluation, filter generation
Polaris AuthorizationGovernance ServiceTable-level access control

RLS Filter Context

Every RLS evaluation starts with a RlsFilterContext that captures the complete security context:

public static class RlsFilterContext {
    private UUID tenantId;               // Tenant owning the data
    private UUID executionId;            // Query execution identifier
    private UUID userId;                 // User executing the query
    private String userEmail;            // User email for email-based policies
    private List<String> userRoles;      // User's RBAC roles
    private Map<String, Object> userAttributes;  // Custom attributes (department, region, etc.)
    private String query;                // Original SQL query
    private String databaseName;         // Database context
    private String schemaName;           // Schema context
    private Map<String, Object> environmentAttributes;  // Time, IP, location
    private Boolean requestBypass;       // Emergency bypass flag
    private String bypassJustification;  // Required justification for bypass
    private String clientIp;             // Client IP address
    private String sessionId;            // Session identifier
}

The context is assembled by the QueryExecutionService from the JWT token claims, the request headers, and the query metadata.


Table Extraction

The RlsFilterService extracts table references from SQL queries using multiple regex patterns to cover different SQL constructs:

FROM Clause Extraction

private static final Pattern FROM_PATTERN = Pattern.compile(
    "\\bFROM\\s+([\\w.\"]+(?:\\s+(?:AS\\s+)?\\w+)?(?:\\s*,\\s*[\\w.\"]+(?:\\s+(?:AS\\s+)?\\w+)?)*)",
    Pattern.CASE_INSENSITIVE
);

Handles:

  • FROM orders -- simple table reference
  • FROM orders o -- table with alias
  • FROM orders AS o -- table with explicit alias
  • FROM schema.orders -- schema-qualified table
  • FROM orders o, customers c -- comma-separated tables

JOIN Clause Extraction

private static final Pattern JOIN_PATTERN = Pattern.compile(
    "\\b(INNER|LEFT|RIGHT|FULL|CROSS)?\\s*JOIN\\s+([\\w.\"]+)(?:\\s+(?:AS\\s+)?(\\w+))?",
    Pattern.CASE_INSENSITIVE
);

Handles all JOIN types: INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN, CROSS JOIN.

UPDATE and DELETE Extraction

private static final Pattern UPDATE_PATTERN = Pattern.compile(
    "\\bUPDATE\\s+([\\w.\"]+)(?:\\s+(?:AS\\s+)?(\\w+))?\\s+SET\\b",
    Pattern.CASE_INSENSITIVE
);
 
private static final Pattern DELETE_PATTERN = Pattern.compile(
    "\\bDELETE\\s+FROM\\s+([\\w.\"]+)(?:\\s+(?:AS\\s+)?(\\w+))?",
    Pattern.CASE_INSENSITIVE
);

RLS filters are applied to all SQL operation types, not just SELECT queries. UPDATE and DELETE operations receive the same WHERE clause restrictions, preventing users from modifying rows they cannot read.

Extracted Table Structure

Each extracted table produces a TableReference object:

public static class TableReference {
    private String name;      // Table name (e.g., "orders")
    private String alias;     // Alias if present (e.g., "o")
    private String schema;    // Schema if qualified (e.g., "public")
    private String database;  // Database if qualified
}

Policy Evaluation

For each extracted table, the RlsFilterService sends an evaluation request to the governance service:

RlsEvaluationRequest request = RlsEvaluationRequest.builder()
    .tenantId(context.getTenantId())
    .executionId(context.getExecutionId())
    .userId(context.getUserId())
    .userEmail(context.getUserEmail())
    .userRoles(context.getUserRoles())
    .userAttributes(context.getUserAttributes())
    .resourceFqn(resourceFqn)           // e.g., "analytics.public.orders"
    .resourceType("TABLE")
    .operation(determineOperation(sql))  // SELECT, INSERT, UPDATE, DELETE
    .environmentAttributes(context.getEnvironmentAttributes())
    .build();
 
RlsEvaluationResult result = rlsServiceClient.evaluateRls(request);

Evaluation Result

The governance service returns an RlsEvaluationResult:

public class RlsEvaluationResult {
    private boolean requiresFiltering;            // Whether RLS applies to this table
    private String whereClause;                   // Generated WHERE predicate
    private List<AppliedPolicyInfo> appliedPolicies;  // Which policies matched
    private boolean accessDenied;                 // Complete access denial
    private String denialReason;                  // Reason for denial
}

Policy Types

The governance service supports several policy types:

Policy TypeDescriptionExample Filter
Attribute-basedFilter by user attribute matchregion = 'US-EAST'
Role-basedFilter by user role membershipdepartment IN ('engineering', 'product')
HierarchicalFilter by organizational hierarchyorg_unit_id IN (SELECT id FROM org_units WHERE path LIKE '/corp/eng/%')
Time-basedFilter by temporal constraintscreated_at >= '2026-01-01'
Row classificationFilter by data classification labelclassification_level <= 'CONFIDENTIAL'
Custom expressionArbitrary SQL predicate(amount < 10000 OR approver_id IS NOT NULL)

WHERE Clause Injection

After collecting filters for all tables, the RlsFilterService injects them into the SQL query:

Injection into Existing WHERE Clause

String injectWhereClause(String query, String whereClause) {
    Matcher whereMatcher = WHERE_PATTERN.matcher(query);
 
    if (whereMatcher.find()) {
        // Query already has WHERE - combine with AND
        // Find existing WHERE clause boundaries
        String existingWhere = extractExistingWhere(query, whereMatcher);
 
        return query.substring(0, whereEnd) +
               "(" + existingWhere + ") AND (" + whereClause + ")" +
               query.substring(clauseEnd);
    } else {
        // No WHERE clause - insert before GROUP BY/ORDER BY/LIMIT
        int insertPosition = findInsertPosition(query);
 
        return query.substring(0, insertPosition).trim() +
               " WHERE " + whereClause +
               (insertPosition < query.length() ? " " + query.substring(insertPosition) : "");
    }
}

Examples of Filter Injection

Simple SELECT with no WHERE clause:

-- Original
SELECT * FROM orders
 
-- After RLS (user restricted to region 'US-EAST')
SELECT * FROM orders WHERE orders.region = 'US-EAST'

SELECT with existing WHERE clause:

-- Original
SELECT * FROM orders WHERE status = 'active' ORDER BY created_at
 
-- After RLS
SELECT * FROM orders WHERE (status = 'active') AND (orders.region = 'US-EAST') ORDER BY created_at

Multi-table JOIN with different policies per table:

-- Original
SELECT o.id, c.name, o.amount
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.amount > 100
 
-- After RLS (user restricted to region 'US-EAST' on orders,
-- and to department 'retail' on customers)
SELECT o.id, c.name, o.amount
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE (o.amount > 100) AND (o.region = 'US-EAST') AND (c.department = 'retail')

Column Qualification

When a filter references a column name without a table qualifier, the RlsFilterService automatically qualifies it with the table alias:

private String qualifyFilter(String filter, String tableRef) {
    // "region = 'US-EAST'" becomes "orders.region = 'US-EAST'"
    // or "o.region = 'US-EAST'" if the table has alias "o"
}

This prevents ambiguous column references in multi-table queries.


RLS Filter Result

The complete filter result is returned to the execution service:

public static class RlsFilterResult {
    private String originalQuery;              // The original SQL
    private String filteredQuery;              // The modified SQL with RLS predicates
    private boolean filtersApplied;            // Whether any filters were applied
    private Map<String, String> appliedFilters; // Table -> filter mapping
    private List<String> filteredTables;       // Tables that received filters
    private List<AppliedPolicyInfo> appliedPolicies;  // Policy metadata for audit
    private String reason;                     // Reason if no filters applied
    private String error;                      // Error message if processing failed
    private long processingTimeMs;             // Time spent on RLS processing
}

Metrics and Monitoring

The RlsFilterService emits Micrometer metrics:

MetricTypeDescription
rls.filter.appliedCounterNumber of queries with RLS filters applied
rls.filter.skippedCounterNumber of queries where RLS was skipped
rls.filter.timeTimerTime spent applying RLS filters
rls.filter.tables_per_queryHistogramNumber of tables filtered per query
rls.filter.policies_per_queryHistogramNumber of policies applied per query

Observability for Compliance

Every RLS evaluation is logged with:

{
  "event": "rls_filter_applied",
  "tenantId": "tenant-123",
  "userId": "user-456",
  "executionId": "exec-789",
  "tablesFiltered": ["orders", "customers"],
  "policiesApplied": ["region_restriction", "department_isolation"],
  "processingTimeMs": 12,
  "timestamp": "2026-02-12T10:30:00Z"
}

This log entry is written to the immutable audit trail for compliance and forensic analysis.


Emergency Bypass

In exceptional circumstances, authorized users can request an RLS bypass:

RlsFilterContext context = RlsFilterContext.builder()
    .tenantId(tenantId)
    .userId(userId)
    .requestBypass(true)
    .bypassJustification("Incident INC-2026-0042: investigating data anomaly")
    .build();

Bypass requests:

  • Require the RLS_BYPASS permission, which is typically restricted to tenant administrators
  • Require a written justification that is stored in the audit log
  • Are flagged in the query audit trail with a bypass indicator
  • Generate an alert to the security monitoring system
  • Have a configurable maximum duration (default: 1 hour)

RLS and Caching Interaction

RLS creates a challenge for result caching: the same SQL query may return different results for different users depending on their RLS policies. The cache key includes a hash of the user's effective roles to ensure proper isolation:

String cacheKey = SHA256(
    tenantId +
    normalizedSql +
    catalog + schema +
    userRolesHash     // Different roles -> different cache entries
);

This means:

  • Two users with the same roles will share cached results (correct behavior since they see the same data)
  • Two users with different roles will have separate cache entries (correct behavior since they see different data)
  • An admin who bypasses RLS will have a separate cache entry from a user with RLS applied

Limitations and Future Work

LimitationDescriptionPlanned Resolution
Regex-based parsingCurrent SQL parsing uses regex patterns, not a full SQL parserMigrate to JSqlParser AST-based extraction
Subquery supportRLS filters are applied to top-level tables; subqueries may not be coveredAdd recursive subquery traversal
CTE supportCommon Table Expressions are not yet filteredAdd CTE table reference extraction
View expansionViews are filtered at the view level, not at underlying table levelIntegrate with catalog to expand view definitions

Related Sections