Skip to main content
Coming Soon: Fine-grained access controls are currently in development. This page provides a preview of planned functionality.
Fine-grained access control allows you to implement row-level and field-level security directly within your Malloy semantic models. These controls are defined using annotations in your .malloy files and are enforced automatically when queries execute.

Three Layers of Security

Credible implements security at three checkpoints:
  1. Application Layer (First Checkpoint): Can you open this resource?
  2. Semantic Layer (Second Checkpoint): Can you query this model through governed connections?
    • Database connections managed at project level
    • Queries execute through semantic models, not directly against databases
    • Ensures consistent business logic and definitions
  3. Data Layer (Final Checkpoint - Coming Soon): Which specific rows and columns can you see?
    • Row-level security: Filter data based on user attributes
    • Field-level security: Hide sensitive columns from unauthorized users
    • Defined in Malloy models using #authorize and #bind annotations
The Golden Rule: Data-layer permissions always take final precedence. Even with application-level access, you can only see data you’re explicitly authorized to view.

Row-Level Security

// Make source publicly accessible
# authorize public
source: public_airports is airports extend {
  dimension:
    has_control_tower is cntl_twr = 'Y'
    major_airport is passenger_count > 100000

  measure:
    airport_count is count()
}

// Restrict to specific group
# authorize [ "group:admin@faa.gov" ]
source: admin_airports is airports extend {
  dimension:
    classified_status is status_code

  measure:
    total_budget is sum(annual_budget)
}

// Filter rows by user's group membership
# authorize [ "group:atc@faa.gov" ]
source: atc_airports is airports extend {
  // Air traffic controllers only see airports with control towers
  where: cntl_twr = 'Y'
}

// Dynamic filtering: bind user's group at runtime
# bind [ "ext_group_id:SESSION_GROUP()" ]
source: group_airports(ext_group_id: string) is airports extend {
  // Users only see airports belonging to their group
  where: group_id = ext_group_id
}

// Alternative: join to user attributes table (similar to BigQuery pattern)
# authorize [ "group:admin@faa.gov" ]
source: user_attributes(user_id: string) is
  duckdb.table('../data/user_attributes.parquet') extend {
  where: user_attributes.user_id = user_id
}

# bind [ "ext_user_id:SESSION_USER()" ]
source: my_airports(ext_user_id: string) is airports extend {
  join_one: user_attributes(ext_user_id)
    inner on group_id = user_attributes.group_id
}

Field-Level Security

// Full source with all fields - admin only
# authorize [ "group:admin@faa.gov" ]
source: airports_full is duckdb.table('../data/airports.parquet') extend {
  dimension:
    code is airport_code
    name is airport_name
    city is city_name
    security_clearance is clearance_level
    classified_operations is ops_classified

  measure:
    airport_count is count()
    total_security_incidents is sum(security_incidents)
}

// Public view excludes sensitive fields
# authorize public
source: airports_public is airports_full include {
  code, name, city, airport_count
}

// Using access modifiers
source: customers is duckdb.table('../data/customers.parquet') extend {
  public dimension:
    customer_id is id
    company_name is company

  internal dimension:
    credit_score is credit_rating

  private dimension:
    ssn is social_security_number

  public measure:
    customer_count is count()
}

Annotation Scopes

// File-level: applies to all sources in file
## authorize public

source: source1 is table1 extend { ... }
source: source2 is table2 extend { ... }

// Source-level: overrides file-level default
# authorize [ "group:admin@company.com" ]
source: sensitive_data is data extend { ... }

Annotation Reference

#authorize

  • public - Anyone with access to the model can query
  • [ "group:admin@company.com" ] - Specific group membership
  • [ "group:admin@faa.gov", "group:manager@faa.gov" ] - Multiple groups

#bind

Format: # bind [ "parameter_name:FUNCTION()" ] Available functions:
  • SESSION_USER() - Authenticated user’s ID/email
  • SESSION_GROUP() - User’s primary group ID
  • Custom functions provided by your authorization service
Examples:
# bind [ "user_id:SESSION_USER()" ]
# bind [ "group_id:SESSION_GROUP()" ]
# bind [ "region:SESSION_REGION()", "dept:SESSION_DEPARTMENT()" ]

Implementation Architecture

Client Request

Gateway / Authorization Service
    ├── 1. Extract user identity from auth token
    ├── 2. Fetch #authorize annotations from Publisher
    ├── 3. Evaluate authorization rules
    ├── 4. Fetch #bind annotations from Publisher
    ├── 5. Retrieve user attribute values
    ├── 6. Inject parameters into query

Publisher (executes query with parameters)

Results (filtered by row/field ACLs)
Gateway responsibilities:
  • Authenticate requests
  • Fetch source annotations from Publisher API
  • Evaluate #authorize rules (allow/deny)
  • Retrieve runtime values for #bind parameters
  • Inject bound parameters into queries

Examples

Regional Sales Data

# bind [ "user_region:SESSION_REGION()" ]
source: regional_sales(user_region: string) is sales extend {
  where: region = user_region

  dimension:
    region is sales_region
    product is product_name

  measure:
    total_revenue is sum(revenue)
    order_count is count()
}

Multi-Tenant SaaS

# bind [ "tenant_id:SESSION_TENANT()" ]
source: tenant_data(tenant_id: string) is app_data extend {
  where: customer_id = tenant_id

  dimension:
    user_name is name
    user_email is email

  measure:
    active_users is count(distinct user_id)
}

Next Steps

I