Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge has_user_access?/4 args and pass entire object to has_user_access?/3 #20

Merged
merged 12 commits into from
Nov 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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