Macros are reusable logic blocks that can be used in Permission Rules. They allow you to encapsulate complex logic, SQL queries, and common patterns into simple function calls.
Important: Macros are global - they are shared across all accounts. Only superadmins can create, update, and delete macros.
Built-in Macros
SnackBase provides several optimized macros for common scenarios.
@has_group(group_name: str)
Checks if the current user is a member of the specified group.
- Returns:
true if the user is a member of the group, false otherwise.
@has_role(role_name: str)
Checks if the current user has the specified role.
- Returns:
true if the user has the role, false otherwise.
@owns_record()
Checks if the current user ID matches the owner_id field on the record.
- Returns:
true if user.id == record.owner_id, false otherwise.
- Note: The field checked is hardcoded to
owner_id.
@is_creator()
Alias for @owns_record(). Checks if the current user ID matches the owner_id field on the record.
@has_permission(operation: str, collection: str)
Checks if the user has permission to perform another operation.
@has_permission("read", "users")
- Returns:
true if the user is allowed to perform the specified operation on the collection.
@in_time_range(start_hour: int, end_hour: int)
Checks if the current server time is within the specified hour range (24-hour format).
@in_time_range(9, 17) # 9 AM to 5 PM
- Returns:
true if current hour is >= start_hour and < end_hour.
SQL Macros
SQL Macros allow you to define custom logic using raw SQL queries.
Creating a SQL Macro
To create a macro, use the Macros API (accessible via Admin UI or API).
Example: Allow access if the user is a “member” of the “project” associated with the “task” record.
{
"name": "is_project_member",
"description": "Check if user is a project member",
"parameters": ["project_id"],
"sql_query": "SELECT 1 FROM project_members WHERE project_id = :project_id AND user_id = :user_id AND account_id = :account_id AND role = 'member' LIMIT 1"
}
Parameters like :user_id and :account_id must be explicitly included in your query’s WHERE clause. They are available in the execution context but are not automatically injected.
Using SQL Macros
Once created, use it in your permission rules like a function:
@is_project_member(record.project_id)
Security Features & Constraints
SQL Macros include several security features:
Query Validation
- SELECT Only: Macros must start with
SELECT
- Forbidden Keywords:
INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, GRANT, REVOKE, CREATE, ALTER TABLE, DROP TABLE
- Parameter Binding: All parameters use prepared statement binding to prevent SQL injection
Execution Constraints
- Timeout: All macros have a 5-second execution timeout
- Error Handling: Any error returns
false rather than exposing database details
- Read-Only Execution: Macros execute within a transaction context designed for read operations
Deletion Safety
When attempting to delete a macro, the system checks if the macro is currently in use by any permissions. If the macro is referenced, the deletion will be rejected.
API Endpoints
List All Macros
- Access: All authenticated users
- Returns: List of all macros with their parameters and descriptions
Create Macro
{
"name": "is_active_subscriber",
"description": "Checks if user has an active subscription",
"parameters": [],
"sql_query": "SELECT 1 FROM subscriptions WHERE user_id = :user_id AND status = 'active' AND expires_at > NOW() LIMIT 1"
}
Validation:
- Name must be a valid Python identifier
- Name must be unique across all macros
- SQL query must start with SELECT
- SQL query cannot contain forbidden keywords
Update Macro
- Access: Superadmin only
- Note: Updating a macro affects all permissions that use it immediately
Test Macro
POST /api/v1/macros/{id}/test
{
"parameters": {
"project_id": "project_123"
}
}
- Behavior: Executes the macro with test parameters using transaction rollback
- Returns: Result of the macro execution (true/false) and any error information
Delete Macro
DELETE /api/v1/macros/{id}
- Access: Superadmin only
- Safety Check: Fails if the macro is used in any permissions
Macro Development Guide
Best Practices
- Use SELECT 1: Always use
SELECT 1 or SELECT true. The engine checks if any row is returned.
- Add LIMIT 1: Always include
LIMIT 1 for performance.
- Index Your Columns: Ensure columns used in
WHERE clauses are properly indexed.
- Include Account Isolation: Always include
account_id = :account_id in WHERE clauses.
- Keep It Simple: Use SQL macros for data lookup only.
- Test Before Deploying: Use the
/test endpoint to validate macro logic.
Naming Conventions
- Use descriptive names that indicate what the macro checks
- Prefix with
is_, has_, can_, or similar verbs
- Examples:
is_project_member - Checks membership in a project
has_active_subscription - Checks subscription status
can_access_resource - Checks resource access rights
Common Patterns
Check relationship existence:
SELECT 1 FROM table_relationships
WHERE parent_id = :parent_id
AND user_id = :user_id
AND account_id = :account_id
LIMIT 1
Check status with date validation:
SELECT 1 FROM subscriptions
WHERE user_id = :user_id
AND account_id = :account_id
AND status = 'active'
AND (expires_at IS NULL OR expires_at > NOW())
LIMIT 1
Check multiple conditions:
SELECT 1 FROM access_grants
WHERE resource_id = :resource_id
AND user_id = :user_id
AND account_id = :account_id
AND role IN ['editor', 'owner']
AND is_active = true
LIMIT 1
Example API Payloads
Create Macro:
{
"name": "is_active_subscriber",
"description": "Checks if user has an active subscription that hasn't expired",
"parameters": [],
"sql_query": "SELECT 1 FROM subscriptions WHERE user_id = :user_id AND account_id = :account_id AND status = 'active' AND (expires_at IS NULL OR expires_at > NOW()) LIMIT 1"
}
Usage in Rule:
Create Macro with Parameters:
{
"name": "has_project_access",
"description": "Checks if user has access to a specific project",
"parameters": ["project_id"],
"sql_query": "SELECT 1 FROM project_members WHERE project_id = :project_id AND user_id = :user_id AND account_id = :account_id LIMIT 1"
}
Usage in Rule:
@has_project_access(record.project_id)