Schema, table and column visibility filtering for Lotus.
Implements a three-level visibility system where the outer level takes precedence:
- Schema visibility is checked first — if a schema is denied, every table in it is blocked
- Table visibility is only checked if the schema is allowed
- Column visibility applies to the columns of an allowed table, and can hide a column from discovery or mask its values in results
This ensures security by default while providing fine-grained control.
Understanding Schemas Across Database Systems
Important: "Schema" means different things in different databases:
PostgreSQL
- True namespaced schemas within a single database
- Examples:
public,reporting,tenant_123 - System schemas:
pg_catalog,information_schema,pg_toast
MySQL
- Schemas = Databases (synonymous terms)
- Examples:
lotus_production,analytics_db,warehouse - System schemas:
mysql,information_schema,performance_schema,sys
SQLite
- No schema support (schema-less database)
- Schema visibility rules don't apply
Quick Start
config :lotus,
# Schema-level rules (higher precedence)
schema_visibility: %{
postgres: [
allow: ["public", ~r/^tenant_/], # Only public + tenant schemas
deny: ["legacy"] # Block legacy schema
],
mysql: [
allow: ["app_db", "analytics_db"], # Only these databases
deny: ["staging_db"] # Block staging database
]
},
# Table-level rules (lower precedence)
table_visibility: %{
default: [
deny: ["user_passwords", "api_keys", ~r/^audit_/]
],
postgres: [
allow: [
{"public", ~r/^dim_/}, # Dimension tables only
{"analytics", ~r/.*/} # All analytics tables
]
]
}Rule Evaluation
1. Schema Gating (First Check)
if not allowed_schema?(repo_name, schema) do
false # Schema denied → all tables blocked
else
# Schema allowed → check table rules
end2. Schema-Scoped Allow Posture
Allow rules are scoped to specific schemas, not global:
# Rules: allow: [{"restricted", "allowed_table"}]
{"restricted", "any_table"} → denied (has allow posture, must match)
{"public", "any_table"} → allowed (no allow posture for public)3. Deny Always Wins
Any deny rule (builtin or user-defined) blocks access immediately.
Rule Formats
Schema Rules
"exact_name"- Matches exact schema name~r/pattern/- Regex pattern for dynamic matching:all- Special allow value (permits all schemas)
Table Rules
{"schema", "table"}- Exact schema.table match{"schema", ~r/pattern/}- Tables matching regex in specific schema{~r/schema_pattern/, "table"}- Table in schemas matching pattern"table"- Table name in any schema (global rule)
Built-in Security
System schemas are automatically denied:
- PostgreSQL:
pg_catalog,information_schema,pg_toast,pg_temp_* - MySQL:
mysql,information_schema,performance_schema,sys - All databases:
schema_migrations,lotus_queries
Examples
Multi-tenant Application
config :lotus,
schema_visibility: %{
postgres: [
allow: ["public", ~r/^tenant_\d+$/], # tenant_123, etc.
deny: ["admin"]
]
},
table_visibility: %{
postgres: [
allow: [
{"public", ~r/^shared_/}, # Shared lookup tables
{~r/^tenant_/, "users"}, # Users in each tenant
{~r/^tenant_/, "orders"} # Orders in each tenant
],
deny: [
{~r/^tenant_/, "audit_logs"} # Hide audit logs
]
]
}Data Warehouse
config :lotus,
schema_visibility: %{
postgres: [
allow: ["public", "warehouse", "analytics"]
]
},
table_visibility: %{
postgres: [
allow: [
{"public", ~r/^dim_/}, # Dimension tables
{"public", ~r/^fact_/}, # Fact tables
{"warehouse", ~r/.*/}, # All warehouse
{"analytics", ~r/^report_/} # Only reports
],
deny: [
{"public", ~r/^raw_/} # Hide raw data
]
]
}MySQL Multi-Database
config :lotus,
schema_visibility: %{
mysql: [
# Remember: schemas = databases in MySQL
allow: ["app_production", "analytics_warehouse"],
deny: ["staging_db", "backup_db"]
]
}API
Direct visibility checking:
# Check schema visibility
Lotus.Visibility.allowed_schema?("postgres", "public") # true/false
# Check table visibility
Lotus.Visibility.allowed_relation?("postgres", {"public", "users"}) # true/false
# Filter lists
Lotus.Visibility.filter_schemas(["public", "pg_catalog"], "postgres") # ["public"]
# Validate requested schemas
Lotus.Visibility.validate_schemas(["public", "restricted"], "postgres")
# :ok | {:error, :schema_not_visible, denied: [...]}Schema-aware Lotus functions automatically apply visibility:
{:ok, schemas} = Lotus.list_schemas("postgres") # Filtered list
{:ok, tables} = Lotus.list_tables("postgres") # Filtered list
{:error, msg} = Lotus.list_tables("postgres", schemas: ["denied"]) # ErrorFor more detailed examples and configuration patterns, see the Visibility Guide.
Summary
Types
What every check accepts in place of a source name: a compiled
Lotus.Visibility.Matcher, or a raw rule set with optional :schema,
:table and :column keys in the formats the resolver callbacks return.
A raw rule set is compiled as given, with no builtin denies.
Functions
Checks if a relation (schema, table) is allowed for the given source.
Checks if a schema is visible for the given source.
Resolves the column policy for a given result column name in the context of accessed relations and source.
Compiles a rule set into a Lotus.Visibility.Matcher.
Filters a list of relations to only those that are visible.
Filters a list of schemas to only those that are visible.
Returns the compiled matcher for a source, from the configured resolver.
Validates that all requested schemas are visible.
Types
@type source() :: String.t() | Lotus.Visibility.Matcher.t() | rules()
What every check accepts in place of a source name: a compiled
Lotus.Visibility.Matcher, or a raw rule set with optional :schema,
:table and :column keys in the formats the resolver callbacks return.
A raw rule set is compiled as given, with no builtin denies.
Functions
Checks if a relation (schema, table) is allowed for the given source.
Schema visibility is checked first, then table visibility. source is a
source name, a compiled matcher or a raw rule set (see source/0).
Checks if a schema is visible for the given source.
source is a source name, a compiled matcher or a raw rule set (see
source/0).
Returns:
trueif the schema is allowedfalseif the schema is denied
@spec column_policy_for( source(), [{String.t() | nil, String.t()}] | nil, String.t(), term() ) :: nil | %{action: atom(), mask: any(), show_in_schema?: boolean()}
Resolves the column policy for a given result column name in the context of accessed relations and source.
source is a source name, a compiled matcher or a raw rule set (see
source/0). Rules support patterns on schema, table, and column names.
Returns a normalized policy map or nil.
@spec compile(Lotus.Visibility.Matcher.t() | rules(), keyword()) :: Lotus.Visibility.Matcher.t()
Compiles a rule set into a Lotus.Visibility.Matcher.
Exact names become sets and regex rules a short ordered list, so a check against the matcher does not walk the raw rules. Pure: the same rules give the same matcher. A matcher passed in is returned as it is, apart from the merge below.
Options
:adapter— an%Lotus.Source.Adapter{}whose built-in schema and table denies are merged into the matcher, ornilfor the conservative fallback denies used when no source can be resolved.:builtin_schema_denies,:builtin_table_denies— the deny lists to merge when the caller has them instead of the adapter.
A resolver that serves rules changed at runtime compiles once when a rule
set is written and returns the matcher from
Lotus.Visibility.Resolver.matcher_for/2.
@spec filter_relations([{String.t() | nil, String.t()}], source(), term()) :: [ {String.t() | nil, String.t()} ]
Filters a list of relations to only those that are visible.
Filters a list of schemas to only those that are visible.
@spec matcher_for(String.t(), term()) :: Lotus.Visibility.Matcher.t()
Returns the compiled matcher for a source, from the configured resolver.
A resolver that exports Lotus.Visibility.Resolver.matcher_for/2
answers directly. Otherwise its three rule callbacks are compiled here,
with the built-in denies of the source's adapter merged in.
Call this once per result or per discovery call and pass the matcher to the checks below, instead of passing the source name to every check.
@spec validate_schemas([String.t()], source(), term()) :: :ok | {:error, :schema_not_visible, [{:denied, [String.t()]}]}
Validates that all requested schemas are visible.
Returns:
:okif all schemas are visible{:error, :schema_not_visible, denied: [schemas]}if any are denied