Lotus.Visibility (Lotus v1.0.0)

Copy Markdown View Source

Schema, table and column visibility filtering for Lotus.

Implements a three-level visibility system where the outer level takes precedence:

  1. Schema visibility is checked first — if a schema is denied, every table in it is blocked
  2. Table visibility is only checked if the schema is allowed
  3. 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
end

2. 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"])  # Error

For more detailed examples and configuration patterns, see the Visibility Guide.

Summary

Functions

Checks if a relation (schema, table) is allowed for the given data repo.

Checks if a schema is visible for the given data repo.

Resolves the column policy for a given result column name in the context of accessed relations and repo.

Filters a list of relations to only those that are visible.

Filters a list of schemas to only those that are visible.

Validates that all requested schemas are visible.

Functions

allowed_relation?(repo_name, arg, scope \\ nil)

@spec allowed_relation?(String.t(), {String.t() | nil, String.t()}, term()) ::
  boolean()

Checks if a relation (schema, table) is allowed for the given data repo.

This now checks schema visibility first, then table visibility.

allowed_schema?(repo_name, schema, scope \\ nil)

@spec allowed_schema?(String.t(), String.t() | nil, term()) :: boolean()

Checks if a schema is visible for the given data repo.

Returns:

  • true if the schema is allowed
  • false if the schema is denied

column_policy_for(repo_name, relations, result_column_name, scope \\ nil)

@spec column_policy_for(
  String.t(),
  [{String.t() | nil, String.t()}],
  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 repo.

Rules are taken from Lotus.Config.column_rules_for_source_name/1 and support patterns on schema, table, and column names. Returns a normalized policy map or nil.

filter_relations(relations, repo_name, scope \\ nil)

@spec filter_relations([{String.t() | nil, String.t()}], String.t(), term()) :: [
  {String.t() | nil, String.t()}
]

Filters a list of relations to only those that are visible.

filter_schemas(schemas, repo_name, scope \\ nil)

@spec filter_schemas([String.t()], String.t(), term()) :: [String.t()]

Filters a list of schemas to only those that are visible.

validate_schemas(schemas, repo_name, scope \\ nil)

@spec validate_schemas([String.t()], String.t(), term()) ::
  :ok | {:error, :schema_not_visible, [{:denied, [String.t()]}]}

Validates that all requested schemas are visible.

Returns:

  • :ok if all schemas are visible
  • {:error, :schema_not_visible, denied: [schemas]} if any are denied