Skip to content

Commit

Permalink
Merge pull request #21 from rschef/improve-docs
Browse files Browse the repository at this point in the history
Improve docs
  • Loading branch information
rschef authored Dec 23, 2019
2 parents 22c550a + 8b061d1 commit b9582f1
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 158 deletions.
209 changes: 128 additions & 81 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,29 @@ end
Create your Authorization module, which will implement the [Rajska Authorization](https://hexdocs.pm/rajska/Rajska.Authorization.html) 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 [role_authorized?/2](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:role_authorized?/2) and [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3), but you can override them with your application needs.

```elixir
defmodule Authorization do
use Rajska,
valid_roles: [:user, :admin],
super_role: :admin,
default_rule: :default
end
defmodule Authorization do
use Rajska,
valid_roles: [:user, :admin],
super_role: :admin,
default_rule: :default
end
```

Add your [Authorization](https://hexdocs.pm/rajska/Rajska.Authorization.html) module to your `Absinthe.Schema` [context/1](https://hexdocs.pm/absinthe/Absinthe.Schema.html#c:context/1) callback and the desired middlewares to the [middleware/3](https://hexdocs.pm/absinthe/Absinthe.Middleware.html#module-the-middleware-3-callback) callback:

```elixir
def context(ctx), do: Map.put(ctx, :authorization, Authorization)
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, %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
def middleware(middleware, field, object) do
Rajska.add_field_authorization(middleware, field, object)
end
```

The only exception is [Object Scope Authorization](#object-scope-authorization), which isn't a middleware, but an [Absinthe Phase](https://hexdocs.pm/absinthe/Absinthe.Phase.html). To use it, add it to your pipeline after the resolution:
Expand Down Expand Up @@ -87,45 +87,91 @@ Middlewares usage can be found below.

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

Usage:
#### Usage:

[Create your Authorization module and add it and QueryAuthorization to your Absinthe.Schema](#usage). Then set the permitted role to access a query or mutation:

```elixir
mutation do
field :create_user, :user do
arg :params, non_null(:user_params)
mutation do
field :create_user, :user do
arg :params, non_null(:user_params)

middleware Rajska.QueryAuthorization, permit: :all
resolve &AccountsResolver.create_user/2
end
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)
field :update_user, :user do
arg :id, non_null(:integer)
arg :params, non_null(:user_params)

middleware Rajska.QueryAuthorization, [permit: :user, scope: User] # same as [permit: :user, scope: User, args: :id]
resolve &AccountsResolver.update_user/2
end
middleware Rajska.QueryAuthorization, [permit: [:user, :manager], scope: false]
resolve &AccountsResolver.update_user/2
end

field :delete_user, :user do
arg :id, non_null(:integer)
field :invite_user, :user do
arg :email, non_null(:string)

middleware Rajska.QueryAuthorization, permit: :admin
resolve &AccountsResolver.delete_user/2
end
middleware Rajska.QueryAuthorization, permit: :admin
resolve &AccountsResolver.invite_user/2
end
end
```

Query authorization will call [role_authorized?/2](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:role_authorized?/2) to check if the [user](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:get_current_user/1) [role](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:get_user_role/1) is authorized to perform the query.

### Query Scope Authorization

Provides scoping to Absinthe's queries, as seen above in [Query Authorization](#query-authorization).
Provides scoping to Absinthe's queries, allowing for more complex authorization rules. It is used together with [Query Authorization](#query-authorization).

```elixir
mutation do
field :create_user, :user do
arg :params, non_null(:user_params)

# all does not require scoping, since it means anyone can execute this query, even without being logged in.
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, scope: User] # same as [permit: :user, scope: User, args: :id]
resolve &AccountsResolver.update_user/2
end

field :delete_user, :user do
arg :user_id, non_null(:integer)

# Providing a map for args is useful to map query argument to struct field.
middleware Rajska.QueryAuthorization, [permit: [:user, :manager], scope: User, args: %{id: :user_id}]
resolve &AccountsResolver.delete_user/2
end

input_object :user_params do
field :id, non_null(:integer)
end

field :accept_user, :user do
arg :params, non_null(:user_params)

middleware Rajska.QueryAuthorization, [
permit: :user,
scope: User,
args: %{id: [:params, :id]},
rule: :accept_user
]
resolve &AccountsResolver.invite_user/2
end
end
```

In the above example, `:all` and `:admin` (`super_role`) permissions don't require the `:scope` keyword, but you can modify this behavior by overriding the [not_scoped_roles/0](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:not_scoped_roles/0) function.

## Options
There are also extra options for this middleware, supporting the definition of custom rules, access of nested parameters and allowing optional parameters. All possibilities are listed below:

#### Options

All the following options are sent to [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3):

Expand All @@ -143,38 +189,36 @@ All the following options are sent to [has_user_access?/3](https://hexdocs.pm/ra

Authorizes all Absinthe's [objects](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#object/3) requested in a query by checking the permission defined in each object meta `authorize`.

Usage:
#### Usage:

[Create your Authorization module and add it and ObjectAuthorization to your Absinthe.Schema](#usage). Then set the permitted role to access an object:

```elixir
object :wallet_balance do
meta :authorize, :admin
object :wallet_balance do
meta :authorize, :admin

field :total, :integer
end

object :company do
meta :authorize, :user

field :name, :string
field :total, :integer
end

field :wallet_balance, :wallet_balance
end
object :company do
meta :authorize, :user

object :user do
meta :authorize, :all
field :name, :string
field :wallet_balance, :wallet_balance
end

field :email, :string
object :user do
meta :authorize, :all

field :company, :company
end
field :email, :string
field :company, :company
end
```

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

```graphql
{
{
userQuery {
name
email
Expand All @@ -192,15 +236,16 @@ Object Authorization middleware runs after Query Authorization middleware (if ad

Absinthe Phase to perform object scoping.

Authorizes all Absinthe's [objects](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#object/3) requested in a query by checking the value of the field defined in each object meta `scope`.
Authorizes all Absinthe's [objects](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#object/3) requested in a query by checking the underlying struct.

Usage:
#### Usage:

[Create your Authorization module and add it and ObjectScopeAuthorization to your Absinthe pipeline](#usage). Then set the scope of an object:

```elixir
object :user do
meta :scope, User # Same as meta :scope, {User, :id}
# Turn on both Object and Field scoping, but if the FieldAuthorization middleware is not included, this is the same as using `scope_object?`
meta :scope?, true

field :id, :integer
field :email, :string
Expand All @@ -210,7 +255,7 @@ object :user do
end

object :company do
meta :scope, {Company, :user_id}
meta :scope_object?, true

field :id, :integer
field :user_id, :integer
Expand All @@ -219,7 +264,8 @@ object :company do
end

object :wallet do
meta :scope, Wallet
meta :scope?, true
meta :rule, :object_authorization

field :total, :integer
end
Expand All @@ -237,41 +283,42 @@ defmodule Authorization do
def has_user_access?(%{role: :admin}, %User{}, _rule), do: true
def has_user_access?(%{id: user_id}, %User{id: id}, _rule) when user_id === id, do: true
def has_user_access?(_current_user, %User{}, _rule), 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`:

```elixir
defmodule Authorization do
use Rajska,
valid_roles: [:user, :admin],
super_role: :admin

@impl true
def has_user_access?(%{role: :admin}, %User{}, _rule), do: true
def has_user_access?(%{id: user_id}, %User{id: id}, _rule) when user_id === id, do: true
def has_user_access?(_current_user, %User{}, _rule), do: false
def has_user_access?(%{id: user_id}, %Wallet{user_id: id}, :object_authorization), do: user_id == id
end
```

This way different rules can be set to the same struct.

### Field Authorization

Authorizes Absinthe's object [field](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4) according to the result of the [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3) function, which receives the user role, the `source` object that is resolving the field and the field rule.

Usage:
#### Usage:

[Create your Authorization module and add it and FieldAuthorization to your Absinthe.Schema](#usage).

```elixir
object :user do
field :name, :string
field :is_email_public, :boolean
object :user do
# Turn on both Object and Field scoping, but if the ObjectScope Phase is not included, this is the same as using `scope_field?`
meta :scope?, true

field :phone, :string, meta: [private: true]
field :email, :string, meta: [private: & !&1.is_email_public]
end
field :name, :string
field :is_email_public, :boolean

field :phone, :string, meta: [private: true]
field :email, :string, meta: [private: & !&1.is_email_public]

# Can also use custom rules for each field
field :always_private, :string, meta: [private: true, rule: :private]
end

object :field_scope_user do
meta :scope_field?, true

field :name, :string
field :phone, :string, meta: [private: true]
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.
Expand Down
25 changes: 19 additions & 6 deletions lib/middlewares/field_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,26 @@ defmodule Rajska.FieldAuthorization do
[Create your Authorization module and add it and FieldAuthorization to your Absinthe.Schema](https://hexdocs.pm/rajska/Rajska.html#module-usage).
```elixir
object :user do
field :name, :string
field :is_email_public, :boolean
object :user do
# Turn on both Object and Field scoping, but if the ObjectScope Phase is not included, this is the same as using `scope_field?`
meta :scope?, true
field :phone, :string, meta: [private: true]
field :email, :string, meta: [private: & !&1.is_email_public]
end
field :name, :string
field :is_email_public, :boolean
field :phone, :string, meta: [private: true]
field :email, :string, meta: [private: & !&1.is_email_public]
# Can also use custom rules for each field
field :always_private, :string, meta: [private: true, rule: :private]
end
object :field_scope_user do
meta :scope_field?, true
field :name, :string
field :phone, :string, meta: [private: true]
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.
Expand Down
Loading

0 comments on commit b9582f1

Please sign in to comment.