Skip to content

Commit

Permalink
Merge pull request #20 from rschef/19-merge-has-user-access-args
Browse files Browse the repository at this point in the history
Merge has_user_access?/4 args and pass entire object to has_user_access?/3
  • Loading branch information
rschef authored Nov 7, 2019
2 parents ab73806 + c04b4b1 commit 89c2d66
Show file tree
Hide file tree
Showing 14 changed files with 178 additions and 244 deletions.
34 changes: 15 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ The package can be installed by adding `rajska` to your list of dependencies in
```elixir
def deps do
[
{:rajska, "~> 0.8.1"},
{:rajska, "~> 0.9.0"},
]
end
```

## Usage

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), [has_user_access?/4](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/4) and [field_authorized?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:field_authorized?/3), but you can override them with your application needs.
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
Expand Down Expand Up @@ -127,14 +127,14 @@ In the above example, `:all` and `:admin` (`super_role`) permissions don't requi

## Options

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

* `:scope`
- `false`: disables scoping
- `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/4`. It must define a struct.
- `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/3`. It must define a struct.
* `:args`
- `%{user_id: [:params, :id]}`: where `user_id` is the scoped field and `id` is an argument nested inside the `params` argument.
- `:id`: this is the same as `%{id: :id}`, where `:id` is both the query argument and the scoped field that will be passed to [has_user_access?/4](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/4)
- `:id`: this is the same as `%{id: :id}`, where `:id` is both the query argument and the scoped field that will be passed to [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3)
- `[:code, :user_group_id]`: this is the same as `%{code: :code, user_group_id: :user_group_id}`, where `code` and `user_group_id` are both query arguments and scoped fields.
* `:optional` (optional) - when set to true the arguments are optional, so if no argument is provided, the query will be authorized. Defaults to false.
* `:rule` (optional) - allows the same struct to have different rules. See `Rajska.Authorization` for `rule` default settings.
Expand Down Expand Up @@ -225,7 +225,7 @@ object :wallet do
end
```

To define custom rules for the scoping, use [has_user_access?/4](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/4). For example:
To define custom rules for the scoping, use [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3). For example:

```elixir
defmodule Authorization do
Expand All @@ -234,13 +234,13 @@ defmodule Authorization do
super_role: :admin

@impl true
def has_user_access?(%{role: :admin}, User, _field, _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, _field, _rule), do: false
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?/4` can be `nil`. This case can be handled as you wish.
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
Expand All @@ -250,26 +250,22 @@ defmodule Authorization do
super_role: :admin

@impl true
def has_user_access?(_user, _scope, {_field, nil}, _rule), do: true

def has_user_access?(%{role: :admin}, User, _field, _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, _field, _rule), do: false
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
```

### Field Authorization

Authorizes Absinthe's object [field](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4) according to the result of the [field_authorized?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c: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.
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:

