# `Lotus.Visibility`
[🔗](https://github.com/typhoonworks/lotus/blob/v0.16.6/lib/lotus/visibility.ex#L1)

Schema and table visibility filtering for Lotus.

Implements a two-level visibility system where **schema visibility takes precedence**:
1. **Schema visibility** is checked first - if a schema is denied, all its tables are blocked
2. **Table visibility** is only checked if the schema is allowed

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

```elixir
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)
```elixir
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:

```elixir
# 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
```elixir
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
```elixir
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
```elixir
config :lotus,
  schema_visibility: %{
    mysql: [
      # Remember: schemas = databases in MySQL
      allow: ["app_production", "analytics_warehouse"],
      deny: ["staging_db", "backup_db"]
    ]
  }
```

## API

Direct visibility checking:
```elixir
# 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:
```elixir
{: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](guides/visibility.html).

# `allowed_relation?`

```elixir
@spec allowed_relation?(
  String.t(),
  {String.t() | nil, String.t()}
) :: 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?`

```elixir
@spec allowed_schema?(String.t(), String.t() | nil) :: 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`

```elixir
@spec column_policy_for(String.t(), [{String.t() | nil, String.t()}], String.t()) ::
  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_repo_name/1` and support patterns
on schema, table, and column names. Returns a normalized policy map or nil.

# `filter_relations`

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

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

# `filter_schemas`

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

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

# `validate_schemas`

```elixir
@spec validate_schemas([String.t()], String.t()) ::
  :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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
