Rajska

Coverage Status

Rajska is an elixir authorization library for Absinthe.

It provides the following middlewares:

Documentation can be found at https://hexdocs.pm/rajska/.

Installation

The package can be installed by adding rajska to your list of dependencies in mix.exs:

def deps do
[
{:rajska, "~> 0.3.0"},
]
end

Usage

Create your Authorization module, which will implement the Rajska Authorization behaviour and contain the logic to validate user permissions and will be called by Rajska middlewares. Rajska provides some helper functions by default, such as is_role_authorized?/2, has_user_access?/3 and is_field_authorized?/3, but you can override them with your application needs.

defmodule Authorization do
use Rajska,
roles: [:user, :admin]
end

Note: if you pass a non Keyword list to roles, as above, Rajska will assume your roles are in ascending order and the last one is the super role. You can override this behavior by defining your own is_super_role?/1 function or define your roles as a Keyword list in the format [user: 0, admin: 1].

Add your Authorization module to your Absinthe.Schemacontext/1 callback and the desired middlewares to the middleware/3 callback:

def context(ctx), do: Map.put(ctx, :authorization, Authorization)
def middleware(middleware, field, %Absinthe.Type.Object{identifier: identifier})
when identifier in [:query, :mutation] do
middleware
|> Rajska.add_query_authorization(field, Authorization)
|> Rajska.add_object_authorization()
end
def middleware(middleware, field, object) do
Rajska.add_field_authorization(middleware, field, object)
end

The only exception is Object Scope Authorization, which isn't a middleware, but an Absinthe Phase. To use it, add it to your pipeline after the resolution:

# router.ex
alias Absinthe.Phase.Document.Execution.Resolution
alias Absinthe.Pipeline
alias Rajska.ObjectScopeAuthorization
forward "/graphql", Absinthe.Plug,
schema: MyProjectWeb.Schema,
socket: MyProjectWeb.UserSocket,
pipeline: {__MODULE__, :pipeline} # Add this line
def pipeline(config, pipeline_opts) do
config
|> Map.fetch!(:schema_mod)
|> Pipeline.for_document(pipeline_opts)
|> Pipeline.insert_after(Resolution, ObjectScopeAuthorization)
end

Since Query Scope Authorization middleware must be used with Query Authorization, it is automatically called when adding the former.

Middlewares usage can be found below.

Middlewares

Query Authorization

Ensures Absinthe's queries can only be accessed by determined users.

Usage:

Create your Authorization module and add it and QueryAuthorization to your Absinthe.Schema. Then set the permitted role to access a query or mutation:

mutation do
field :create_user, :user do
arg :params, non_null(:user_params)
middleware Rajska.QueryAuthorization, permit: :all
resolve &AccountsResolver.create_user/2
end
field :update_user, :user do
arg :id, non_null(:integer)
arg :params, non_null(:user_params)
middleware Rajska.QueryAuthorization, [permit: :user, scoped: User] # same as {User, :id}
resolve &AccountsResolver.update_user/2
end
field :delete_user, :user do
arg :id, non_null(:integer)
middleware Rajska.QueryAuthorization, permit: :admin
resolve &AccountsResolver.delete_user/2
end
end

Query authorization will call is_role_authorized?/2 to check if the userrole is authorized to perform the query.

Query Scope Authorization

Provides scoping to Absinthe's queries, as seen above in Query Authorization.

In the above example, :all and :admin permissions don't require the :scoped keyword, as defined in the not_scoped_roles/0 function, but you can modify this behavior by overriding it.

Valid values for the :scoped keyword are:

Object Authorization

Authorizes all Absinthe's objects requested in a query by checking the permission defined in each object meta authorize.

Usage:

Create your Authorization module and add it and ObjectAuthorization to your Absinthe.Schema. Then set the permitted role to access an object:

object :wallet_balance do
meta :authorize, :admin
field :total, :integer
end
object :company do
meta :authorize, :user
field :name, :string
field :wallet_balance, :wallet_balance
end
object :user do
meta :authorize, :all
field :email, :string
field :company, :company
end

With the permissions above, a query like the following would only be allowed by an admin user:

{
userQuery {
name
email
company {
name
walletBalance { total }
}
}
}

Object Authorization middleware runs after Query Authorization middleware (if added) and before the query is resolved by recursively checking the requested objects permissions in the is_role_authorized?/2 function (which is also used by Query Authorization). It can be overridden by your own implementation.

Object Scope Authorization

Absinthe Phase to perform object scoping.

Authorizes all Absinthe's objects requested in a query by checking the value of the field defined in each object meta scope.

Usage:

Create your Authorization module and add it and ObjectScopeAuthorization to your Absinthe pipeline. Then set the scope of an object:

object :user do
meta :scope, User # Same as meta :scope, {User, :id}
field :id, :integer
field :email, :string
field :name, :string
field :company, :company
end
object :company do
meta :scope, {Company, :user_id}
field :id, :integer
field :user_id, :integer
field :name, :string
field :wallet, :wallet
end
object :wallet do
meta :scope, Wallet
field :total, :integer
end

To define custom rules for the scoping, use has_user_access?/3. For example:

defmodule Authorization do
use Rajska,
roles: [:user, :admin]
@impl true
def has_user_access?(%{role: :admin}, User, _id), do: true
def has_user_access?(%{id: user_id}, User, id) when user_id === id, do: true
def has_user_access?(_current_user, User, _id), do: false
end

Keep in mind that the field_value provided to has_user_access?/3 can be nil. This case can be handled as you wish. For example, to not raise any authorization errors and just return nil:

defmodule Authorization do
use Rajska,
roles: [:user, :admin]
@impl true
def has_user_access?(_user, _, nil), do: true
def has_user_access?(%{role: :admin}, User, _id), do: true
def has_user_access?(%{id: user_id}, User, id) when user_id === id, do: true
def has_user_access?(_current_user, User, _id), do: false
end

Field Authorization

Authorizes Absinthe's object field according to the result of the is_field_authorized?/3 function, which receives the user role, the meta scope_by atom defined in the object schema and the source object that is resolving the field.

Usage:

Create your Authorization module and add it and FieldAuthorization to your Absinthe.Schema. Then add the meta scope_by to an object and meta private to your sensitive fields:

object :user do
meta :scope_by, :id
field :name, :string
field :is_email_public, :boolean
field :phone, :string, meta: [private: true]
field :email, :string, meta: [private: & !&1.is_email_public]
end

As seen in the example above, a function can also be passed as value to the meta :private key, in order to check if a field is private dynamically, depending of the value of another field.

Crudry is an elixir library for DRYing CRUD of Phoenix Contexts and Absinthe Resolvers.

License

MIT License.

See LICENSE for more information.