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

Support nested objects scoping #9

Merged
merged 9 commits into from
Sep 2, 2019
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Add your [Authorization](https://hexdocs.pm/rajska/Rajska.Authorization.html) mo
middleware
|> Rajska.add_query_authorization(field, Authorization)
|> Rajska.add_object_authorization()
|> Rajska.add_object_scope_auhtorization()
|> Rajska.add_object_scope_authorization()
end

def middleware(middleware, field, object) do
Expand Down Expand Up @@ -199,7 +199,7 @@ object :wallet do
end
```

To define custom rules for the scoping, use `c:Rajska.Authorization.has_user_access?/3`. 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 Down
9 changes: 5 additions & 4 deletions lib/middlewares/field_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ defmodule Rajska.FieldAuthorization do
scope_by = get_scope_by_field!(object, is_field_private?)

resolution
|> Map.get(:context)
|> authorized?(is_field_private?, scope_by, resolution.source)
|> put_result(resolution, field)
end
Expand All @@ -52,12 +53,12 @@ defmodule Rajska.FieldAuthorization do
end
end

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

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

Expand Down
6 changes: 3 additions & 3 deletions lib/middlewares/object_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,14 @@ defmodule Rajska.ObjectAuthorization do
defp authorize_object(object, fields, resolution) do
object
|> Type.meta(:authorize)
|> is_authorized?(resolution, object)
|> is_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 is_authorized?(permission, resolution, _object) do
Rajska.apply_auth_mod(resolution, :is_resolution_authorized?, [resolution, permission])
defp is_authorized?(permission, context, _object) do
Rajska.apply_auth_mod(context, :is_context_authorized?, [context, permission])
end

defp put_result(true, fields, resolution, _type), do: find_associations(fields, resolution)
Expand Down
117 changes: 52 additions & 65 deletions lib/middlewares/object_scope_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,97 +64,84 @@ defmodule Rajska.ObjectScopeAuthorization do
end
```
"""
@behaviour Absinthe.Middleware

alias Absinthe.{
Resolution,
Schema,
Type
}
alias Absinthe.{Blueprint, Phase, Type}
use Absinthe.Phase

def call(%Resolution{definition: definition} = resolution, _config) do
authorize(definition.schema_node.type, definition.selections, resolution)
@spec run(Blueprint.t() | Phase.Error.t(), Keyword.t()) :: {:ok, map}
def run(%Blueprint{execution: execution} = bp, _options \\ []) do
{:ok, %{bp | execution: process(execution)}}
end

defp authorize(type, fields, resolution, nested_keys \\ []) do
type
|> lookup_object(resolution.schema)
|> authorize_object(fields, resolution, nested_keys)
end
defp process(%{validation_errors: [], result: result} = execution), do: %{execution | result: result(result, execution.context)}
defp process(execution), do: execution

# When is a list, inspect object that composes the list.
defp lookup_object(%Type.List{of_type: object_type}, schema) do
lookup_object(object_type, schema)
# Root
defp result(%{fields: fields, emitter: %{schema_node: %{identifier: identifier}}} = result, context)
when identifier in [:query, :mutation, :subscription] do
%{result | fields: field_result(fields, context)}
end

defp lookup_object(object_type, schema) do
Schema.lookup_type(schema, object_type)
end
# Object
defp result(%{fields: fields, emitter: %{schema_node: schema_node} = emitter} = result, context) do
type = get_object_type(schema_node.type)
scope = Type.meta(type, :scope)

# When is a Scalar, Custom or Enum type, authorize.
defp authorize_object(%type{} = object, fields, resolution, nested_keys)
when type in [Scalar, Custom, Type.Enum, Type.Enum.Value] do
put_result(true, fields, resolution, object, nested_keys)
case is_authorized?(scope, result.root_value, context, type) do
true -> %{result | fields: field_result(fields, context)}
false -> Map.put(result, :errors, [error(emitter)])
end
end

# When is an user defined object, lookup the scope meta tag.
defp authorize_object(object, fields, resolution, nested_keys) do
object
|> Type.meta(:scope)
|> is_authorized?(resolution, object, nested_keys)
|> put_result(fields, resolution, object, nested_keys)
# List
defp result(%{values: values} = result, context) do
%{result | values: list_result(values, context)}
end

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

defp is_authorized?(false, _resolution, _object, _nested_keys), do: true
# When is a list, inspect object that composes the list.
defp get_object_type(%Type.List{of_type: object_type}), do: object_type
defp get_object_type(object_type), do: object_type

defp is_authorized?({scoped_struct, field}, resolution, _object, nested_keys) do
field_keys = nested_keys ++ [field]
apply_authorization!(resolution, scoped_struct, Map.get(resolution, :value), field_keys)
end
defp field_result(fields, context, new_fields \\ [])

defp is_authorized?(scoped_struct, resolution, _object, nested_keys) do
apply_authorization!(resolution, scoped_struct, Map.get(resolution, :value), nested_keys ++ [:id])
end
defp field_result([], _context, new_fields), do: new_fields

defp apply_authorization!(resolution, scoped_struct, values, keys) when is_list(values) do
Enum.all?(values, fn value ->
apply_authorization!(resolution, scoped_struct, value, keys)
end)
defp field_result([field | fields], context, new_fields) do
new_fields = [result(field, context) | new_fields]
field_result(fields, context, new_fields)
end

defp apply_authorization!(resolution, scoped_struct, nil, _keys) do
Rajska.apply_auth_mod(resolution, :has_resolution_access?, [resolution, scoped_struct, nil])
end
defp list_result(values, context, new_values \\ [])

defp apply_authorization!(resolution, scoped_struct, value, [first_key | remaining_keys]) when length(remaining_keys) > 0 do
nested_value = Map.get(value, first_key)
apply_authorization!(resolution, scoped_struct, nested_value, remaining_keys)
end
defp list_result([], _context, new_values), do: new_values

defp apply_authorization!(resolution, scoped_struct, value, [first_key]) do
scoped_field_value = Map.get(value, first_key)
Rajska.apply_auth_mod(resolution, :has_resolution_access?, [resolution, scoped_struct, scoped_field_value])
defp list_result([value | values], context, new_values) do
new_values = [result(value, context) | new_values]
list_result(values, context, new_values)
gabrielpra1 marked this conversation as resolved.
Show resolved Hide resolved
end

defp put_result(true, fields, resolution, _type, nested_keys), do: find_associations(fields, resolution, nested_keys)
defp is_authorized?(nil, _values, _context, object), do: raise "No meta scope defined for object #{inspect object.identifier}"

defp put_result(false, _fields, resolution, object, _nested_keys) do
Resolution.put_result(resolution, {:error, "Not authorized to access object #{object.identifier}"})
end
defp is_authorized?(false, _values, _context, _object), do: true

defp find_associations([%{selections: []} | tail], resolution, nested_keys) do
find_associations(tail, resolution, nested_keys)
defp is_authorized?({scoped_struct, field}, values, context, _object) do
scoped_field_value = Map.get(values, field)
Rajska.apply_auth_mod(context, :has_context_access?, [context, scoped_struct, scoped_field_value])
end

defp find_associations(
[%{schema_node: schema_node, selections: selections} | tail],
resolution,
nested_keys
) do
authorize(schema_node.type, selections ++ tail, resolution, nested_keys ++ [schema_node.identifier])
defp is_authorized?(scoped_struct, values, context, _object) do
scoped_field_value = Map.get(values, :id)
Rajska.apply_auth_mod(context, :has_context_access?, [context, scoped_struct, scoped_field_value])
end

defp find_associations([], resolution, _nested_keys), do: resolution
defp error(%{source_location: location, schema_node: %{type: type}}) do
%Phase.Error{
phase: __MODULE__,
message: "Not authorized to access object #{get_object_type(type).identifier}",
locations: [location]
}
end
end
16 changes: 8 additions & 8 deletions lib/middlewares/query_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,17 @@ defmodule Rajska.QueryAuthorization do

@behaviour Absinthe.Middleware

def call(resolution, [{:permit, permission} | _scoped] = config) do
validate_permission!(resolution, permission)
def call(%{context: context} = resolution, [{:permit, permission} | _scoped] = config) do
validate_permission!(context, permission)

resolution
|> Rajska.apply_auth_mod(:is_resolution_authorized?, [resolution, permission])
context
|> Rajska.apply_auth_mod(:is_context_authorized?, [context, permission])
|> update_result(resolution)
|> QueryScopeAuthorization.call(config)
end

defp validate_permission!(resolution, permitted_roles) do
valid_roles = Rajska.apply_auth_mod(resolution, :valid_roles)
defp validate_permission!(context, permitted_roles) do
valid_roles = Rajska.apply_auth_mod(context, :valid_roles)

unless permission_valid?(valid_roles, permitted_roles) do
raise """
Expand All @@ -70,7 +70,7 @@ defmodule Rajska.QueryAuthorization do

defp update_result(true, resolution), do: resolution

defp update_result(false, resolution) do
Resolution.put_result(resolution, {:error, Rajska.apply_auth_mod(resolution, :unauthorized_msg, [resolution])})
defp update_result(false, %{context: context} = resolution) do
Resolution.put_result(resolution, {:error, Rajska.apply_auth_mod(context, :unauthorized_msg, [context])})
end
end
8 changes: 4 additions & 4 deletions lib/middlewares/scope_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ defmodule Rajska.QueryScopeAuthorization do
def call(resolution, [_ | [scoped: false]]), do: resolution

def call(resolution, [{:permit, permission} | scoped_config]) do
not_scoped_roles = Rajska.apply_auth_mod(resolution, :not_scoped_roles)
not_scoped_roles = Rajska.apply_auth_mod(resolution.context, :not_scoped_roles)

case Enum.member?(not_scoped_roles, permission) do
true -> resolution
Expand Down Expand Up @@ -105,9 +105,9 @@ defmodule Rajska.QueryScopeAuthorization do
raise "Error in query #{name}: no argument found in middleware Scope Authorization"
end

def apply_scope_authorization(resolution, field_value, scoped_struct) do
resolution
|> Rajska.apply_auth_mod(:has_resolution_access?, [resolution, scoped_struct, field_value])
def apply_scope_authorization(%{context: context} = resolution, field_value, scoped_struct) do
context
|> Rajska.apply_auth_mod(:has_context_access?, [context, scoped_struct, field_value])
|> update_result(resolution)
end

Expand Down
28 changes: 13 additions & 15 deletions lib/rajska.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,6 @@ defmodule Rajska do
Since Scope Authorization middleware must be used with Query Authorization, it is automatically called when adding the former.
"""

alias Absinthe.Resolution

alias Rajska.Authorization

defmacro __using__(opts \\ []) do
Expand All @@ -73,7 +71,7 @@ defmodule Rajska do
Keyword.merge(unquote(opts), [all_role: unquote(all_role), roles: unquote(roles_with_tier)])
end

def get_current_user(%Resolution{context: %{current_user: current_user}}), do: current_user
def get_current_user(%{current_user: current_user}), do: current_user

def get_user_role(%{role: role}), do: role
def get_user_role(nil), do: nil
Expand Down Expand Up @@ -110,28 +108,28 @@ defmodule Rajska do

def unauthorized_msg(_resolution), do: "unauthorized"

def is_super_user?(%Resolution{} = resolution) do
resolution
def is_super_user?(context) do
context
|> get_current_user()
|> get_user_role()
|> is_super_role?()
end

def is_resolution_authorized?(%Resolution{} = resolution, allowed_role) do
resolution
def is_context_authorized?(context, allowed_role) do
context
|> get_current_user()
|> get_user_role()
|> is_role_authorized?(allowed_role)
end

def is_resolution_field_authorized?(%Resolution{} = resolution, scope_by, source) do
resolution
def is_context_field_authorized?(context, scope_by, source) do
context
|> get_current_user()
|> is_field_authorized?(scope_by, source)
end

def has_resolution_access?(%Resolution{} = resolution, scoped_struct, field_value) do
resolution
def has_context_access?(context, scoped_struct, field_value) do
context
|> get_current_user()
|> has_user_access?(scoped_struct, field_value)
end
Expand Down Expand Up @@ -170,18 +168,18 @@ defmodule Rajska do
end

@doc false
def apply_auth_mod(resolution, fnc_name, args \\ [])
def apply_auth_mod(context, fnc_name, args \\ [])

def apply_auth_mod(%Resolution{context: %{authorization: authorization}}, fnc_name, args) do
def apply_auth_mod(%{authorization: authorization}, fnc_name, args) do
apply(authorization, fnc_name, args)
end

def apply_auth_mod(_resolution, _fnc_name, _args) do
def apply_auth_mod(_context, _fnc_name, _args) do
raise "Rajska authorization module not found in Absinthe's context"
end

defdelegate add_query_authorization(middleware, field, authorization), to: Rajska.Schema
defdelegate add_object_authorization(middleware), to: Rajska.Schema
defdelegate add_field_authorization(middleware, field, object), to: Rajska.Schema
defdelegate add_object_scope_auhtorization(middleware), to: Rajska.Schema
defdelegate add_object_scope_authorization(middleware), to: Rajska.Schema
end
4 changes: 2 additions & 2 deletions lib/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ defmodule Rajska.Schema do
[{{FieldAuthorization, :call}, object: object, field: field} | middleware]
end

@spec add_object_scope_auhtorization([Middleware.spec(), ...]) :: [Middleware.spec(), ...]
def add_object_scope_auhtorization(middleware) do
@spec add_object_scope_authorization([Middleware.spec(), ...]) :: [Middleware.spec(), ...]
def add_object_scope_authorization(middleware) do
middleware ++ [ObjectScopeAuthorization]
end

Expand Down
Loading