Skip to content

Commit

Permalink
Merge a0ce7f6 into f100ab9
Browse files Browse the repository at this point in the history
  • Loading branch information
rschef authored Oct 7, 2019
2 parents f100ab9 + a0ce7f6 commit 9dea9f1
Show file tree
Hide file tree
Showing 15 changed files with 253 additions and 142 deletions.
24 changes: 13 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,24 @@ The package can be installed by adding `rajska` to your list of dependencies in
```elixir
def deps do
[
{:rajska, "~> 0.4.1"},
{:rajska, "~> 0.5.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 [is_role_authorized?/2](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:is_role_authorized?/2), [has_user_access?/4](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/4) and [is_field_authorized?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:is_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), [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.

```elixir
defmodule Authorization do
use Rajska,
roles: [:user, :admin]
valid_roles: [:user, :admin],
super_role: :admin,
default_rule: :default
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](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
Expand Down Expand Up @@ -117,13 +117,13 @@ Usage:
end
```

Query authorization will call [is_role_authorized?/2](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:is_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 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).

In the above example, `:all` and `:admin` permissions don't require the `:scoped` keyword, as defined in the [not_scoped_roles/0](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:not_scoped_roles/0) function, but you can modify this behavior by overriding it.
In the above example, `:all` and `:admin` (`super_role`) permissions don't require the `:scoped` 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.

Valid values for the `:scoped` keyword are:

Expand Down Expand Up @@ -180,7 +180,7 @@ With the permissions above, a query like the following would only be allowed by
}
```

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](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:is_role_authorized?/2) function (which is also used by Query Authorization). It can be overridden by your own implementation.
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 [role_authorized?/2](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:role_authorized?/2) function (which is also used by Query Authorization). It can be overridden by your own implementation.

### Object Scope Authorization

Expand Down Expand Up @@ -224,7 +224,8 @@ To define custom rules for the scoping, use [has_user_access?/4](https://hexdocs
```elixir
defmodule Authorization do
use Rajska,
roles: [:user, :admin]
valid_roles: [:user, :admin],
super_role: :admin

@impl true
def has_user_access?(%{role: :admin}, User, _id, _rule), do: true
Expand All @@ -239,7 +240,8 @@ For example, to not raise any authorization errors and just return `nil`:
```elixir
defmodule Authorization do
use Rajska,
roles: [:user, :admin]
valid_roles: [:user, :admin],
super_role: :admin

@impl true
def has_user_access?(_user, _, nil), do: true
Expand All @@ -252,7 +254,7 @@ end

### Field Authorization

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

Usage:

Expand Down
14 changes: 4 additions & 10 deletions lib/authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,9 @@ defmodule Rajska.Authorization do

@callback not_scoped_roles() :: list(role)

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

@callback is_all_role?(role) :: boolean()

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

@callback is_field_authorized?(current_user_role, scope_by :: atom(), source :: map()) :: boolean()
@callback field_authorized?(current_user_role, scope_by :: atom(), source :: map()) :: boolean()

@callback has_user_access?(current_user, scoped_struct :: module(), field_value :: any(), rule :: any()) :: boolean()

Expand All @@ -30,10 +26,8 @@ defmodule Rajska.Authorization do
@optional_callbacks get_current_user: 1,
get_user_role: 1,
not_scoped_roles: 0,
is_super_role?: 1,
is_all_role?: 1,
is_role_authorized?: 2,
is_field_authorized?: 3,
role_authorized?: 2,
field_authorized?: 3,
has_user_access?: 4,
unauthorized_msg: 1
end
18 changes: 9 additions & 9 deletions lib/middlewares/field_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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.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.
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.
## Usage
Expand Down Expand Up @@ -31,18 +31,18 @@ defmodule Rajska.FieldAuthorization do
}

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

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

defp is_field_private?(true, _source), do: true
defp is_field_private?(private, source) when is_function(private), do: private.(source)
defp is_field_private?(_private, _source), do: false
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

Expand All @@ -56,9 +56,9 @@ defmodule Rajska.FieldAuthorization do
defp authorized?(_context, false, _scope_by, _source), do: true

defp authorized?(context, true, scope_by, source) do
case Rajska.apply_auth_mod(context, :is_super_user?, [context]) do
case Rajska.apply_auth_mod(context, :super_user?, [context]) do
true -> true
false -> Rajska.apply_auth_mod(context, :is_context_field_authorized?, [context, scope_by, source])
false -> Rajska.apply_auth_mod(context, :context_field_authorized?, [context, scope_by, source])
end
end

