Permission struct with parsing and matching capabilities.
This module provides the core permission representation for AshGrant. Permissions follow an Apache Shiro-inspired string format that handles both role-based (RBAC) and instance-level access, with an optional field group for column-level restrictions.
Permission Struct
A permission consists of:
resource- The resource type (e.g., "blog", "post") or"*"for allinstance_id- The specific resource ID or"*"for all instancesaction- The action (e.g., "read", "update") or wildcard patternsscope- The access scope (e.g., "always", "own") for filteringfield_group- Optional field group for column-level access (e.g., "sensitive")deny- Whether this is a deny rule (takes precedence over allow)
Permission Format
[!]resource:instance_id:action:scope[:field_group]| Component | Description | Valid Values |
|---|---|---|
! | Deny prefix (optional) | ! or omitted |
| resource | Resource type | identifier, * |
| instance_id | Resource instance or * | prefixed_id, UUID, * |
| action | Action name | identifier, *, prefix* |
| scope | Access scope | all, own, custom, or empty |
| field_group | Column-level group (optional) | public, sensitive, custom |
Wildcard Patterns
Resource wildcards:
*- Matches any resource type
Instance wildcards:
*- Matches any instance (RBAC-style permission)post_abc123xyz789ab- Matches specific instance only
Action wildcards:
*- Matches any actionread*- Matches any action whose type is:read(requires action_type)
Examples
RBAC Permissions (instance_id = "*")
"blog:*:read:always" # Read all blogs
"blog:*:read:published" # Read only published blogs
"blog:*:update:own" # Update own blogs only
"blog:*:*:always" # All actions on all blogs
"*:*:read:always" # Read all resources
"*:*:*:always" # Full access to everything
"blog:*:read*:always" # All read-type actions
"!blog:*:delete:always" # DENY delete on all blogsInstance Permissions (specific instance_id)
For sharing specific resource instances (like Google Docs sharing):
"blog:post_abc123xyz789ab:read:" # Read specific post (no conditions)
"blog:post_abc123xyz789ab:*:" # Full access to specific post
"!blog:post_abc123xyz789ab:delete:" # DENY delete on specific postInstance Permissions with Scopes (ABAC)
Instance permissions can also include scopes for attribute-based conditions:
"doc:doc_123:update:draft" # Update only when document is in draft
"doc:doc_123:read:business_hours" # Read only during business hours
"invoice:inv_456:approve:small_amount" # Approve only if amount is small
"project:proj_789:admin:owner" # Admin access only when ownerWhen a scope is provided on an instance permission, it acts as an authorization condition that must be satisfied. Empty scopes (trailing colon) mean "no conditions" and are backward compatible with earlier versions.
Backward Compatibility
The parser also accepts shorter formats for convenience:
- Two-part:
resource:action→resource:*:action: - Three-part:
resource:action:scope→resource:*:action:scope
Usage
# Parse from string (new format)
{:ok, perm} = AshGrant.Permission.parse("blog:*:read:always")
# Legacy format also works
{:ok, perm} = AshGrant.Permission.parse("blog:read:always")
# Parse with error on failure
perm = AshGrant.Permission.parse!("blog:*:read:always")
# Check if permission matches for RBAC
AshGrant.Permission.matches?(perm, "blog", "read")
# => true
# Check instance permissions
inst_perm = AshGrant.Permission.parse!("blog:post_abc123:read:")
AshGrant.Permission.matches_instance?(inst_perm, "post_abc123", "read")
# => true
# Convert back to string
AshGrant.Permission.to_string(perm)
# => "blog:*:read:always"
Summary
Functions
Checks if this is a deny rule.
Creates a Permission struct from a PermissionInput, preserving metadata.
Checks if this is an instance-level permission.
Checks if a permission matches a resource and action.
Checks if a permission matches a resource, action, and optional Ash action type.
Checks if an action pattern matches an action name.
Checks if an action pattern matches an action name, with optional Ash action type.
Checks if a permission matches a specific resource instance.
Checks if a resource pattern matches a resource name.
Parses a permission string into a Permission struct.
Parses a permission string, raising on error.
Returns the resource type from this permission.
Converts a Permission struct back to string format.
Types
Functions
Checks if this is a deny rule.
@spec from_input(AshGrant.PermissionInput.t()) :: t()
Creates a Permission struct from a PermissionInput, preserving metadata.
This function parses the permission string from the input and copies over the metadata fields (description, source, metadata).
Examples
iex> input = %AshGrant.PermissionInput{
...> string: "blog:*:read:always",
...> description: "Read all blogs",
...> source: "editor_role"
...> }
iex> AshGrant.Permission.from_input(input)
%AshGrant.Permission{
resource: "blog",
instance_id: "*",
action: "read",
scope: "always",
deny: false,
description: "Read all blogs",
source: "editor_role",
metadata: nil
}
Checks if this is an instance-level permission.
An instance permission has a specific instance_id (not "*").
Checks if a permission matches a resource and action.
This only matches RBAC-style permissions (where instance_id is "*").
For instance-level matching, use matches_instance?/3.
Does not consider scope - that's handled by the ScopeResolver.
Examples
iex> perm = AshGrant.Permission.parse!("blog:*:read:always")
iex> AshGrant.Permission.matches?(perm, "blog", "read")
true
iex> perm = AshGrant.Permission.parse!("blog:*:read*:always")
iex> AshGrant.Permission.matches?(perm, "blog", "read_published")
false
iex> perm = AshGrant.Permission.parse!("blog:*:*:always")
iex> AshGrant.Permission.matches?(perm, "blog", "delete")
true
Checks if a permission matches a resource, action, and optional Ash action type.
When action_type is provided, prefix patterns like "read*" will also match
actions whose Ash type equals the prefix (e.g., a :read-type action named
list_published matches "read*").
Examples
iex> perm = AshGrant.Permission.parse!("blog:*:read*:always")
iex> AshGrant.Permission.matches?(perm, "blog", "list_published", :read)
true
iex> perm = AshGrant.Permission.parse!("blog:*:read*:always")
iex> AshGrant.Permission.matches?(perm, "blog", "list_published", :update)
false
Checks if an action pattern matches an action name.
Supports wildcard matching with "*" and prefix matching with "prefix*".
Examples
iex> AshGrant.Permission.matches_action?("*", "read")
true
iex> AshGrant.Permission.matches_action?("read", "read")
true
iex> AshGrant.Permission.matches_action?("read*", "read_all")
false
iex> AshGrant.Permission.matches_action?("read", "write")
false
Checks if an action pattern matches an action name, with optional Ash action type.
When action_type is provided and the pattern is a type wildcard like "read*",
the match succeeds if the action type matches the prefix. This allows "read*" to
match :read-type actions like list_published or by_slug. Without action_type,
type wildcards never match — use exact action names instead.
Examples
iex> AshGrant.Permission.matches_action?("*", "anything", :read)
true
iex> AshGrant.Permission.matches_action?("read*", "list_published", :read)
true
iex> AshGrant.Permission.matches_action?("read*", "list_published", :update)
false
iex> AshGrant.Permission.matches_action?("read*", "read_all", nil)
false
iex> AshGrant.Permission.matches_action?("update*", "publish", :update)
true
iex> AshGrant.Permission.matches_action?("read", "read", :read)
true
Checks if a permission matches a specific resource instance.
Examples
iex> perm = AshGrant.Permission.parse!("blog:post_abc123xyz789ab:read:")
iex> AshGrant.Permission.matches_instance?(perm, "post_abc123xyz789ab", "read")
true
iex> perm = AshGrant.Permission.parse!("blog:post_abc123xyz789ab:*:")
iex> AshGrant.Permission.matches_instance?(perm, "post_abc123xyz789ab", "write")
true
Checks if a resource pattern matches a resource name.
Supports wildcard matching with "*".
Examples
iex> AshGrant.Permission.matches_resource?("*", "blog")
true
iex> AshGrant.Permission.matches_resource?("blog", "blog")
true
iex> AshGrant.Permission.matches_resource?("blog", "post")
false
Parses a permission string into a Permission struct.
Supports 4-part, 5-part (with field_group), and legacy formats.
Formats
"resource:instance_id:action:scope" # 4-part
"resource:instance_id:action:scope:field_group" # 5-part (with field group)
"resource:action:scope" # Legacy 3-part → resource:*:action:scope
"resource:action" # Legacy 2-part → resource:*:action:Examples
iex> AshGrant.Permission.parse("blog:*:read:always")
{:ok, %AshGrant.Permission{resource: "blog", instance_id: "*", action: "read", scope: "always", deny: false}}
iex> AshGrant.Permission.parse("employee:*:read:always:sensitive")
{:ok, %AshGrant.Permission{resource: "employee", instance_id: "*", action: "read", scope: "always", field_group: "sensitive", deny: false}}
iex> AshGrant.Permission.parse("!blog:*:delete:always")
{:ok, %AshGrant.Permission{resource: "blog", instance_id: "*", action: "delete", scope: "always", deny: true}}
iex> AshGrant.Permission.parse("blog:post_abc123xyz789ab:read:")
{:ok, %AshGrant.Permission{resource: "blog", instance_id: "post_abc123xyz789ab", action: "read", scope: nil, deny: false}}
Parses a permission string, raising on error.
Returns the resource type from this permission.
Converts a Permission struct back to string format.
Produces a 4-part string, or 5-part when field_group is set.
Examples
iex> perm = %AshGrant.Permission{resource: "blog", instance_id: "*", action: "read", scope: "always"}
iex> AshGrant.Permission.to_string(perm)
"blog:*:read:always"
iex> perm = %AshGrant.Permission{resource: "employee", instance_id: "*", action: "read", scope: "always", field_group: "sensitive"}
iex> AshGrant.Permission.to_string(perm)
"employee:*:read:always:sensitive"
iex> perm = %AshGrant.Permission{resource: "blog", instance_id: "*", action: "delete", scope: "always", deny: true}
iex> AshGrant.Permission.to_string(perm)
"!blog:*:delete:always"