# `AshGrant.Permission`
[🔗](https://github.com/jhlee111/ash_grant/blob/v0.14.1/lib/ash_grant/permission.ex#L1)

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 all
- `instance_id` - The specific resource ID or `"*"` for all instances
- `action` - The action (e.g., "read", "update") or wildcard patterns
- `scope` - The access scope (e.g., "always", "own") for filtering
- `field_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 action
- `read*` - 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 blogs

### Instance 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 post

### Instance 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 owner

When 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"

# `t`

```elixir
@type t() :: %AshGrant.Permission{
  action: String.t(),
  deny: boolean(),
  description: String.t() | nil,
  field_group: String.t() | nil,
  instance_id: String.t(),
  metadata: map() | nil,
  resource: String.t(),
  scope: String.t() | nil,
  source: String.t() | nil
}
```

# `deny?`

```elixir
@spec deny?(t()) :: boolean()
```

Checks if this is a deny rule.

# `from_input`

```elixir
@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
    }

# `instance_permission?`

```elixir
@spec instance_permission?(t()) :: boolean()
```

Checks if this is an instance-level permission.

An instance permission has a specific instance_id (not "*").

# `matches?`

```elixir
@spec matches?(t(), String.t(), String.t()) :: boolean()
```

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

# `matches?`

```elixir
@spec matches?(t(), String.t(), String.t(), atom() | nil) :: boolean()
```

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

# `matches_action?`

```elixir
@spec matches_action?(String.t(), String.t()) :: boolean()
```

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

# `matches_action?`

```elixir
@spec matches_action?(String.t(), String.t(), atom() | nil) :: boolean()
```

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

# `matches_instance?`

```elixir
@spec matches_instance?(t(), String.t(), String.t()) :: boolean()
```

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

# `matches_resource?`

```elixir
@spec matches_resource?(String.t(), String.t()) :: boolean()
```

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

# `parse`

```elixir
@spec parse(String.t()) :: {:ok, t()} | {:error, String.t()}
```

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}}

# `parse!`

```elixir
@spec parse!(String.t()) :: t()
```

Parses a permission string, raising on error.

# `resource`

```elixir
@spec resource(t()) :: String.t()
```

Returns the resource type from this permission.

# `to_string`

```elixir
@spec to_string(t()) :: String.t()
```

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"

---

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