Skip to content

Commit

Permalink
Merge 6b667a4 into c12bd4a
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielpra1 committed Oct 18, 2019
2 parents c12bd4a + 6b667a4 commit e99e740
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 55 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ All the following options are sent to [has_user_access?/4](https://hexdocs.pm/ra

* `:scope`
- `false`: disables scoping
- `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/4`. It must implement a `Rajska.Authorization` behaviour and a `__schema__(:source)` function (used to check if the module is valid in `Rajska.Schema.validate_query_auth_config!/2`)
- `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/4`. 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)
Expand Down
22 changes: 12 additions & 10 deletions lib/middlewares/object_scope_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -111,19 +111,25 @@ defmodule Rajska.ObjectScopeAuthorization do
end

# Object
defp result(%{fields: fields, emitter: %{schema_node: schema_node} = emitter} = result, context) do
defp result(%{fields: fields, emitter: %{schema_node: schema_node} = emitter, root_value: %scope{} = root_value} = result, context) do
type = Introspection.get_object_type(schema_node.type)
scope = Type.meta(type, :scope)
scope_by = Type.meta(type, :scope_by)

default_rule = Rajska.apply_auth_mod(context, :default_rule)
rule = Type.meta(type, :rule) || default_rule

case authorized?(scope, result.root_value, context, rule, type) do
case authorized?(scope, scope_by, root_value, context, rule, type) do
true -> %{result | fields: walk_result(fields, context)}
false -> Map.put(result, :errors, [error(emitter)])
end
end

# Invalid object
defp result(%{emitter: %{schema_node: schema_node}, root_value: root_value}, _context) do
type = Introspection.get_object_type(schema_node.type)
raise "Expected a Struct for object #{inspect(type.identifier)}, got #{inspect(root_value)}"
end

# List
defp result(%{values: values} = result, context) do
%{result | values: walk_result(values, context)}
Expand All @@ -141,20 +147,16 @@ defmodule Rajska.ObjectScopeAuthorization do
walk_result(fields, context, new_fields)
end

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

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

defp authorized?({scope, scope_field}, values, context, rule, _object) do
defp authorized?(scope, scope_field, values, context, rule, _object) do
field_value = Map.get(values, scope_field)

Rajska.apply_auth_mod(context, :has_context_access?, [context, scope, {scope_field, field_value}, rule])
end

defp authorized?(scope, values, context, rule, object) do
authorized?({scope, :id}, values, context, rule, object)
end

