Current section
Files
Jump to
Current section
Files
lib/permit/resolver_base.ex
defmodule Permit.ResolverBase do
@moduledoc ~S"""
Provides a basis for building _resolver modules_. A resolver is conceptually a module containing functions answering the following questions:
* Given current **permission configuration**, is a **subject** authorized to perform a certain **action** on a given **resource**?
* Given current **permission configuration**, a certain **subject**, an **action** and a **resource**, as well as an **execution context** (e.g. including controller parameters, loader functions, etc.), load a resource (e.g. by ID) and check whether the subject is authorized to perform the action on the resource.
* Given current **permission configuration**, a certain **subject**, an **action** and a **resource**, as well as an **execution context**, load all resources on which the subject can perform the action.
The `Permit.ResolverBase` module implements `authorized?/4`, `authorize_and_preload_one!/5` and `authorize_and_preload_all!/5` functions to check against the permissions and provide a uniform API for the outcome of the resolution.
Creating a resolver (see `Permit.Ecto.Resolver` as an example) requires the developer to implement the `c:resolve/6` callback that fetches data to be authorized against.
Replacing the standard resolver (`Permit.Resolver`) with a more specific one (e.g. `Permit.Ecto.Resolver` from the `permit_ecto` library) is done by the usage of a different resolver module (e.g. `Permit.Ecto.Resolver`) instead of `Permit.Resolver` in the `__using__/1` macro of the module which implements the `Permit` behaviour. The `resolver_module/0` function has to be overridden to point to the new resolver (see `Permit.Ecto` for sample usage).
"""
alias Permit.Types
@doc ~S"""
Implement to define a resolver's behavior.
The callback takes arguments in the following order:
* `subject` typically takes the current user (or a record that `Permit.SubjectMapping` maps it to),
* `authorization_module` takes the application's authorization configuration (i.e. the module that calls `use Permit` or `use Permit.Ecto`, or any other module with `Permit` behaviour),
* `resource_module` takes the resource module - often, in Ecto applications, it's an Ecto schema,
* `action_group` takes a name of an action, e.g. in Phoenix it's taken from a controller `conn` or a LiveView `socket`'s `live_action`
* `meta` - depending on the resolver's needs, it will carry metadata such as loader functions, Ecto query processing functions, controller params, etc. - this generally is private to the integration library a developer is creating,
* `arity` - it takes `:all` if plural resolution is to be performed (e.g. `:index`), and `:one` if singular resolution is performed (e.g. `:show`)
The callback implementation should use the arguments, including authorization context and application-specific context (particularly in `:meta` key), to resolve and return a record or records according to the spec.
Returned value:
* `{:authorized, object}` in a singular action if a record is found and authorization is granted,
* `{:authorized, [object]}` in a plural action if authorization to given action is granted - and it is assumed that the resolver filters out records that are not authorized,
* `:not_found` in a singular action if no record found, and thus authorization cannot be checked _(note that in plural actions `{:authorized, []}` should be returned)
* `:unauthorized` in a singular action if a record is found but authorization check is negative, or in a singular or plural action if the action itself is not authorized at all.
"""
@callback resolve(
Types.subject(),
Types.authorization_module(),
Types.resource_module(),
Types.action_group(),
Types.resolution_context(),
:all | :one
) :: {:authorized, Types.object() | [Types.object()]} | :unauthorized | :not_found
defmacro __using__(_opts) do
quote do
@behaviour unquote(__MODULE__)
@spec authorized?(
Types.subject(),
module(),
Types.object_or_resource_module(),
Types.action_group()
) :: boolean()
def authorized?(subject, authorization_module, resource_or_module, action)
when not is_nil(subject) do
Permit.ResolverBase.authorized?(subject, authorization_module, resource_or_module, action)
end
@spec authorize_and_preload_one!(
Types.subject(),
module(),
Types.resource_module(),
Types.action_group(),
map()
) :: {:authorized, [struct()]} | :unauthorized
def authorize_and_preload_one!(subject, authorization_module, resource_module, action, meta)
when not is_nil(subject) do
resolve(subject, authorization_module, resource_module, action, meta, :one)
end
# Possible spec improvement:
# originally term() was Ecto.Queryable.t(), figure out how to make it play with Ecto separated
@spec authorize_and_preload_all!(
Types.subject(),
module(),
Types.resource_module(),
Types.action_group(),
map()
) :: {:authorized, [struct()]} | :unauthorized | {:not_found, term()}
def authorize_and_preload_all!(subject, authorization_module, resource_module, action, meta)
when not is_nil(subject) do
resolve(subject, authorization_module, resource_module, action, meta, :all)
end
end
end
@doc false
def authorized?(subject, authorization_module, resource_or_module, action) do
auth = authorization_module.can(subject)
actions_module = authorization_module.actions_module()
verify_fn = &Permit.verify_record(auth, &1, resource_or_module)
Permit.Actions.verify_transitively!(actions_module, action, verify_fn)
end
end