[Create your Authorization module and add it and FieldAuthorization to your Absinthe.Schema](#usage). Then add the meta `scope_by` to an object and meta `private` to your sensitive fields:
[Create your Authorization module and add it and FieldAuthorization to your Absinthe.Schema](#usage).

```elixir
object :user do
meta :scope_by, :id

field :name, :string
field :is_email_public, :boolean

Expand Down
25 changes: 13 additions & 12 deletions lib/authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,32 @@ defmodule Rajska.Authorization do
@type current_user :: any()
@type role :: atom()
@type current_user_role :: role
@type context :: map()
@type scoped_struct :: struct()
@type rule :: atom()

@callback get_current_user(context :: map()) :: current_user
@callback get_current_user(context) :: current_user

@callback get_user_role(current_user) :: role

@callback not_scoped_roles() :: list(role)

@callback role_authorized?(current_user_role, allowed_role :: role) :: boolean()

@callback field_authorized?(current_user_role, scope_by :: atom(), source :: map()) :: boolean()
@callback has_user_access?(current_user, scoped_struct, rule) :: boolean()

@callback has_user_access?(
current_user,
scope :: module(),
{field :: any(), field_value :: any()},
rule :: any()
) :: boolean()
@callback unauthorized_message(resolution :: Resolution.t()) :: String.t()

@callback unauthorized_msg(resolution :: Resolution.t()) :: String.t()
@callback context_role_authorized?(context, allowed_role :: role) :: boolean()

@callback context_user_authorized?(context, scoped_struct, rule) :: boolean()

@optional_callbacks get_current_user: 1,
get_user_role: 1,
not_scoped_roles: 0,
role_authorized?: 2,
field_authorized?: 3,
has_user_access?: 4,
unauthorized_msg: 1
has_user_access?: 3,
unauthorized_message: 1,
context_role_authorized?: 2,
context_user_authorized?: 3
end
40 changes: 15 additions & 25 deletions lib/middlewares/field_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ defmodule Rajska.FieldAuthorization do
@moduledoc """
Absinthe middleware to ensure field permissions.
Authorizes Absinthe's object [field](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4) according to the result of the `c:Rajska.Authorization.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.
Authorizes Absinthe's object [field](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4) according to the result of the `c:Rajska.Authorization.has_user_access?/3` function, which receives the user role, the `source` object that is resolving the field and the field rule.
## Usage
[Create your Authorization module and add it and FieldAuthorization to your Absinthe.Schema](https://hexdocs.pm/rajska/Rajska.html#module-usage). Then add the meta `scope_by` to an object and meta `private` to your sensitive fields:
[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
meta :scope_by, :id
field :name, :string
field :is_email_public, :boolean
Expand All @@ -32,45 +30,37 @@ defmodule Rajska.FieldAuthorization do

def call(resolution, [object: %Type.Object{fields: fields} = object, field: field]) do
field_private? = fields[field] |> Type.meta(:private) |> field_private?(resolution.source)
scope_by = get_scope_by_field!(object, field_private?)
scope? = get_scope!(object)

default_rule = Rajska.apply_auth_mod(resolution.context, :default_rule)
rule = Type.meta(fields[field], :rule) || default_rule

resolution
|> Map.get(:context)
|> authorized?(field_private?, scope_by, resolution, rule)
|> authorized?(scope? && field_private?, resolution.source, rule)
|> put_result(resolution, field)
end

defp field_private?(true, _source), do: true
defp field_private?(private, source) when is_function(private), do: private.(source)
defp field_private?(_private, _source), do: false

defp get_scope_by_field!(_object, false), do: :ok

defp get_scope_by_field!(object, _private) do
general_scope_by = Type.meta(object, :scope_by)
field_scope_by = Type.meta(object, :scope_field_by)
defp get_scope!(object) do
scope? = Type.meta(object, :scope?)
scope_field? = Type.meta(object, :scope_field?)

case {general_scope_by, field_scope_by} do
{nil, nil} -> raise "No meta scope_by or scope_field_by defined for object #{inspect object.identifier}"
{nil, field_scope_by} -> field_scope_by
{general_scope_by, nil} -> general_scope_by
{_, _} -> raise "Error in #{inspect object.identifier}. If scope_field_by is defined, then scope_by must not be defined"
case {scope?, scope_field?} do
{nil, nil} -> true
{nil, scope_field?} -> scope_field?
{scope?, nil} -> scope?
{_, _} -> raise "Error in #{inspect object.identifier}. If scope_field? is defined, then scope? must not be defined"
end
end

defp authorized?(_context, false, _scope_by, _source, _rule), do: true

defp authorized?(context, true, scope_by, %{source: %scope{} = source}, rule) do
field_value = Map.get(source, scope_by)

Rajska.apply_auth_mod(context, :has_context_access?, [context, scope, {scope_by, field_value}, rule])
end
defp authorized?(_context, false, _source, _rule), do: true

defp authorized?(_context, true, _scope_by, %{source: source, definition: definition}, _rule) do
raise "Expected a Struct for source object in field #{inspect(definition.name)}, got #{inspect(source)}"
defp authorized?(context, true, source, rule) do
Rajska.apply_auth_mod(context, :context_user_authorized?, [context, source, rule])
end

defp put_result(true, resolution, _field), do: resolution
Expand Down
2 changes: 1 addition & 1 deletion lib/middlewares/object_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ defmodule Rajska.ObjectAuthorization do
defp authorized?(nil, _, object), do: raise "No meta authorize defined for object #{inspect object.identifier}"

defp authorized?(permission, context, _object) do
Rajska.apply_auth_mod(context, :context_authorized?, [context, permission])
Rajska.apply_auth_mod(context, :context_role_authorized?, [context, permission])
end

defp put_result(true, fields, resolution, _type), do: find_associations(fields, resolution)
Expand Down
71 changes: 31 additions & 40 deletions lib/middlewares/object_scope_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ defmodule Rajska.ObjectScopeAuthorization do
```elixir
object :user do
meta :scope_by, :id
meta :rule, :default
field :id, :integer
Expand All @@ -21,7 +20,6 @@ defmodule Rajska.ObjectScopeAuthorization do
end
object :company do
meta :scope_by, :user_id
meta :rule, :default
field :id, :integer
Expand All @@ -31,29 +29,35 @@ defmodule Rajska.ObjectScopeAuthorization do
end
object :wallet do
meta :scope_by, :id
meta :rule, :read_only
field :id, :integer
field :total, :integer
end
object :available_dates do
meta :scope?, false
field :id, :integer
field :date, :date
end
```
To define custom rules for the scoping, use `c:Rajska.Authorization.has_user_access?/4`. For example:
To define custom rules for the scoping, use `c:Rajska.Authorization.has_user_access?/3`. For example:
```elixir
defmodule Authorization do
use Rajska,
valid_roles: [:user, :admin]
@impl true
def has_user_access?(%{role: :admin}, _struct, {_field, _field_value}, _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, {_field, _field_value}, _rule), do: false
def has_user_access?(%{role: :admin}, _, _scoped_struct, _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?/4` can be `nil`. This case can be handled as you wish.
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
Expand All @@ -62,24 +66,22 @@ defmodule Rajska.ObjectScopeAuthorization do
valid_roles: [:user, :admin]
@impl true
def has_user_access?(_user, _scope, {_field, nil}, _rule), do: true
def has_user_access?(%{role: :admin}, User, {_field, _field_value}, _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, {_field, _field_value}, _rule), do: false
def has_user_access?(%User{role: :admin}, _scoped_struct, _rule), do: true
def has_user_access?(%User{id: user_id}, %User{id: id}, _rule) when user_id === id, do: true
def has_user_access?(_current_user, %User{}, _rule), do: false
end
```
The `rule` keyword is not mandatory and will be pattern matched in `has_user_access?/4`:
The `rule` keyword is not mandatory and will be pattern matched in `has_user_access?/3`:
```elixir
defmodule Authorization do
use Rajska,
valid_roles: [:user, :admin]
@impl true
def has_user_access?(%{id: user_id}, Wallet, {_field, _field_value}, :read_only), do: true
def has_user_access?(%{id: user_id}, Wallet, {_field, _field_value}, :default), do: false
def has_user_access?(%{id: user_id}, %Wallet{}, :read_only), do: true
def has_user_access?(%{id: user_id}, %Wallet{}, :default), do: false
end
```
Expand Down Expand Up @@ -114,13 +116,11 @@ defmodule Rajska.ObjectScopeAuthorization do
# Object
defp result(%{fields: fields, emitter: %{schema_node: schema_node} = emitter, root_value: root_value} = result, context) do
type = Introspection.get_object_type(schema_node.type)
scope_by = get_scope_by!(type)
scope = get_scope!(scope_by, result)

scope? = get_scope!(type)
default_rule = Rajska.apply_auth_mod(context, :default_rule)
rule = Type.meta(type, :rule) || default_rule

case authorized?(scope, scope_by, root_value, context, rule, type) do
case authorized?(scope?, context, root_value, rule) do
true -> %{result | fields: walk_result(fields, context)}
false -> Map.put(result, :errors, [error(emitter)])
end
Expand All @@ -143,31 +143,22 @@ defmodule Rajska.ObjectScopeAuthorization do
walk_result(fields, context, new_fields)
end

defp get_scope_by!(object) do
general_scope_by = Type.meta(object, :scope_by)
object_scope_by = Type.meta(object, :scope_object_by)
defp get_scope!(object) do
scope? = Type.meta(object, :scope?)
scope_object? = Type.meta(object, :scope_object?)

case {general_scope_by, object_scope_by} do
{nil, nil} -> raise "No meta scope_by or scope_object_by defined for object #{inspect object.identifier}"
{nil, object_scope_by} -> object_scope_by
{general_scope_by, nil} -> general_scope_by
{_, _} -> raise "Error in #{inspect object.identifier}. If scope_object_by is defined, then scope_by must not be defined"
case {scope?, scope_object?} do
{nil, nil} -> true
{nil, scope_object?} -> scope_object?
{scope?, nil} -> scope?
{_, _} -> raise "Error in #{inspect object.identifier}. If scope_object? is defined, then scope? must not be defined"
end
end

defp get_scope!(false, _result), do: false
defp get_scope!(_scope_by, %{root_value: %scope{}}), do: scope
defp get_scope!(_scope_by, %{emitter: %{schema_node: schema_node}, root_value: root_value}) do
type = Introspection.get_object_type(schema_node.type)
raise "Expected a Struct for object #{inspect(type.identifier)}, got #{inspect(root_value)}"
end

defp authorized?(_scope, false, _values, _context, _, _object), do: true

defp authorized?(scope, scope_field, values, context, rule, _object) do
field_value = Map.get(values, scope_field)
defp authorized?(false, _context, _scoped_struct, _rule), do: true

Rajska.apply_auth_mod(context, :has_context_access?, [context, scope, {scope_field, field_value}, rule])
defp authorized?(true, context, scoped_struct, rule) do
Rajska.apply_auth_mod(context, :context_user_authorized?, [context, scoped_struct, rule])
end

defp error(%{source_location: location, schema_node: %{type: type}}) do
Expand Down
4 changes: 2 additions & 2 deletions lib/middlewares/query_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ defmodule Rajska.QueryAuthorization do
validate_permission!(context, permission)

context
|> Rajska.apply_auth_mod(:context_authorized?, [context, permission])
|> Rajska.apply_auth_mod(:context_role_authorized?, [context, permission])
|> update_result(resolution)
|> QueryScopeAuthorization.call(config)
end
Expand All @@ -71,6 +71,6 @@ defmodule Rajska.QueryAuthorization do
defp update_result(true, resolution), do: resolution

defp update_result(false, %{context: context} = resolution) do
Resolution.put_result(resolution, {:error, Rajska.apply_auth_mod(context, :unauthorized_msg, [resolution])})
Resolution.put_result(resolution, {:error, Rajska.apply_auth_mod(context, :unauthorized_message, [resolution])})
end
end
Loading

0 comments on commit 89c2d66

Please sign in to comment.