defp error(%{source_location: location, schema_node: %{type: type}}) do
%Phase.Error{
phase: __MODULE__,
Expand Down
4 changes: 1 addition & 3 deletions lib/middlewares/query_scope_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ defmodule Rajska.QueryScopeAuthorization do
* `:scope`
- `false`: disables scoping
- `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/4`. It must implement a `Rajska.Authorization` behaviour and a `__schema__(:source)` function (used to check if the module is valid in `Rajska.Schema.validate_query_auth_config!/2`)
- `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/4`. 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 `c:Rajska.Authorization.has_user_access?/4`
Expand Down Expand Up @@ -96,8 +96,6 @@ defmodule Rajska.QueryScopeAuthorization do
raise "Error in query #{name}: no scope argument found in middleware Scope Authorization"
end

defp get_arguments_source!(%Resolution{source: source}, :source), do: source

defp get_arguments_source!(%Resolution{arguments: args}, _scope), do: args

def apply_scope_authorization(
Expand Down
8 changes: 3 additions & 5 deletions lib/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ defmodule Rajska.Schema do
@spec validate_query_auth_config!(
[
permit: atom(),
scope: false | :source | module(),
scope: false | module(),
args: %{} | [] | atom(),
optional: false | true,
rule: atom()
Expand Down Expand Up @@ -97,12 +97,10 @@ defmodule Rajska.Schema do

defp validate_scope!(false, _role, _authorization), do: :ok

defp validate_scope!(:source, _role, _authorization), do: :ok

defp validate_scope!(scope, _role, _authorization) when is_atom(scope) do
scope.__schema__(:source)
struct!(scope)
rescue
UndefinedFunctionError -> reraise ":scope option #{inspect(scope)} doesn't implement a __schema__(:source) function.", __STACKTRACE__
UndefinedFunctionError -> reraise ":scope option #{inspect(scope)} is not a struct.", __STACKTRACE__
end

defp validate_args!(args) when is_map(args) do
Expand Down
115 changes: 98 additions & 17 deletions test/middlewares/object_scope_authorization_test.exs
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
defmodule Rajska.ObjectScopeAuthorizationTest do
use ExUnit.Case, async: true

defmodule Wallet do
defstruct [
id: 1,
total: 10,
]
end

defmodule Company do
defstruct [
id: 1,
name: "User",
user_id: 1,
wallet: %Wallet{}
]
end

defmodule User do
defstruct [
id: 1,
name: "User",
email: "email@user.com"
email: "email@user.com",
company: nil,
companies: [],
not_scoped: nil,
]
end

def __schema__(:source), do: "users"
defmodule NotScoped do
defstruct [
id: 1,
]
end

defmodule Authorization do
Expand All @@ -19,6 +42,7 @@ defmodule Rajska.ObjectScopeAuthorizationTest do
def has_user_access?(%{role: :admin}, User, _field, :default), do: true
def has_user_access?(%{id: user_id}, User, {:id, id}, :default) when user_id === id, do: true
def has_user_access?(_current_user, User, _field, :default), do: false
def has_user_access?(_crreutn_user, User, _field, :object), do: false

def has_user_access?(%{role: :admin}, Company, _field, :default), do: true
def has_user_access?(%{id: user_id}, Company, {:user_id, company_user_id}, :default) when user_id === company_user_id, do: true
Expand All @@ -41,14 +65,14 @@ defmodule Rajska.ObjectScopeAuthorizationTest do
arg :user_id, non_null(:integer)

resolve fn args, _ ->
{:ok, %{
{:ok, %User{
id: args.user_id,
name: "bob",
company: %{
company: %Company{
id: 5,
user_id: args.user_id,
name: "company",
wallet: %{id: 1, total: 10}
wallet: %Wallet{id: 1, total: 10}
}
}}
end
Expand All @@ -58,7 +82,7 @@ defmodule Rajska.ObjectScopeAuthorizationTest do
arg :user_id, non_null(:integer)

resolve fn args, _ ->
{:ok, %{
{:ok, %User{
id: args.user_id,
name: "bob"
}}
Expand All @@ -69,12 +93,12 @@ defmodule Rajska.ObjectScopeAuthorizationTest do
arg :user_id, non_null(:integer)

resolve fn args, _ ->
{:ok, %{
{:ok, %User{
id: args.user_id,
name: "bob",
companies: [
%{id: 1, user_id: args.user_id, wallet: %{id: 2, total: 10}},
%{id: 2, user_id: args.user_id, wallet: %{id: 1, total: 10}},
%Company{id: 1, user_id: args.user_id, wallet: %Wallet{id: 2, total: 10}},
%Company{id: 2, user_id: args.user_id, wallet: %Wallet{id: 1, total: 10}},
]
}}
end
Expand All @@ -83,15 +107,22 @@ defmodule Rajska.ObjectScopeAuthorizationTest do
field :object_not_scoped_query, :user do
arg :id, non_null(:integer)
resolve fn args, _ ->
{:ok, %{id: args.id, name: "bob", not_scoped: %{name: "name"}}}
{:ok, %User{id: args.id, name: "bob", not_scoped: %NotScoped{id: 1}}}
end
end

field :object_not_struct_query, :user do
arg :id, non_null(:integer)
resolve fn args, _ ->
{:ok, %{id: args.id, name: "bob"}}
end
end

field :users_query, list_of(:user) do
resolve fn _args, _ ->
{:ok, [
%{id: 1, name: "bob"},
%{id: 2, name: "bob"},
%User{id: 1, name: "bob"},
%User{id: 2, name: "bob"},
]}
end
end
Expand All @@ -101,10 +132,16 @@ defmodule Rajska.ObjectScopeAuthorizationTest do
{:ok, nil}
end
end

field :user_query_with_rule, :user_rule do
resolve fn _args, _ ->
{:ok, %User{id: 1}}
end
end
end

object :user do
meta :scope, User
meta :scope_by, :id

field :id, :integer
field :email, :string
Expand All @@ -116,7 +153,7 @@ defmodule Rajska.ObjectScopeAuthorizationTest do
end

object :company do
meta :scope, {Company, :user_id}
meta :scope_by, :user_id

field :id, :integer
field :user_id, :integer
Expand All @@ -125,14 +162,21 @@ defmodule Rajska.ObjectScopeAuthorizationTest do
end

object :wallet do
meta :scope, {Wallet, :user_id}
meta :scope_by, :user_id

field :total, :integer
end

object :not_scoped do
field :name, :string
end

object :user_rule do
meta :scope_by, :id
meta :rule, :object

field :id, :integer
end
end

test "Only user with same ID and admin has access to scoped user" do
Expand Down Expand Up @@ -257,12 +301,28 @@ defmodule Rajska.ObjectScopeAuthorizationTest do
refute Map.has_key?(result, :errors)
end

test "Raises when no meta scope is defined for an object" do
assert_raise RuntimeError, ~r/No meta scope defined for object :not_scoped/, fn ->
test "accepts a meta rule" do
assert {:ok, %{errors: errors}} = run_pipeline(user_query_with_rule(), context(:admin, 1))
assert [
%{
locations: [%{column: 0, line: 2}],
message: "Not authorized to access object user_rule",
}
] == errors
end

test "Raises when no meta scope_by is defined for an object" do
assert_raise RuntimeError, ~r/No meta scope_by defined for object :not_scoped/, fn ->
assert {:ok, _result} = run_pipeline(object_not_scoped_query(2), context(:user, 2))
end
end

test "Raises when returned object is not a struct" do
assert_raise RuntimeError, ~r/Expected a Struct for object :user, got %{id: 2, name: \"bob\"}/, fn ->
assert {:ok, _result} = run_pipeline(object_not_struct_query(2), context(:user, 2))
end
end

defp all_query(id) do
"""
{
Expand Down Expand Up @@ -377,6 +437,27 @@ defmodule Rajska.ObjectScopeAuthorizationTest do
"""
end

defp object_not_struct_query(id) do
"""
{
objectNotStructQuery(id: #{id}) {
name
email
}
}
"""
end

defp user_query_with_rule do
"""
{
userQueryWithRule {
id
}
}
"""
end

defp context(role, id), do: [context: %{current_user: %{role: role, id: id}}]

defp run_pipeline(document, opts) do
Expand Down
4 changes: 0 additions & 4 deletions test/middlewares/query_scope_authorization_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,13 @@ defmodule Rajska.QueryScopeAuthorizationTest do
name: "User",
email: "email@user.com"
]

def __schema__(:source), do: "users"
end

defmodule BankAccount do
defstruct [
id: 1,
total: 5,
]

def __schema__(:source), do: "bank_account"
end

defmodule Authorization do
Expand Down
Loading

0 comments on commit e99e740

Please sign in to comment.