Current section

Files

Jump to
phoenix_gen_api lib phoenix_gen_api permission.ex
Raw

lib/phoenix_gen_api/permission.ex

defmodule PhoenixGenApi.Permission do
@moduledoc """
Provides permission checking functionality for API requests.
This module implements a flexible permission system that can verify whether a user
has the right to execute a specific request. Permissions can be disabled, or configured
to check that certain request arguments match the requesting user's identity.
## Permission Modes
### Disabled Permissions (`false`)
When `check_permission: false` is set in the FunConfig, all permission checks pass.
This is useful for public endpoints that don't require authentication or authorization.
### Any Authenticated (`:any_authenticated`)
When `check_permission: :any_authenticated` is configured, the system verifies that
the request has a valid `user_id`. Any authenticated user is allowed access.
### Argument-Based Permissions (`{:arg, arg_name}`)
When `check_permission: {:arg, arg_name}` is configured, the system verifies that
the value of the specified argument matches the request's `user_id`. This ensures
users can only access their own data.
For example, with `{:arg, "user_id"}`, a request from user "123" will only be
allowed if `request.args["user_id"] == "123"`.
### Role-Based Permissions (`{:role, allowed_roles}`)
When `check_permission: {:role, allowed_roles}` is configured, the system verifies
that the user has one of the allowed roles. Roles are checked against the
`request.user_roles` field (must be a list of strings or atoms).
For example, with `{:role, ["admin", "moderator"]}`, only users with those roles
are allowed access.
## Examples
# Public endpoint - no permission check
config = %FunConfig{
request_type: "get_public_data",
check_permission: false
}
request = %Request{user_id: "any_user"}
Permission.check_permission(request, config)
# => true
# Any authenticated user
config = %FunConfig{
request_type: "get_profile",
check_permission: :any_authenticated
}
request = %Request{user_id: "user_123"}
Permission.check_permission(request, config)
# => true
request = %Request{user_id: nil}
Permission.check_permission(request, config)
# => false
# User-specific endpoint - must match user_id
config = %FunConfig{
request_type: "get_user_profile",
check_permission: {:arg, "user_id"}
}
# This passes - user accessing their own data
request = %Request{
user_id: "user_123",
args: %{"user_id" => "user_123"}
}
Permission.check_permission(request, config)
# => true
# This fails - user trying to access another user's data
request = %Request{
user_id: "user_123",
args: %{"user_id" => "user_999"}
}
Permission.check_permission(request, config)
# => false
# Role-based access
config = %FunConfig{
request_type: "delete_user",
check_permission: {:role, ["admin"]}
}
request = %Request{
user_id: "user_123",
user_roles: ["admin"]
}
Permission.check_permission(request, config)
# => true
## Security Considerations
- Always use `check_permission!/2` in the execution path to raise on failures
- The `check_permission/2` function is non-raising and returns a boolean
- Missing arguments result in permission denial (returns `false`)
- Permission checks happen before argument validation and function execution
- All permission failures are logged for audit purposes
- Use specific permission modes rather than `false` when possible
"""
alias PhoenixGenApi.Structs.{FunConfig, Request}
require Logger
@doc """
Exception raised when a permission check fails.
Contains details about the request and the permission mode that was denied.
"""
defmodule PermissionDenied do
@moduledoc false
defexception [:message, :user_id, :request_id, :request_type, :permission_mode]
@impl true
def exception(opts) do
user_id = Keyword.get(opts, :user_id)
request_id = Keyword.get(opts, :request_id)
request_type = Keyword.get(opts, :request_type)
permission_mode = Keyword.get(opts, :permission_mode)
message =
"Permission denied for user: #{inspect(user_id)}, " <>
"request: #{inspect(request_id)}, " <>
"type: #{inspect(request_type)}, " <>
"mode: #{inspect(permission_mode)}"
%__MODULE__{
message: message,
user_id: user_id,
request_id: request_id,
request_type: request_type,
permission_mode: permission_mode
}
end
end
def check_permission(%Request{}, %FunConfig{check_permission: false}) do
true
end
def check_permission(%Request{user_id: user_id}, %FunConfig{check_permission: :any_authenticated})
when is_binary(user_id) or is_nil(user_id) do
true
end
def check_permission(%Request{user_id: nil}, %FunConfig{check_permission: :any_authenticated}) do
false
end
def check_permission(%Request{user_id: user_id}, %FunConfig{check_permission: :any_authenticated})
when is_binary(user_id) and byte_size(user_id) > 0 do
true
end
def check_permission(%Request{}, %FunConfig{check_permission: :any_authenticated}) do
false
end
def check_permission(
request = %Request{user_id: user_id},
%FunConfig{check_permission: {:arg, arg_name}}
)
when is_binary(user_id) and byte_size(user_id) > 0 do
case Map.get(request.args, arg_name) do
nil ->
Logger.warning(
"PhoenixGenApi.Permission, check_permission, missing argument #{inspect(arg_name)} in request: #{inspect(request.request_id)}"
)
false
^user_id ->
true
_other ->
false
end
end
def check_permission(%Request{}, %FunConfig{check_permission: {:arg, _arg_name}}) do
false
end
def check_permission(
%Request{user_roles: user_roles} = request,
%FunConfig{check_permission: {:role, allowed_roles}}
)
when is_list(user_roles) and is_list(allowed_roles) do
allowed_roles_set = MapSet.new(allowed_roles)
user_roles_set = MapSet.new(user_roles)
case MapSet.intersection(user_roles_set, allowed_roles_set) |> MapSet.size() do
0 ->
Logger.warning(
"PhoenixGenApi.Permission, check_permission, user #{inspect(request.user_id)} lacks required roles, request: #{inspect(request.request_id)}"
)
false
_ ->
true
end
end
def check_permission(%Request{}, %FunConfig{check_permission: {:role, _allowed_roles}}) do
false
end
def check_permission(%Request{}, fun_config = %FunConfig{}) do
Logger.error(
"PhoenixGenApi.Permission, check_permission, invalid permission mode: #{inspect(fun_config.check_permission)}"
)
false
end
@doc """
Checks permission and raises an exception if the check fails.
This is the raising version of `check_permission/2`. It should be used in the
request execution pipeline where permission failures should halt processing.
## Parameters
- `request` - The `Request` struct containing user information and arguments
- `fun_config` - The `FunConfig` struct with permission settings
## Returns
- `nil` - Permission check passed (function returns nothing on success)
## Raises
- `PermissionDenied` - If the permission check fails
## Examples
request = %Request{
user_id: "user_123",
args: %{"user_id" => "user_999"}
}
config = %FunConfig{check_permission: {:arg, "user_id"}}
# This will raise because user_ids don't match
Permission.check_permission!(request, config)
# ** (PhoenixGenApi.Permission.PermissionDenied) Permission denied...
## Notes
- Logs a warning with request details when permission is denied
- Always called before request execution in the Executor module
- Returns nothing on success (only side effect is potential exception)
"""
def check_permission!(request = %Request{}, fun_config = %FunConfig{}) do
if not check_permission(request, fun_config) do
Logger.warning(
"PhoenixGenApi.Permission, check_permission!, denied, user: #{inspect(request.user_id)}, " <>
"request_id: #{inspect(request.request_id)}, " <>
"request_type: #{inspect(request.request_type)}, " <>
"permission_mode: #{inspect(fun_config.check_permission)}"
)
raise PermissionDenied,
user_id: request.user_id,
request_id: request.request_id,
request_type: request.request_type,
permission_mode: fun_config.check_permission
end
nil
end
end