Expand Down
10 changes: 5 additions & 5 deletions lib/middlewares/object_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ defmodule Rajska.ObjectAuthorization do
}
```
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 `c:Rajska.Authorization.is_role_authorized?/2` function (which is also used by Query Authorization). It can be overridden by your own implementation.
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 `c:Rajska.Authorization.role_authorized?/2` function (which is also used by Query Authorization). It can be overridden by your own implementation.
"""

@behaviour Absinthe.Middleware
Expand Down Expand Up @@ -87,14 +87,14 @@ defmodule Rajska.ObjectAuthorization do
defp authorize_object(object, fields, resolution) do
object
|> Type.meta(:authorize)
|> is_authorized?(resolution.context, object)
|> authorized?(resolution.context, object)
|> put_result(fields, resolution, object)
end

defp is_authorized?(nil, _, object), do: raise "No meta authorize defined for object #{inspect object.identifier}"
defp authorized?(nil, _, object), do: raise "No meta authorize defined for object #{inspect object.identifier}"

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

defp put_result(true, fields, resolution, _type), do: find_associations(fields, resolution)
Expand Down
16 changes: 8 additions & 8 deletions lib/middlewares/object_scope_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ defmodule Rajska.ObjectScopeAuthorization do
```elixir
defmodule Authorization do
use Rajska,
roles: [:user, :admin]
valid_roles: [:user, :admin]
@impl true
def has_user_access?(%{role: :admin}, _struct, _field_value, _rule), do: true
Expand All @@ -58,7 +58,7 @@ defmodule Rajska.ObjectScopeAuthorization do
```elixir
defmodule Authorization do
use Rajska,
roles: [:user, :admin]
valid_roles: [:user, :admin]
@impl true
def has_user_access?(_user, _, nil, _), do: true
Expand All @@ -74,7 +74,7 @@ defmodule Rajska.ObjectScopeAuthorization do
```elixir
defmodule Authorization do
use Rajska,
roles: [:user, :admin]
valid_roles: [:user, :admin]
@impl true
def has_user_access?(%{id: user_id}, Wallet, _field_value, :read_only), do: true
Expand Down Expand Up @@ -118,7 +118,7 @@ defmodule Rajska.ObjectScopeAuthorization do
default_rule = Rajska.apply_auth_mod(context, :default_rule)
rule = Type.meta(type, :rule) || default_rule

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

defp is_authorized?(nil, _values, _context, _, object), do: raise "No meta scope defined for object #{inspect object.identifier}"
defp authorized?(nil, _values, _context, _, object), do: raise "No meta scope defined for object #{inspect object.identifier}"

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

defp is_authorized?({scoped_struct, field}, values, context, rule, _object) do
defp authorized?({scoped_struct, field}, values, context, rule, _object) do
scoped_field_value = Map.get(values, field)
Rajska.apply_auth_mod(context, :has_context_access?, [context, scoped_struct, scoped_field_value, rule])
end

defp is_authorized?(scoped_struct, values, context, rule, _object) do
defp authorized?(scoped_struct, values, context, rule, _object) do
scoped_field_value = Map.get(values, :id)
Rajska.apply_auth_mod(context, :has_context_access?, [context, scoped_struct, scoped_field_value, rule])
end
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 @@ -32,7 +32,7 @@ defmodule Rajska.QueryAuthorization do
end
```
Query authorization will call `c:Rajska.Authorization.is_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 authorization will call `c:Rajska.Authorization.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.
"""
alias Absinthe.Resolution

Expand All @@ -44,7 +44,7 @@ defmodule Rajska.QueryAuthorization do
validate_permission!(context, permission)

context
|> Rajska.apply_auth_mod(:is_context_authorized?, [context, permission])
|> Rajska.apply_auth_mod(:context_authorized?, [context, permission])
|> update_result(resolution)
|> QueryScopeAuthorization.call(config)
end
Expand Down
17 changes: 1 addition & 16 deletions lib/middlewares/query_scope_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ defmodule Rajska.QueryScopeAuthorization do

@behaviour Absinthe.Middleware

alias Absinthe.{Resolution, Type}
alias Absinthe.Resolution

alias Rajska.Introspection

Expand Down Expand Up @@ -99,19 +99,6 @@ defmodule Rajska.QueryScopeAuthorization do
apply_scope_authorization(resolution, get_scoped_field_value(args, :id), scoped_struct, rule)
end

def scope_user!(
%Resolution{
definition: %{
name: name,
schema_node: %{type: %Type.List{of_type: _}}
}
},
_,
_scoped_config
) do
raise "Error in query #{name}: Scope Authorization can't be used with a list query object type"
end

def scope_user!(%Resolution{definition: %{name: name}}, _, _scoped_config) do
raise "Error in query #{name}: no scoped argument found in middleware Scope Authorization"
end
Expand Down Expand Up @@ -139,8 +126,6 @@ defmodule Rajska.QueryScopeAuthorization do
put_error(resolution, "Not authorized to access this #{replace_underscore(object_type)}")
end

defp update_result({:error, msg}, resolution), do: put_error(resolution, msg)

defp put_error(resolution, message), do: Resolution.put_result(resolution, {:error, message})

defp replace_underscore(string) when is_binary(string), do: String.replace(string, "_", " ")
Expand Down
Loading

0 comments on commit 9dea9f1

Please sign in to comment.