diff --git a/config/.credo.exs b/config/.credo.exs new file mode 100644 index 0000000..58b6c55 --- /dev/null +++ b/config/.credo.exs @@ -0,0 +1,161 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any exec using `mix credo -C `. If no exec name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: ["lib/"], + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: [], + # + # If you want to use uncolored output by default, you can change `color` + # to `[]` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `[]` as second element: + # + # {Credo.Check.Design.DuplicatedCode, []} + # + checks: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + {Credo.Check.Design.TagFIXME, []}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 140]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + # TODO: enable by default in Credo 1.1 + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapInto, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + + # + # Controversial and experimental checks (opt-in, just replace `false` with `[]`) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, + {Credo.Check.Design.DuplicatedCode, false}, + {Credo.Check.Readability.MultiAlias, false}, + {Credo.Check.Readability.Specs, false}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Refactor.ABCSize, [max_size: 40]}, + {Credo.Check.Refactor.AppendSingleItem, false}, + {Credo.Check.Refactor.DoubleBooleanNegation, false}, + {Credo.Check.Refactor.ModuleDependencies, false}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.UnsafeToAtom, false} + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + ] +} diff --git a/lib/middlewares/field_authorization.ex b/lib/middlewares/field_authorization.ex index 724dc83..0cd8441 100644 --- a/lib/middlewares/field_authorization.ex +++ b/lib/middlewares/field_authorization.ex @@ -34,9 +34,12 @@ defmodule Rajska.FieldAuthorization do field_private? = fields[field] |> Type.meta(:private) |> field_private?(resolution.source) scope_by = get_scope_by_field!(object, field_private?) + 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.source) + |> authorized?(field_private?, scope_by, resolution, rule) |> put_result(resolution, field) end @@ -47,19 +50,27 @@ defmodule Rajska.FieldAuthorization do defp get_scope_by_field!(_object, false), do: :ok defp get_scope_by_field!(object, _private) do - case Type.meta(object, :scope_by) do - nil -> raise "No scope_by meta defined for object returned from query #{object.identifier}" - scope_by_field when is_atom(scope_by_field) -> scope_by_field + general_scope_by = Type.meta(object, :scope_by) + field_scope_by = Type.meta(object, :scope_field_by) + + 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" end end - defp authorized?(_context, false, _scope_by, _source), do: true + defp authorized?(_context, false, _scope_by, _source, _rule), do: true - defp authorized?(context, true, scope_by, source) do - case Rajska.apply_auth_mod(context, :super_user?, [context]) do - true -> true - false -> Rajska.apply_auth_mod(context, :context_field_authorized?, [context, scope_by, source]) - end + 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, true, _scope_by, %{source: source, definition: definition}, _rule) do + raise "Expected a Struct for source object in field #{inspect(definition.name)}, got #{inspect(source)}" end defp put_result(true, resolution, _field), do: resolution diff --git a/lib/middlewares/object_scope_authorization.ex b/lib/middlewares/object_scope_authorization.ex index 16e3160..c919117 100644 --- a/lib/middlewares/object_scope_authorization.ex +++ b/lib/middlewares/object_scope_authorization.ex @@ -114,7 +114,7 @@ defmodule Rajska.ObjectScopeAuthorization do # Object 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_by = Type.meta(type, :scope_by) + scope_by = get_scope_by!(type) default_rule = Rajska.apply_auth_mod(context, :default_rule) rule = Type.meta(type, :rule) || default_rule @@ -148,7 +148,17 @@ defmodule Rajska.ObjectScopeAuthorization do walk_result(fields, context, new_fields) end - defp authorized?(_scope, nil, _values, _context, _, object), do: raise "No meta scope_by defined for object #{inspect object.identifier}" + defp get_scope_by!(object) do + general_scope_by = Type.meta(object, :scope_by) + object_scope_by = Type.meta(object, :scope_object_by) + + 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" + end + end defp authorized?(_scope, false, _values, _context, _, _object), do: true diff --git a/lib/rajska.ex b/lib/rajska.ex index 705bda2..c1ee93f 100644 --- a/lib/rajska.ex +++ b/lib/rajska.ex @@ -100,9 +100,6 @@ defmodule Rajska do def role_authorized?(user_role, allowed_role) when is_atom(allowed_role), do: user_role === allowed_role def role_authorized?(user_role, allowed_roles) when is_list(allowed_roles), do: user_role in allowed_roles - def field_authorized?(nil, _scope_by, _source), do: false - def field_authorized?(%{id: user_id}, scope_by, source), do: user_id === Map.get(source, scope_by) - def has_user_access?(%user_struct{id: user_id} = current_user, scope, {field, field_value}, unquote(default_rule)) do super_user? = current_user |> get_user_role() |> super_role?() owner? = @@ -129,12 +126,6 @@ defmodule Rajska do |> role_authorized?(allowed_role) end - def context_field_authorized?(context, scope_by, source) do - context - |> get_current_user() - |> field_authorized?(scope_by, source) - end - def has_context_access?(context, scope, {scope_field, field_value}, rule) do context |> get_current_user() diff --git a/mix.exs b/mix.exs index a11375f..423bbdf 100644 --- a/mix.exs +++ b/mix.exs @@ -46,7 +46,7 @@ defmodule Rajska.MixProject do defp deps do [ {:ex_doc, "~> 0.19", only: :dev, runtime: false}, - {:credo, "~> 1.0.0", only: [:dev, :test], runtime: false}, + {:credo, "~> 1.1.0", only: [:dev, :test], runtime: false}, {:absinthe, "~> 1.4.0"}, {:excoveralls, "~> 0.11", only: :test}, ] diff --git a/mix.lock b/mix.lock index cf7a0b6..4e57854 100644 --- a/mix.lock +++ b/mix.lock @@ -2,19 +2,19 @@ "absinthe": {:hex, :absinthe, "1.4.16", "0933e4d9f12652b12115d5709c0293a1bf78a22578032e9ad0dad4efee6b9eb1", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, - "credo": {:hex, :credo, "1.0.5", "fdea745579f8845315fe6a3b43e2f9f8866839cfbc8562bb72778e9fdaa94214", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, - "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.11.1", "dd677fbdd49114fdbdbf445540ec735808250d56b011077798316505064edb2c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, - "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, + "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, + "excoveralls": {:hex, :excoveralls, "0.12.0", "50e17a1b116fdb7facc2fe127a94db246169f38d7627b391376a0bc418413ce1", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, + "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, } diff --git a/test/middlewares/field_authorization_test.exs b/test/middlewares/field_authorization_test.exs index b903eb0..0e958f5 100644 --- a/test/middlewares/field_authorization_test.exs +++ b/test/middlewares/field_authorization_test.exs @@ -1,10 +1,26 @@ defmodule Rajska.FieldAuthorizationTest do use ExUnit.Case, async: true + defmodule User do + defstruct [ + id: 1, + name: "User", + email: "email@user.com", + phone: "123456", + is_email_public: true, + always_private: "private!" + ] + end + defmodule Authorization do use Rajska, valid_roles: [:user, :admin], super_role: :admin + + def has_user_access?(_current_user, User, _field, :private), do: false + 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 end defmodule Schema do @@ -25,14 +41,34 @@ defmodule Rajska.FieldAuthorizationTest do arg :is_email_public, non_null(:boolean) resolve fn args, _ -> - {:ok, %{ + {:ok, %User{ id: args.id, name: "bob", is_email_public: args.is_email_public, phone: "123456", - email: "bob@email.com" + email: "bob@email.com", + always_private: "private!", + }} end + end + + field :get_field_scope_user, :field_scope_user do + arg :id, non_null(:integer) + + resolve fn args, _ -> + {:ok, %User{ + id: args.id, + name: "bob", + phone: "123456", }} end end + + field :get_not_scoped, :not_scoped do + resolve fn _args, _ -> {:ok, %{phone: "123456"}} end + end + + field :get_both_scopes, :both_scopes do + resolve fn _args, _ -> {:ok, %{phone: "123456"}} end + end end object :user do @@ -43,14 +79,32 @@ defmodule Rajska.FieldAuthorizationTest do field :phone, :string, meta: [private: true] field :email, :string, meta: [private: & !&1.is_email_public] + field :always_private, :string, meta: [private: true, rule: :private] + end + + object :field_scope_user do + meta :scope_field_by, :id + + field :name, :string + field :phone, :string, meta: [private: true] + end + + object :not_scoped do + field :phone, :string, meta: [private: true] + end + + object :both_scopes do + meta :scope_by, :id + meta :scope_field_by, :id + + field :phone, :string, meta: [private: true] end end test "User can access own fields" do - user = %{role: :user, id: 1} get_user_query = get_user_query(1, false) - {:ok, result} = Absinthe.run(get_user_query, __MODULE__.Schema, context: %{current_user: user}) + {:ok, result} = Absinthe.run(get_user_query, __MODULE__.Schema, context(:user, 1)) assert %{data: %{"getUser" => data}} = result refute Map.has_key?(result, :errors) @@ -60,14 +114,25 @@ defmodule Rajska.FieldAuthorizationTest do assert is_binary(data["phone"]) end + test "Custom rules are applied" do + {:ok, %{ + errors: errors, + data: %{"getUser" => data} + }} = Absinthe.run(get_user_private_query(1), __MODULE__.Schema, context(:user, 1)) + + error_messages = Enum.map(errors, & &1.message) + assert Enum.member?(error_messages, "Not authorized to access field always_private") + + assert is_nil(data["alwaysPrivate"]) + end + test "User cannot access other user private fields" do - user = %{role: :user, id: 1} get_user_query = get_user_query(2, false) {:ok, %{ errors: errors, data: %{"getUser" => data} - }} = Absinthe.run(get_user_query, __MODULE__.Schema, context: %{current_user: user}) + }} = Absinthe.run(get_user_query, __MODULE__.Schema, context(:user, 1)) error_messages = Enum.map(errors, & &1.message) assert Enum.member?(error_messages, "Not authorized to access field phone") @@ -79,10 +144,8 @@ defmodule Rajska.FieldAuthorizationTest do end test "Admin can access all fields" do - user = %{role: :admin, id: 3} get_user_query = get_user_query(2, false) - - {:ok, result} = Absinthe.run(get_user_query, __MODULE__.Schema, context: %{current_user: user}) + {:ok, result} = Absinthe.run(get_user_query, __MODULE__.Schema, context(:admin, 3)) assert %{data: %{"getUser" => data}} = result refute Map.has_key?(result, :errors) @@ -92,6 +155,34 @@ defmodule Rajska.FieldAuthorizationTest do assert is_binary(data["phone"]) end + test "Works when defining scope_field_by" do + user = %{role: :user, id: 1} + get_user_query = get_field_scope_user(2) + + {:ok, %{ + errors: errors, + data: %{"getFieldScopeUser" => data} + }} = Absinthe.run(get_user_query, __MODULE__.Schema, context: %{current_user: user}) + + error_messages = Enum.map(errors, & &1.message) + assert Enum.member?(error_messages, "Not authorized to access field phone") + + assert is_binary(data["name"]) + assert data["phone"] === nil + end + + test "Raises when no meta scope_by or scope_field_by is defined for an object" do + assert_raise RuntimeError, ~r/No meta scope_by or scope_field_by defined for object :not_scoped/, fn -> + Absinthe.run("{ getNotScoped { phone } }", __MODULE__.Schema, context(:user, 2)) + end + end + + test "Raises when both scope metas are defined for an object" do + assert_raise RuntimeError, ~r/Error in :both_scopes. If scope_field_by is defined, then scope_by must not be defined/, fn -> + Absinthe.run("{ getBothScopes { phone } }", __MODULE__.Schema, context(:user, 2)) + end + end + defp get_user_query(id, is_email_public) do """ { @@ -104,4 +195,27 @@ defmodule Rajska.FieldAuthorizationTest do } """ end + + defp get_field_scope_user(id) do + """ + { + getFieldScopeUser(id: #{id}) { + name + phone + } + } + """ + end + + defp get_user_private_query(id) do + """ + { + getUser(id: #{id}, isEmailPublic: true) { + alwaysPrivate + } + } + """ + end + + defp context(role, id), do: [context: %{current_user: %{role: role, id: id}}] end diff --git a/test/middlewares/object_scope_authorization_test.exs b/test/middlewares/object_scope_authorization_test.exs index 3ae9aca..7b62109 100644 --- a/test/middlewares/object_scope_authorization_test.exs +++ b/test/middlewares/object_scope_authorization_test.exs @@ -144,6 +144,15 @@ defmodule Rajska.ObjectScopeAuthorizationTest do {:ok, "STRING"} end end + + field :get_both_scopes, :both_scopes do + resolve fn _args, _ -> {:ok, %User{}} end + end + + field :get_object_scope_user, :object_scope_user do + arg :id, non_null(:integer) + resolve fn args, _ -> {:ok, %User{id: args.id}} end + end end object :user do @@ -183,6 +192,19 @@ defmodule Rajska.ObjectScopeAuthorizationTest do field :id, :integer end + + object :both_scopes do + meta :scope_by, :id + meta :scope_object_by, :id + + field :name, :string + end + + object :object_scope_user do + meta :scope_object_by, :id + + field :id, :integer + end end test "Only user with same ID and admin has access to scoped user" do @@ -221,6 +243,25 @@ defmodule Rajska.ObjectScopeAuthorizationTest do ] == errors end + test "Works when defining scope_object_by instead of scope_by" do + query = "{ getObjectScopeUser(id: 1) { id } }" + {:ok, result} = run_pipeline(query, context(:user, 1)) + assert %{data: %{"getObjectScopeUser" => %{}}} = result + refute Map.has_key?(result, :errors) + + {:ok, result} = run_pipeline(query, context(:admin, 2)) + assert %{data: %{"getObjectScopeUser" => %{}}} = result + refute Map.has_key?(result, :errors) + + assert {:ok, %{errors: errors}} = run_pipeline(query, context(:user, 2)) + assert [ + %{ + locations: [%{column: 0, line: 1}], + message: "Not authorized to access object object_scope_user", + } + ] == errors + end + test "Works for deeply nested objects" do assert {:ok, %{errors: errors}} = run_pipeline(all_query_company_wallet(2), context(:user, 2)) assert [ @@ -324,14 +365,20 @@ defmodule Rajska.ObjectScopeAuthorizationTest do 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)) + assert_raise RuntimeError, ~r/No meta scope_by or scope_object_by defined for object :not_scoped/, fn -> + run_pipeline(object_not_scoped_query(2), context(:user, 2)) + end + end + + test "Raises when both scope metas are defined for an object" do + assert_raise RuntimeError, ~r/Error in :both_scopes. If scope_object_by is defined, then scope_by must not be defined/, fn -> + run_pipeline("{ getBothScopes { name } }", 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)) + run_pipeline(object_not_struct_query(2), context(:user, 2)) end end