From c75eeec34f08b70bd65ecd55deb2f5b5665abb66 Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Tue, 8 Oct 2019 19:10:16 -0300 Subject: [PATCH 1/9] Add args option to query scope authorization --- README.md | 24 +++---- lib/authorization.ex | 10 ++- lib/middlewares/object_scope_authorization.ex | 28 ++++---- lib/middlewares/query_scope_authorization.ex | 68 ++++++++----------- lib/rajska.ex | 13 ++-- .../object_scope_authorization_test.exs | 18 ++--- .../query_scope_authorization_test.exs | 10 +-- 7 files changed, 86 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 6d822cd..d51a03a 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ 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), [has_user_access?/5](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/5) 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 @@ -128,8 +128,8 @@ In the above example, `:all` and `:admin` (`super_role`) permissions don't requi Valid values for the `:scoped` keyword are: - `false`: disables scoping -- `User`: a module that will be passed to [has_user_access?/4](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/4). It must implement a [Authorization behaviour](https://hexdocs.pm/rajska/Rajska.Authorization.html) and a `__schema__(:source)` function (used to check if the module is valid in [validate_query_auth_config!/2](https://hexdocs.pm/rajska/Rajska.Schema.html#validate_query_auth_config!/2)) -- `{User, :id}`: where `:id` is the query argument that will also be passed to [has_user_access?/4](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/4) +- `User`: a module that will be passed to [has_user_access?/5](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/5). It must implement a [Authorization behaviour](https://hexdocs.pm/rajska/Rajska.Authorization.html) and a `__schema__(:source)` function (used to check if the module is valid in [validate_query_auth_config!/2](https://hexdocs.pm/rajska/Rajska.Schema.html#validate_query_auth_config!/2)) +- `{User, :id}`: where `:id` is the query argument that will also be passed to [has_user_access?/5](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/5) - `{User, [:params, :id]}`: where `id` is the query argument as above, but it's not defined directly as an `arg` for the query. Instead, it's nested inside the `params` argument. - `{User, :user_group_id, :optional}`: where `user_group_id` (it could also be a nested argument) is an optional argument for the query. If it's present, the scoping will be applied, otherwise no scoping is applied. @@ -219,7 +219,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?/5](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/5). For example: ```elixir defmodule Authorization do @@ -228,13 +228,13 @@ defmodule Authorization do super_role: :admin @impl true - def has_user_access?(%{role: :admin}, User, _id, _rule), do: true - def has_user_access?(%{id: user_id}, User, id, _rule) when user_id === id, do: true - def has_user_access?(_current_user, User, _id, _rule), do: false + def has_user_access?(%{role: :admin}, User, _id, _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, _id, _field, _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?/5` can be `nil`. This case can be handled as you wish. For example, to not raise any authorization errors and just return `nil`: ```elixir @@ -244,11 +244,11 @@ defmodule Authorization do super_role: :admin @impl true - def has_user_access?(_user, _, nil), do: true + def has_user_access?(_user, _, _, nil), do: true - def has_user_access?(%{role: :admin}, User, _id, _rule), do: true - def has_user_access?(%{id: user_id}, User, id, _rule) when user_id === id, do: true - def has_user_access?(_current_user, User, _id, _rule), do: false + def has_user_access?(%{role: :admin}, User, _id, _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, _id, _field, _rule), do: false end ``` diff --git a/lib/authorization.ex b/lib/authorization.ex index e1b5db6..a06f5c7 100644 --- a/lib/authorization.ex +++ b/lib/authorization.ex @@ -19,7 +19,13 @@ defmodule Rajska.Authorization do @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() + @callback has_user_access?( + current_user, + scoped_struct :: module(), + field_value :: any(), + field :: any(), + rule :: any() + ) :: boolean() @callback unauthorized_msg(resolution :: Resolution.t()) :: String.t() @@ -28,6 +34,6 @@ defmodule Rajska.Authorization do not_scoped_roles: 0, role_authorized?: 2, field_authorized?: 3, - has_user_access?: 4, + has_user_access?: 5, unauthorized_msg: 1 end diff --git a/lib/middlewares/object_scope_authorization.ex b/lib/middlewares/object_scope_authorization.ex index 30f8466..fba9a97 100644 --- a/lib/middlewares/object_scope_authorization.ex +++ b/lib/middlewares/object_scope_authorization.ex @@ -38,7 +38,7 @@ defmodule Rajska.ObjectScopeAuthorization do 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?/5`. For example: ```elixir defmodule Authorization do @@ -46,13 +46,13 @@ defmodule Rajska.ObjectScopeAuthorization do valid_roles: [:user, :admin] @impl true - def has_user_access?(%{role: :admin}, _struct, _field_value, _rule), do: true - def has_user_access?(%{id: user_id}, User, id, _rule) when user_id === id, do: true - def has_user_access?(_current_user, User, _field_value, _rule), do: false + def has_user_access?(%{role: :admin}, _struct, _field_value, field, _rule), do: true + def has_user_access?(%{id: user_id}, User, id, field, _rule) when user_id === id, do: true + def has_user_access?(_current_user, User, _field_value, field, _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?/5` can be `nil`. This case can be handled as you wish. For example, to not raise any authorization errors and just return `nil`: ```elixir @@ -61,15 +61,15 @@ defmodule Rajska.ObjectScopeAuthorization do valid_roles: [:user, :admin] @impl true - def has_user_access?(_user, _, nil, _), do: true + def has_user_access?(_user, _, nil, _, _), do: true - def has_user_access?(%{role: :admin}, User, _field_value, _rule), do: true - def has_user_access?(%{id: user_id}, User, id, _rule) when user_id === id, do: true - def has_user_access?(_current_user, User, _field_value, _rule), do: false + def has_user_access?(%{role: :admin}, User, _field_value, _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_value, _field, _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?/5`: ```elixir defmodule Authorization do @@ -77,8 +77,8 @@ defmodule Rajska.ObjectScopeAuthorization do valid_roles: [:user, :admin] @impl true - def has_user_access?(%{id: user_id}, Wallet, _field_value, :read_only), do: true - def has_user_access?(%{id: user_id}, Wallet, _field_value, :default), do: false + def has_user_access?(%{id: user_id}, Wallet, _field_value, _field, :read_only), do: true + def has_user_access?(%{id: user_id}, Wallet, _field_value, _field, :default), do: false end ``` @@ -147,12 +147,12 @@ defmodule Rajska.ObjectScopeAuthorization 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]) + Rajska.apply_auth_mod(context, :has_context_access?, [context, scoped_struct, scoped_field_value, field, rule]) end 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]) + Rajska.apply_auth_mod(context, :has_context_access?, [context, scoped_struct, scoped_field_value, :id, rule]) end defp error(%{source_location: location, schema_node: %{type: type}}) do diff --git a/lib/middlewares/query_scope_authorization.ex b/lib/middlewares/query_scope_authorization.ex index 094a2bc..2e83c35 100644 --- a/lib/middlewares/query_scope_authorization.ex +++ b/lib/middlewares/query_scope_authorization.ex @@ -40,13 +40,13 @@ defmodule Rajska.QueryScopeAuthorization do ``` In the above example, `:all` and `:admin` permissions don't require the `:scoped` keyword, as defined in the `c:Rajska.Authorization.not_scoped_roles/0` function, but you can modify this behavior by overriding it. - The `rule` keyword is not mandatory and will be pattern matched in `c:Rajska.Authorization.has_user_access?/4`. This way different rules can be set to the same struct. + The `rule` keyword is not mandatory and will be pattern matched in `c:Rajska.Authorization.has_user_access?/5`. This way different rules can be set to the same struct. See `Rajska.Authorization` for `rule` default settings. Valid values for the `:scoped` keyword are: - `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, :id}`: where `:id` is the query argument that will also be passed to `c:Rajska.Authorization.has_user_access?/4` + - `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/5`. 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, :id}`: where `:id` is the query argument that will also be passed to `c:Rajska.Authorization.has_user_access?/5` - `{User, [:params, :id]}`: where `id` is the query argument as above, but it's not defined directly as an `arg` for the query. Instead, it's nested inside the `params` argument. - `{User, :user_group_id, :optional}`: where `user_group_id` (it could also be a nested argument) is an optional argument for the query. If it's present, the scoping will be applied, otherwise no scoping is applied. """ @@ -61,59 +61,51 @@ defmodule Rajska.QueryScopeAuthorization do def call(resolution, [_ | [scoped: false]]), do: resolution - def call(resolution, [{:permit, permission} | scoped_config]) do + def call(resolution, [{:permit, permission} | scope_config]) do not_scoped_roles = Rajska.apply_auth_mod(resolution.context, :not_scoped_roles) case Enum.member?(not_scoped_roles, permission) do true -> resolution - false -> - default_rule = Rajska.apply_auth_mod(resolution.context, :default_rule) - scope_user!( - resolution, - Keyword.get(scoped_config, :rule, default_rule), - Keyword.delete(scoped_config, :rule) - ) + false -> scope_user!(resolution, scope_config) end end - def scope_user!(%Resolution{source: source} = resolution, rule, scoped: :source) do - apply_scope_authorization(resolution, get_scoped_field_value(source, :id), source.__struct__, rule) - end + def scope_user!(%{context: context} = resolution, config) do + default_rule = Rajska.apply_auth_mod(resolution.context, :default_rule) + rule = Keyword.get(config, :rule, default_rule) + scope = Keyword.get(config, :scope) + arg_fields = config |> Keyword.get(:args, :id) |> arg_fields_to_map() + optional = Keyword.get(config, :optional, false) + arguments_source = get_arguments_source!(resolution, scope) - def scope_user!(%Resolution{source: source} = resolution, rule, scoped: {:source, scoped_field}) do - apply_scope_authorization(resolution, get_scoped_field_value(source, scoped_field), source.__struct__, rule) + arg_fields + |> Enum.all?(& apply_scope_authorization(context, scope, arguments_source, &1, rule, optional)) + |> update_result(resolution) end - def scope_user!(%Resolution{arguments: args} = resolution, rule, scoped: {scoped_struct, scoped_field}) do - apply_scope_authorization(resolution, get_scoped_field_value(args, scoped_field), scoped_struct, rule) - end + defp arg_fields_to_map(field) when is_atom(field), do: Map.new([{field, field}]) + defp arg_fields_to_map(fields) when is_list(fields), do: fields |> Enum.map(& {&1, &1}) |> Map.new() + defp arg_fields_to_map(field) when is_map(field), do: field - def scope_user!(%Resolution{arguments: args} = resolution, rule, scoped: {scoped_struct, scoped_field, :optional}) do - case get_scoped_field_value(args, scoped_field) do - nil -> update_result(true, resolution) - field_value -> apply_scope_authorization(resolution, field_value, scoped_struct, rule) - end + defp get_arguments_source!(%Resolution{definition: %{name: name}}, nil) do + raise "Error in query #{name}: no scope argument found in middleware Scope Authorization" end - def scope_user!(%Resolution{arguments: args} = resolution, rule, scoped: scoped_struct) do - apply_scope_authorization(resolution, get_scoped_field_value(args, :id), scoped_struct, rule) - end + defp get_arguments_source!(%Resolution{source: source}, :source), do: source - def scope_user!(%Resolution{definition: %{name: name}}, _, _scoped_config) do - raise "Error in query #{name}: no scoped argument found in middleware Scope Authorization" - end + defp get_arguments_source!(%Resolution{arguments: args}, _scope), do: args - defp get_scoped_field_value(args, fields) when is_list(fields), do: get_in(args, fields) - defp get_scoped_field_value(args, field) when is_atom(field), do: Map.get(args, field) + def apply_scope_authorization(context, scope, arguments_source, {scope_field, arg_field}, rule, optional) do + field_value = get_scoped_field_value(arguments_source, arg_field) - def apply_scope_authorization(%Resolution{definition: %{name: name}}, nil, _scoped_struct, _) do - raise "Error in query #{name}: no argument found in middleware Scope Authorization" + (optional && field_value === nil) || has_context_access?(context, scope, field_value, scope_field, rule) end - def apply_scope_authorization(%{context: context} = resolution, field_value, scoped_struct, rule) do - context - |> Rajska.apply_auth_mod(:has_context_access?, [context, scoped_struct, field_value, rule]) - |> update_result(resolution) + defp get_scoped_field_value(arguments_source, fields) when is_list(fields), do: get_in(arguments_source, fields) + defp get_scoped_field_value(arguments_source, field) when is_atom(field), do: Map.get(arguments_source, field) + + defp has_context_access?(context, scope, field_value, scope_field, rule) do + Rajska.apply_auth_mod(context, :has_context_access?, [context, scope, field_value, scope_field, rule]) end defp update_result(true, resolution), do: resolution diff --git a/lib/rajska.ex b/lib/rajska.ex index 923ade6..1e7d0af 100644 --- a/lib/rajska.ex +++ b/lib/rajska.ex @@ -23,7 +23,7 @@ defmodule Rajska do ## Usage - Create your Authorization module, which will implement the `Rajska.Authorization` 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 `c:Rajska.Authorization.role_authorized?/2`, `c:Rajska.Authorization.has_user_access?/4` and `c:Rajska.Authorization.field_authorized?/3`, but you can override them with your application needs. + Create your Authorization module, which will implement the `Rajska.Authorization` 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 `c:Rajska.Authorization.role_authorized?/2`, `c:Rajska.Authorization.has_user_access?/5` and `c:Rajska.Authorization.field_authorized?/3`, but you can override them with your application needs. ```elixir defmodule Authorization do @@ -103,9 +103,12 @@ defmodule Rajska do 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, scoped_struct, field_value, unquote(default_rule)) do + def has_user_access?(%user_struct{id: user_id} = current_user, scoped_struct, field_value, field, unquote(default_rule)) do super_user? = current_user |> get_user_role() |> super_role?() - owner? = (user_struct === scoped_struct) && (user_id === field_value) + owner? = + (user_struct === scoped_struct) + && (field === :id) + && (user_id === field_value) super_user? || owner? end @@ -132,10 +135,10 @@ defmodule Rajska do |> field_authorized?(scope_by, source) end - def has_context_access?(context, scoped_struct, field_value, rule) do + def has_context_access?(context, scoped_struct, field_value, field, rule) do context |> get_current_user() - |> has_user_access?(scoped_struct, field_value, rule) + |> has_user_access?(scoped_struct, field_value, field, rule) end defoverridable Authorization diff --git a/test/middlewares/object_scope_authorization_test.exs b/test/middlewares/object_scope_authorization_test.exs index b4fef29..822450c 100644 --- a/test/middlewares/object_scope_authorization_test.exs +++ b/test/middlewares/object_scope_authorization_test.exs @@ -16,17 +16,17 @@ defmodule Rajska.ObjectScopeAuthorizationTest do valid_roles: [:user, :admin], super_role: :admin - def has_user_access?(%{role: :admin}, User, _id, :default), do: true - def has_user_access?(%{id: user_id}, User, id, :default) when user_id === id, do: true - def has_user_access?(_current_user, User, _id, :default), do: false + def has_user_access?(%{role: :admin}, User, _id, _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, _id, _field, :default), do: false - def has_user_access?(%{role: :admin}, Company, _id, :default), do: true - def has_user_access?(%{id: user_id}, Company, company_user_id, :default) when user_id === company_user_id, do: true - def has_user_access?(_current_user, Company, _id, :default), do: false + def has_user_access?(%{role: :admin}, Company, _id, _field, :default), do: true + def has_user_access?(%{id: user_id}, Company, company_user_id, :id, :default) when user_id === company_user_id, do: true + def has_user_access?(_current_user, Company, _id, _field, :default), do: false - def has_user_access?(%{role: :admin}, Wallet, _id, :default), do: true - def has_user_access?(%{id: user_id}, Wallet, id, :default) when user_id === id, do: true - def has_user_access?(_current_user, Wallet, _id, :default), do: false + def has_user_access?(%{role: :admin}, Wallet, _id, _field, :default), do: true + def has_user_access?(%{id: user_id}, Wallet, id, :id, :default) when user_id === id, do: true + def has_user_access?(_current_user, Wallet, _id, _field, :default), do: false end defmodule Schema do diff --git a/test/middlewares/query_scope_authorization_test.exs b/test/middlewares/query_scope_authorization_test.exs index 3b78f1d..8d9f48c 100644 --- a/test/middlewares/query_scope_authorization_test.exs +++ b/test/middlewares/query_scope_authorization_test.exs @@ -25,12 +25,12 @@ defmodule Rajska.QueryScopeAuthorizationTest do valid_roles: [:user, :admin], super_role: :admin - def has_user_access?(%{role: :admin}, User, _id, :default), do: true - def has_user_access?(%{id: user_id}, User, id, :default) when user_id === id, do: true - def has_user_access?(_current_user, User, _id, :default), do: false + def has_user_access?(%{role: :admin}, User, _id, _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, _id, _field, :default), do: false - def has_user_access?(_current_user, BankAccount, _id, :edit), do: false - def has_user_access?(_current_user, BankAccount, _id, :read_only), do: true + def has_user_access?(_current_user, BankAccount, _id, _field, :edit), do: false + def has_user_access?(_current_user, BankAccount, _id, _field, :read_only), do: true end defmodule Schema do From f3c6d9d89f5e7071286e0277c480c62ff3b041a4 Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Tue, 8 Oct 2019 20:09:41 -0300 Subject: [PATCH 2/9] Validate query-scope authorization middlewares in schema module --- README.md | 15 ++-- lib/authorization.ex | 2 +- lib/middlewares/object_scope_authorization.ex | 12 ++-- lib/middlewares/query_authorization.ex | 4 +- lib/middlewares/query_scope_authorization.ex | 25 ++++--- lib/rajska.ex | 8 +-- lib/schema.ex | 70 ++++++++++++------- .../middlewares/object_authorization_test.exs | 2 +- test/middlewares/query_authorization_test.exs | 4 +- .../query_scope_authorization_test.exs | 25 +++++-- test/middlewares/schema_test.exs | 10 +-- 11 files changed, 106 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index d51a03a..0815342 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Usage: arg :id, non_null(:integer) arg :params, non_null(:user_params) - middleware Rajska.QueryAuthorization, [permit: :user, scoped: User] # same as {User, :id} + middleware Rajska.QueryAuthorization, [permit: :user, scope: User] # same as {User, :id} resolve &AccountsResolver.update_user/2 end @@ -123,15 +123,18 @@ Query authorization will call [role_authorized?/2](https://hexdocs.pm/rajska/Raj Provides scoping to Absinthe's queries, as seen above in [Query Authorization](#query-authorization). -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. +In the above example, `:all` and `:admin` (`super_role`) permissions don't require the `:scope` 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: +Valid values for the `:scope` keyword are: - `false`: disables scoping - `User`: a module that will be passed to [has_user_access?/5](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/5). It must implement a [Authorization behaviour](https://hexdocs.pm/rajska/Rajska.Authorization.html) and a `__schema__(:source)` function (used to check if the module is valid in [validate_query_auth_config!/2](https://hexdocs.pm/rajska/Rajska.Schema.html#validate_query_auth_config!/2)) -- `{User, :id}`: where `:id` is the query argument that will also be passed to [has_user_access?/5](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/5) -- `{User, [:params, :id]}`: where `id` is the query argument as above, but it's not defined directly as an `arg` for the query. Instead, it's nested inside the `params` argument. -- `{User, :user_group_id, :optional}`: where `user_group_id` (it could also be a nested argument) is an optional argument for the query. If it's present, the scoping will be applied, otherwise no scoping is applied. + +Valid values for the `:args` keyword are: + +- `:id`: where `:id` is the query argument that will also be passed to [has_user_access?/5](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/5) +- `%{id: [:params, :id]}`: where `id` is the query argument as above, but it's not defined directly as an `arg` for the query. Instead, it's nested inside the `params` argument. +- `[:code, :user_group_id]`: where `user_group_id` (it could also be a nested argument) is an optional argument for the query. If it's present, the scoping will be applied, otherwise no scoping is applied. ### Object Authorization diff --git a/lib/authorization.ex b/lib/authorization.ex index a06f5c7..1c4939d 100644 --- a/lib/authorization.ex +++ b/lib/authorization.ex @@ -21,7 +21,7 @@ defmodule Rajska.Authorization do @callback has_user_access?( current_user, - scoped_struct :: module(), + scope :: module(), field_value :: any(), field :: any(), rule :: any() diff --git a/lib/middlewares/object_scope_authorization.ex b/lib/middlewares/object_scope_authorization.ex index fba9a97..8b91a37 100644 --- a/lib/middlewares/object_scope_authorization.ex +++ b/lib/middlewares/object_scope_authorization.ex @@ -145,14 +145,14 @@ defmodule Rajska.ObjectScopeAuthorization do defp authorized?(false, _values, _context, _, _object), do: true - 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, field, rule]) + defp authorized?({scope, field}, values, context, rule, _object) do + scope_field_value = Map.get(values, field) + Rajska.apply_auth_mod(context, :has_context_access?, [context, scope, scope_field_value, field, rule]) end - 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, :id, rule]) + defp authorized?(scope, values, context, rule, _object) do + scope_field_value = Map.get(values, :id) + Rajska.apply_auth_mod(context, :has_context_access?, [context, scope, scope_field_value, :id, rule]) end defp error(%{source_location: location, schema_node: %{type: type}}) do diff --git a/lib/middlewares/query_authorization.ex b/lib/middlewares/query_authorization.ex index 0f18dac..d39cc3a 100644 --- a/lib/middlewares/query_authorization.ex +++ b/lib/middlewares/query_authorization.ex @@ -19,7 +19,7 @@ defmodule Rajska.QueryAuthorization do arg :id, non_null(:integer) arg :params, non_null(:user_params) - middleware Rajska.QueryAuthorization, [permit: :user, scoped: User] # same as {User, :id} + middleware Rajska.QueryAuthorization, [permit: :user, scope: User] # same as {User, :id} resolve &AccountsResolver.update_user/2 end @@ -40,7 +40,7 @@ defmodule Rajska.QueryAuthorization do @behaviour Absinthe.Middleware - def call(%{context: context} = resolution, [{:permit, permission} | _scoped] = config) do + def call(%{context: context} = resolution, [{:permit, permission} | _scope] = config) do validate_permission!(context, permission) context diff --git a/lib/middlewares/query_scope_authorization.ex b/lib/middlewares/query_scope_authorization.ex index 2e83c35..081b26a 100644 --- a/lib/middlewares/query_scope_authorization.ex +++ b/lib/middlewares/query_scope_authorization.ex @@ -19,7 +19,7 @@ defmodule Rajska.QueryScopeAuthorization do arg :id, non_null(:integer) arg :params, non_null(:user_params) - middleware Rajska.QueryAuthorization, [permit: :user, scoped: User] # same as {User, :id} + middleware Rajska.QueryAuthorization, [permit: :user, scope: User] # same as {User, :id} resolve &AccountsResolver.update_user/2 end @@ -33,22 +33,25 @@ defmodule Rajska.QueryScopeAuthorization do field :invite_user, :user do arg :email, non_null(:string) - middleware Rajska.QueryAuthorization, [permit: :user, scoped: User, rule: :invitation] + middleware Rajska.QueryAuthorization, [permit: :user, scope: User, rule: :invitation] resolve &AccountsResolver.invite_user/2 end end ``` - In the above example, `:all` and `:admin` permissions don't require the `:scoped` keyword, as defined in the `c:Rajska.Authorization.not_scoped_roles/0` function, but you can modify this behavior by overriding it. + In the above example, `:all` and `:admin` permissions don't require the `:scope` keyword, as defined in the `c:Rajska.Authorization.not_scoped_roles/0` function, but you can modify this behavior by overriding it. The `rule` keyword is not mandatory and will be pattern matched in `c:Rajska.Authorization.has_user_access?/5`. This way different rules can be set to the same struct. See `Rajska.Authorization` for `rule` default settings. - Valid values for the `:scoped` keyword are: + Valid values for the `:scope` keyword are: - `false`: disables scoping - `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/5`. 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, :id}`: where `:id` is the query argument that will also be passed to `c:Rajska.Authorization.has_user_access?/5` - - `{User, [:params, :id]}`: where `id` is the query argument as above, but it's not defined directly as an `arg` for the query. Instead, it's nested inside the `params` argument. - - `{User, :user_group_id, :optional}`: where `user_group_id` (it could also be a nested argument) is an optional argument for the query. If it's present, the scoping will be applied, otherwise no scoping is applied. + + Valid values for the `:args` keyword are: + + - `:id`: where `:id` is the query argument that will also be passed to `c:Rajska.Authorization.has_user_access?/5` + - `%{id: [:params, :id]}`: where `id` is the query argument as above, but it's not defined directly as an `arg` for the query. Instead, it's nested inside the `params` argument. + - `[:code, :user_group_id]`: where `user_group_id` (it could also be a nested argument) is an optional argument for the query. If it's present, the scoping will be applied, otherwise no scoping is applied. """ @behaviour Absinthe.Middleware @@ -59,7 +62,7 @@ defmodule Rajska.QueryScopeAuthorization do def call(%Resolution{state: :resolved} = resolution, _config), do: resolution - def call(resolution, [_ | [scoped: false]]), do: resolution + def call(resolution, [_ | [scope: false]]), do: resolution def call(resolution, [{:permit, permission} | scope_config]) do not_scoped_roles = Rajska.apply_auth_mod(resolution.context, :not_scoped_roles) @@ -96,13 +99,13 @@ defmodule Rajska.QueryScopeAuthorization do defp get_arguments_source!(%Resolution{arguments: args}, _scope), do: args def apply_scope_authorization(context, scope, arguments_source, {scope_field, arg_field}, rule, optional) do - field_value = get_scoped_field_value(arguments_source, arg_field) + field_value = get_scope_field_value(arguments_source, arg_field) (optional && field_value === nil) || has_context_access?(context, scope, field_value, scope_field, rule) end - defp get_scoped_field_value(arguments_source, fields) when is_list(fields), do: get_in(arguments_source, fields) - defp get_scoped_field_value(arguments_source, field) when is_atom(field), do: Map.get(arguments_source, field) + defp get_scope_field_value(arguments_source, fields) when is_list(fields), do: get_in(arguments_source, fields) + defp get_scope_field_value(arguments_source, field) when is_atom(field), do: Map.get(arguments_source, field) defp has_context_access?(context, scope, field_value, scope_field, rule) do Rajska.apply_auth_mod(context, :has_context_access?, [context, scope, field_value, scope_field, rule]) diff --git a/lib/rajska.ex b/lib/rajska.ex index 1e7d0af..d946db8 100644 --- a/lib/rajska.ex +++ b/lib/rajska.ex @@ -103,10 +103,10 @@ defmodule Rajska do 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, scoped_struct, field_value, field, unquote(default_rule)) do + def has_user_access?(%user_struct{id: user_id} = current_user, scope, field_value, field, unquote(default_rule)) do super_user? = current_user |> get_user_role() |> super_role?() owner? = - (user_struct === scoped_struct) + (user_struct === scope) && (field === :id) && (user_id === field_value) @@ -135,10 +135,10 @@ defmodule Rajska do |> field_authorized?(scope_by, source) end - def has_context_access?(context, scoped_struct, field_value, field, rule) do + def has_context_access?(context, scope, field_value, field, rule) do context |> get_current_user() - |> has_user_access?(scoped_struct, field_value, field, rule) + |> has_user_access?(scope, field_value, field, rule) end defoverridable Authorization diff --git a/lib/schema.ex b/lib/schema.ex index 490c1fd..704374f 100644 --- a/lib/schema.ex +++ b/lib/schema.ex @@ -22,10 +22,10 @@ defmodule Rajska.Schema do ) :: [Middleware.spec(), ...] def add_query_authorization( [{{QueryAuthorization, :call}, config} = query_authorization | middleware] = _middleware, - _field, + %Field{name: query_name}, authorization ) do - validate_query_auth_config!(config, authorization) + validate_query_auth_config!(config, authorization, query_name) [query_authorization | middleware] end @@ -53,43 +53,59 @@ defmodule Rajska.Schema do @spec validate_query_auth_config!( [ permit: atom(), - scoped: false | :source | {:source | module(), atom()} + scope: false | :source | module(), + args: %{} | [] | atom(), + optional: false | true, + rule: atom() ], - module() + module(), + String.t() ) :: :ok | Exception.t() - def validate_query_auth_config!([permit: _, scoped: _, rule: _] = config, authorization) do - config - |> Keyword.delete(:rule) - |> validate_query_auth_config!(authorization) + def validate_query_auth_config!(config, authorization, query_name) do + permit = Keyword.get(config, :permit) + scope = Keyword.get(config, :scope) + args = Keyword.get(config, :args, :id) + rule = Keyword.get(config, :rule, :default_rule) + optional = Keyword.get(config, :optional, false) + + try do + validate_presence!(permit, :permit) + validate_boolean!(optional, :optional) + validate_atom!(rule, :rule) + + validate_scope!(scope, permit, authorization) + validate_args!(args) + rescue + e in RuntimeError -> reraise "Query #{query_name} is configured incorrectly, #{e.message}", __STACKTRACE__ + end end - def validate_query_auth_config!([permit: _, scoped: false] = _config, _authorization), do: :ok + defp validate_presence!(nil, option), do: raise "#{option} option must be present." + defp validate_presence!(_value, _option), do: :ok - def validate_query_auth_config!([permit: _, scoped: :source], _authorization), do: :ok + defp validate_boolean!(value, _option) when is_boolean(value), do: :ok + defp validate_boolean!(_value, option), do: raise "#{option} option must be a boolean." - def validate_query_auth_config!([permit: _, scoped: {:source, _scoped_field}], _authorization), do: :ok + defp validate_atom!(value, _option) when is_atom(value), do: :ok + defp validate_atom!(_value, option), do: raise "#{option} option must be an atom." - def validate_query_auth_config!([permit: _, scoped: {scoped_struct, _scoped_field}], _authorization) do - scoped_struct.__schema__(:source) + defp validate_scope!(nil, role, authorization) do + unless Enum.member?(authorization.not_scoped_roles(), role), + do: raise ":scope option must be present for role #{role}." end - def validate_query_auth_config!([permit: _, scoped: {scoped_struct, _scoped_field, _opts}], _authorization) do - scoped_struct.__schema__(:source) - end + defp validate_scope!(false, _role, _authorization), do: :ok - def validate_query_auth_config!([permit: _, scoped: scoped_struct], _authorization) do - scoped_struct.__schema__(:source) - end + defp validate_scope!(:source, _role, _authorization), do: :ok - def validate_query_auth_config!([permit: role], authorization) do - case Enum.member?(authorization.not_scoped_roles(), role) do - true -> :ok - false -> raise "Query permitter is configured incorrectly, :scoped key must be present for role #{role}." - end + defp validate_scope!(scope, _role, _authorization) when is_atom(scope) do + unless scope.__schema__(:source), + do: raise ":scope option #{scope} doesn't implement a __schema__(:source) function." end - def validate_query_auth_config!(_config, _authorization) do - raise "Query permitter is configured incorrectly, :permit key must be present." - end + defp validate_args!(args) when is_map(args), do: :ok + defp validate_args!(args) when is_list(args), do: :ok + defp validate_args!(args) when is_atom(args), do: :ok + defp validate_args!(args), do: raise "the following args option is invalid: #{args}" end diff --git a/test/middlewares/object_authorization_test.exs b/test/middlewares/object_authorization_test.exs index 035783e..2d18fa7 100644 --- a/test/middlewares/object_authorization_test.exs +++ b/test/middlewares/object_authorization_test.exs @@ -34,7 +34,7 @@ defmodule Rajska.ObjectAuthorizationTest do end field :user_query, :user do - middleware Rajska.QueryAuthorization, [permit: :user, scoped: false] + middleware Rajska.QueryAuthorization, [permit: :user, scope: false] resolve fn _, _ -> {:ok, %{ name: "bob", diff --git a/test/middlewares/query_authorization_test.exs b/test/middlewares/query_authorization_test.exs index ae5ef75..496d6a6 100644 --- a/test/middlewares/query_authorization_test.exs +++ b/test/middlewares/query_authorization_test.exs @@ -26,12 +26,12 @@ defmodule Rajska.QueryAuthorizationTest do end field :user_query, :user do - middleware Rajska.QueryAuthorization, [permit: :user, scoped: false] + middleware Rajska.QueryAuthorization, [permit: :user, scope: false] resolve fn _, _ -> {:ok, %{name: "bob"}} end end field :user_viewer_query, :user do - middleware Rajska.QueryAuthorization, [permit: [:viewer, :user], scoped: false] + middleware Rajska.QueryAuthorization, [permit: [:viewer, :user], scope: false] resolve fn _, _ -> {:ok, %{name: "bob"}} end end diff --git a/test/middlewares/query_scope_authorization_test.exs b/test/middlewares/query_scope_authorization_test.exs index 8d9f48c..18b49e4 100644 --- a/test/middlewares/query_scope_authorization_test.exs +++ b/test/middlewares/query_scope_authorization_test.exs @@ -49,7 +49,7 @@ defmodule Rajska.QueryScopeAuthorizationTest do field :user_scoped_query, :user do arg :id, non_null(:integer) - middleware Rajska.QueryAuthorization, [permit: :user, scoped: User] + middleware Rajska.QueryAuthorization, [permit: :user, scope: User] resolve fn _, _ -> {:ok, %{ name: "bob", @@ -60,28 +60,41 @@ defmodule Rajska.QueryScopeAuthorizationTest do field :custom_arg_scoped_query, :user do arg :user_id, non_null(:integer) - middleware Rajska.QueryAuthorization, [permit: :user, scoped: {User, :user_id}] + middleware Rajska.QueryAuthorization, [ + permit: :user, + scope: User, + args: %{id: :user_id} + ] resolve fn _, _ -> {:ok, %{name: "bob"}} end end field :custom_nested_arg_scoped_query, :user do arg :params, non_null(:user_params) - middleware Rajska.QueryAuthorization, [permit: :user, scoped: {User, [:params, :id]}] + middleware Rajska.QueryAuthorization, [ + permit: :user, + scope: User, + args: %{id: [:params, :id]} + ] resolve fn _, _ -> {:ok, %{name: "bob"}} end end field :custom_nested_optional_arg_scoped_query, :user do arg :params, non_null(:user_params) - middleware Rajska.QueryAuthorization, [permit: :user, scoped: {User, [:params, :id], :optional}] + middleware Rajska.QueryAuthorization, [ + permit: :user, + scope: User, + args: %{id: [:params, :id]}, + optional: true + ] resolve fn _, _ -> {:ok, %{name: "bob"}} end end field :not_scoped_query, :user do arg :id, non_null(:integer) - middleware Rajska.QueryAuthorization, [permit: :user, scoped: false] + middleware Rajska.QueryAuthorization, [permit: :user, scope: false] resolve fn _, _ -> {:ok, %{name: "bob"}} end end @@ -89,7 +102,7 @@ defmodule Rajska.QueryScopeAuthorizationTest do arg :id, :integer arg :params, :bank_account_params - middleware Rajska.QueryAuthorization, [permit: :user, scoped: BankAccount, rule: :edit] + middleware Rajska.QueryAuthorization, [permit: :user, scope: BankAccount, rule: :edit] resolve fn _, _ -> {:ok, %{total: 100}} end end end diff --git a/test/middlewares/schema_test.exs b/test/middlewares/schema_test.exs index 4baf998..fc0b873 100644 --- a/test/middlewares/schema_test.exs +++ b/test/middlewares/schema_test.exs @@ -30,10 +30,10 @@ defmodule Rajska.SchemaTest do end end - test "Raises in compile time if no scoped key is specified for a scoped role" do + test "Raises in compile time if no scope key is specified for a scope role" do assert_raise( RuntimeError, - ~r/Query permitter is configured incorrectly, :scoped key must be present for role user/, + ~r/Query get_user is configured incorrectly, :scope option must be present for role user/, fn -> defmodule Schema do use Absinthe.Schema @@ -58,10 +58,10 @@ defmodule Rajska.SchemaTest do ) end - test "Raises in runtime if no scoped key is specified for a scoped role" do + test "Raises in runtime if no scope key is specified for a scope role" do assert_raise( RuntimeError, - ~r/Error in query getUser: no scoped argument found in middleware Scope Authorization/, + ~r/Error in query getUser: no scope argument found in middleware Scope Authorization/, fn -> defmodule Schema do use Absinthe.Schema @@ -82,7 +82,7 @@ defmodule Rajska.SchemaTest do end test "Raises if no permit key is specified for a query" do - assert_raise RuntimeError, ~r/Query permitter is configured incorrectly, :permit key must be present/, fn -> + assert_raise RuntimeError, ~r/Query get_user is configured incorrectly, permit option must be present/, fn -> defmodule Schema do use Absinthe.Schema From bafd7879d6720eecc36039b54d125347fb2a4a78 Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Tue, 8 Oct 2019 20:37:45 -0300 Subject: [PATCH 3/9] Pass scope field and field value as a tuple to has_user_access?/4 --- README.md | 22 ++++++------ lib/authorization.ex | 5 ++- lib/middlewares/object_scope_authorization.ex | 34 +++++++++---------- lib/middlewares/query_scope_authorization.ex | 30 ++++++++++------ lib/rajska.ex | 8 ++--- .../object_scope_authorization_test.exs | 18 +++++----- .../query_scope_authorization_test.exs | 12 +++---- 7 files changed, 68 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 0815342..c9467d7 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ 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?/5](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/5) 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), [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 @@ -128,11 +128,11 @@ In the above example, `:all` and `:admin` (`super_role`) permissions don't requi Valid values for the `:scope` keyword are: - `false`: disables scoping -- `User`: a module that will be passed to [has_user_access?/5](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/5). It must implement a [Authorization behaviour](https://hexdocs.pm/rajska/Rajska.Authorization.html) and a `__schema__(:source)` function (used to check if the module is valid in [validate_query_auth_config!/2](https://hexdocs.pm/rajska/Rajska.Schema.html#validate_query_auth_config!/2)) +- `User`: a module that will be passed to [has_user_access?/4](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/4). It must implement a [Authorization behaviour](https://hexdocs.pm/rajska/Rajska.Authorization.html) and a `__schema__(:source)` function (used to check if the module is valid in [validate_query_auth_config!/2](https://hexdocs.pm/rajska/Rajska.Schema.html#validate_query_auth_config!/2)) Valid values for the `:args` keyword are: -- `:id`: where `:id` is the query argument that will also be passed to [has_user_access?/5](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/5) +- `:id`: where `:id` is the query argument that will also be passed to [has_user_access?/4](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/4) - `%{id: [:params, :id]}`: where `id` is the query argument as above, but it's not defined directly as an `arg` for the query. Instead, it's nested inside the `params` argument. - `[:code, :user_group_id]`: where `user_group_id` (it could also be a nested argument) is an optional argument for the query. If it's present, the scoping will be applied, otherwise no scoping is applied. @@ -222,7 +222,7 @@ object :wallet do end ``` -To define custom rules for the scoping, use [has_user_access?/5](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/5). For example: +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: ```elixir defmodule Authorization do @@ -231,13 +231,13 @@ defmodule Authorization do super_role: :admin @impl true - def has_user_access?(%{role: :admin}, User, _id, _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, _id, _field, _rule), do: false + 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 end ``` -Keep in mind that the `field_value` provided to `has_user_access?/5` can be `nil`. This case can be handled as you wish. +Keep in mind that the `field_value` provided to `has_user_access?/4` can be `nil`. This case can be handled as you wish. For example, to not raise any authorization errors and just return `nil`: ```elixir @@ -249,9 +249,9 @@ defmodule Authorization do @impl true def has_user_access?(_user, _, _, nil), do: true - def has_user_access?(%{role: :admin}, User, _id, _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, _id, _field, _rule), do: false + 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 end ``` diff --git a/lib/authorization.ex b/lib/authorization.ex index 1c4939d..6033f52 100644 --- a/lib/authorization.ex +++ b/lib/authorization.ex @@ -22,8 +22,7 @@ defmodule Rajska.Authorization do @callback has_user_access?( current_user, scope :: module(), - field_value :: any(), - field :: any(), + {field :: any(), field_value :: any()}, rule :: any() ) :: boolean() @@ -34,6 +33,6 @@ defmodule Rajska.Authorization do not_scoped_roles: 0, role_authorized?: 2, field_authorized?: 3, - has_user_access?: 5, + has_user_access?: 4, unauthorized_msg: 1 end diff --git a/lib/middlewares/object_scope_authorization.ex b/lib/middlewares/object_scope_authorization.ex index 8b91a37..bce3f70 100644 --- a/lib/middlewares/object_scope_authorization.ex +++ b/lib/middlewares/object_scope_authorization.ex @@ -38,7 +38,7 @@ defmodule Rajska.ObjectScopeAuthorization do end ``` - To define custom rules for the scoping, use `c:Rajska.Authorization.has_user_access?/5`. For example: + To define custom rules for the scoping, use `c:Rajska.Authorization.has_user_access?/4`. For example: ```elixir defmodule Authorization do @@ -46,13 +46,13 @@ defmodule Rajska.ObjectScopeAuthorization do valid_roles: [:user, :admin] @impl true - def has_user_access?(%{role: :admin}, _struct, _field_value, field, _rule), do: true - def has_user_access?(%{id: user_id}, User, id, field, _rule) when user_id === id, do: true - def has_user_access?(_current_user, User, _field_value, field, _rule), do: false + 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 end ``` - Keep in mind that the `field_value` provided to `has_user_access?/5` can be `nil`. This case can be handled as you wish. + Keep in mind that the `field_value` provided to `has_user_access?/4` can be `nil`. This case can be handled as you wish. For example, to not raise any authorization errors and just return `nil`: ```elixir @@ -63,13 +63,13 @@ defmodule Rajska.ObjectScopeAuthorization do @impl true def has_user_access?(_user, _, nil, _, _), do: true - def has_user_access?(%{role: :admin}, User, _field_value, _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_value, _field, _rule), do: false + 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 end ``` - The `rule` keyword is not mandatory and will be pattern matched in `has_user_access?/5`: + The `rule` keyword is not mandatory and will be pattern matched in `has_user_access?/4`: ```elixir defmodule Authorization do @@ -77,8 +77,8 @@ defmodule Rajska.ObjectScopeAuthorization do valid_roles: [:user, :admin] @impl true - def has_user_access?(%{id: user_id}, Wallet, _field_value, _field, :read_only), do: true - def has_user_access?(%{id: user_id}, Wallet, _field_value, _field, :default), do: false + 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 end ``` @@ -145,14 +145,14 @@ defmodule Rajska.ObjectScopeAuthorization do defp authorized?(false, _values, _context, _, _object), do: true - defp authorized?({scope, field}, values, context, rule, _object) do - scope_field_value = Map.get(values, field) - Rajska.apply_auth_mod(context, :has_context_access?, [context, scope, scope_field_value, field, rule]) + 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 - scope_field_value = Map.get(values, :id) - Rajska.apply_auth_mod(context, :has_context_access?, [context, scope, scope_field_value, :id, rule]) + 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 diff --git a/lib/middlewares/query_scope_authorization.ex b/lib/middlewares/query_scope_authorization.ex index 081b26a..8dde1e2 100644 --- a/lib/middlewares/query_scope_authorization.ex +++ b/lib/middlewares/query_scope_authorization.ex @@ -40,16 +40,16 @@ defmodule Rajska.QueryScopeAuthorization do ``` In the above example, `:all` and `:admin` permissions don't require the `:scope` keyword, as defined in the `c:Rajska.Authorization.not_scoped_roles/0` function, but you can modify this behavior by overriding it. - The `rule` keyword is not mandatory and will be pattern matched in `c:Rajska.Authorization.has_user_access?/5`. This way different rules can be set to the same struct. + The `rule` keyword is not mandatory and will be pattern matched in `c:Rajska.Authorization.has_user_access?/4`. This way different rules can be set to the same struct. See `Rajska.Authorization` for `rule` default settings. Valid values for the `:scope` keyword are: - `false`: disables scoping - - `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/5`. 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 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`) Valid values for the `:args` keyword are: - - `:id`: where `:id` is the query argument that will also be passed to `c:Rajska.Authorization.has_user_access?/5` + - `:id`: where `:id` is the query argument that will also be passed to `c:Rajska.Authorization.has_user_access?/4` - `%{id: [:params, :id]}`: where `id` is the query argument as above, but it's not defined directly as an `arg` for the query. Instead, it's nested inside the `params` argument. - `[:code, :user_group_id]`: where `user_group_id` (it could also be a nested argument) is an optional argument for the query. If it's present, the scoping will be applied, otherwise no scoping is applied. """ @@ -74,7 +74,7 @@ defmodule Rajska.QueryScopeAuthorization do end def scope_user!(%{context: context} = resolution, config) do - default_rule = Rajska.apply_auth_mod(resolution.context, :default_rule) + default_rule = Rajska.apply_auth_mod(context, :default_rule) rule = Keyword.get(config, :rule, default_rule) scope = Keyword.get(config, :scope) arg_fields = config |> Keyword.get(:args, :id) |> arg_fields_to_map() @@ -82,7 +82,7 @@ defmodule Rajska.QueryScopeAuthorization do arguments_source = get_arguments_source!(resolution, scope) arg_fields - |> Enum.all?(& apply_scope_authorization(context, scope, arguments_source, &1, rule, optional)) + |> Enum.all?(& apply_scope_authorization(resolution, scope, arguments_source, &1, rule, optional)) |> update_result(resolution) end @@ -98,17 +98,25 @@ defmodule Rajska.QueryScopeAuthorization do defp get_arguments_source!(%Resolution{arguments: args}, _scope), do: args - def apply_scope_authorization(context, scope, arguments_source, {scope_field, arg_field}, rule, optional) do - field_value = get_scope_field_value(arguments_source, arg_field) - - (optional && field_value === nil) || has_context_access?(context, scope, field_value, scope_field, rule) + def apply_scope_authorization( + %Resolution{definition: definition, context: context}, + scope, + arguments_source, + {scope_field, arg_field}, + rule, + optional + ) do + case get_scope_field_value(arguments_source, arg_field) do + nil -> optional || raise "Error in query #{definition.name}: no argument #{inspect arg_field} found in #{inspect arguments_source}" + field_value -> has_context_access?(context, scope, {scope_field, field_value}, rule) + end end defp get_scope_field_value(arguments_source, fields) when is_list(fields), do: get_in(arguments_source, fields) defp get_scope_field_value(arguments_source, field) when is_atom(field), do: Map.get(arguments_source, field) - defp has_context_access?(context, scope, field_value, scope_field, rule) do - Rajska.apply_auth_mod(context, :has_context_access?, [context, scope, field_value, scope_field, rule]) + defp has_context_access?(context, scope, {scope_field, field_value}, rule) do + Rajska.apply_auth_mod(context, :has_context_access?, [context, scope, {scope_field, field_value}, rule]) end defp update_result(true, resolution), do: resolution diff --git a/lib/rajska.ex b/lib/rajska.ex index d946db8..21b8f22 100644 --- a/lib/rajska.ex +++ b/lib/rajska.ex @@ -23,7 +23,7 @@ defmodule Rajska do ## Usage - Create your Authorization module, which will implement the `Rajska.Authorization` 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 `c:Rajska.Authorization.role_authorized?/2`, `c:Rajska.Authorization.has_user_access?/5` and `c:Rajska.Authorization.field_authorized?/3`, but you can override them with your application needs. + Create your Authorization module, which will implement the `Rajska.Authorization` 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 `c:Rajska.Authorization.role_authorized?/2`, `c:Rajska.Authorization.has_user_access?/4` and `c:Rajska.Authorization.field_authorized?/3`, but you can override them with your application needs. ```elixir defmodule Authorization do @@ -103,7 +103,7 @@ defmodule Rajska do 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_value, field, unquote(default_rule)) do + 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? = (user_struct === scope) @@ -135,10 +135,10 @@ defmodule Rajska do |> field_authorized?(scope_by, source) end - def has_context_access?(context, scope, field_value, field, rule) do + def has_context_access?(context, scope, {scope_field, field_value}, rule) do context |> get_current_user() - |> has_user_access?(scope, field_value, field, rule) + |> has_user_access?(scope, {scope_field, field_value}, rule) end defoverridable Authorization diff --git a/test/middlewares/object_scope_authorization_test.exs b/test/middlewares/object_scope_authorization_test.exs index 822450c..0148d16 100644 --- a/test/middlewares/object_scope_authorization_test.exs +++ b/test/middlewares/object_scope_authorization_test.exs @@ -16,17 +16,17 @@ defmodule Rajska.ObjectScopeAuthorizationTest do valid_roles: [:user, :admin], super_role: :admin - def has_user_access?(%{role: :admin}, User, _id, _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, _id, _field, :default), 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 - def has_user_access?(%{role: :admin}, Company, _id, _field, :default), do: true - def has_user_access?(%{id: user_id}, Company, company_user_id, :id, :default) when user_id === company_user_id, do: true - def has_user_access?(_current_user, Company, _id, _field, :default), 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 + def has_user_access?(_current_user, Company, _field, :default), do: false - def has_user_access?(%{role: :admin}, Wallet, _id, _field, :default), do: true - def has_user_access?(%{id: user_id}, Wallet, id, :id, :default) when user_id === id, do: true - def has_user_access?(_current_user, Wallet, _id, _field, :default), do: false + def has_user_access?(%{role: :admin}, Wallet, _field, :default), do: true + def has_user_access?(%{id: user_id}, Wallet, {:user_id, id}, :default) when user_id === id, do: true + def has_user_access?(_current_user, Wallet, _field, :default), do: false end defmodule Schema do diff --git a/test/middlewares/query_scope_authorization_test.exs b/test/middlewares/query_scope_authorization_test.exs index 18b49e4..89b621c 100644 --- a/test/middlewares/query_scope_authorization_test.exs +++ b/test/middlewares/query_scope_authorization_test.exs @@ -25,12 +25,12 @@ defmodule Rajska.QueryScopeAuthorizationTest do valid_roles: [:user, :admin], super_role: :admin - def has_user_access?(%{role: :admin}, User, _id, _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, _id, _field, :default), 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 - def has_user_access?(_current_user, BankAccount, _id, _field, :edit), do: false - def has_user_access?(_current_user, BankAccount, _id, _field, :read_only), do: true + def has_user_access?(_current_user, BankAccount, _field, :edit), do: false + def has_user_access?(_current_user, BankAccount, _field, :read_only), do: true end defmodule Schema do @@ -205,7 +205,7 @@ defmodule Rajska.QueryScopeAuthorizationTest do user = %{role: :user, id: 1} custom_nested_arg_scoped_query = custom_nested_arg_scoped_query(nil) - assert_raise RuntimeError, "Error in query customNestedArgScopedQuery: no argument found in middleware Scope Authorization", fn -> + assert_raise RuntimeError, "Error in query customNestedArgScopedQuery: no argument [:params, :id] found in %{params: %{id: nil}}", fn -> Absinthe.run(custom_nested_arg_scoped_query, __MODULE__.Schema, context: %{current_user: user}) end end From d3784763cf8111f11ecd92b98c95517bd906bdbf Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Tue, 8 Oct 2019 20:59:08 -0300 Subject: [PATCH 4/9] Create schema validations tests --- lib/schema.ex | 5 +- test/middlewares/schema_test.exs | 112 +++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/lib/schema.ex b/lib/schema.ex index 704374f..1195232 100644 --- a/lib/schema.ex +++ b/lib/schema.ex @@ -100,8 +100,9 @@ defmodule Rajska.Schema do defp validate_scope!(:source, _role, _authorization), do: :ok defp validate_scope!(scope, _role, _authorization) when is_atom(scope) do - unless scope.__schema__(:source), - do: raise ":scope option #{scope} doesn't implement a __schema__(:source) function." + scope.__schema__(:source) + rescue + UndefinedFunctionError -> reraise ":scope option #{scope} doesn't implement a __schema__(:source) function.", __STACKTRACE__ end defp validate_args!(args) when is_map(args), do: :ok diff --git a/test/middlewares/schema_test.exs b/test/middlewares/schema_test.exs index fc0b873..5ab54a6 100644 --- a/test/middlewares/schema_test.exs +++ b/test/middlewares/schema_test.exs @@ -105,6 +105,118 @@ defmodule Rajska.SchemaTest do end end + test "Raises if scope module doesn't implement a __schema__(:source) function" do + assert_raise( + RuntimeError, + ~r/Query get_user is configured incorrectly, :scope option invalid_module doesn't implement a __schema__/, + fn -> + defmodule Schema do + use Absinthe.Schema + + def context(ctx), do: Map.put(ctx, :authorization, Authorization) + + def middleware(middleware, field, %{identifier: identifier}) + when identifier in [:query, :mutation] do + Rajska.add_query_authorization(middleware, field, Authorization) + end + + def middleware(middleware, _field, _object), do: middleware + + query do + field :get_user, :string do + middleware Rajska.QueryAuthorization, [permit: :user, scope: :invalid_module] + resolve fn _args, _info -> {:ok, "bob"} end + end + end + end + end + ) + end + + test "Raises if args option is invalid" do + assert_raise( + RuntimeError, + ~r/Query get_user is configured incorrectly, the following args option is invalid: args/, + fn -> + defmodule Schema do + use Absinthe.Schema + + def context(ctx), do: Map.put(ctx, :authorization, Authorization) + + def middleware(middleware, field, %{identifier: identifier}) + when identifier in [:query, :mutation] do + Rajska.add_query_authorization(middleware, field, Authorization) + end + + def middleware(middleware, _field, _object), do: middleware + + query do + field :get_user, :string do + middleware Rajska.QueryAuthorization, [permit: :user, scope: :source, args: "args"] + resolve fn _args, _info -> {:ok, "bob"} end + end + end + end + end + ) + end + + test "Raises if optional option is not a boolean" do + assert_raise( + RuntimeError, + ~r/Query get_user is configured incorrectly, optional option must be a boolean./, + fn -> + defmodule Schema do + use Absinthe.Schema + + def context(ctx), do: Map.put(ctx, :authorization, Authorization) + + def middleware(middleware, field, %{identifier: identifier}) + when identifier in [:query, :mutation] do + Rajska.add_query_authorization(middleware, field, Authorization) + end + + def middleware(middleware, _field, _object), do: middleware + + query do + field :get_user, :string do + middleware Rajska.QueryAuthorization, [permit: :user, scope: :source, optional: :invalid] + resolve fn _args, _info -> {:ok, "bob"} end + end + end + end + end + ) + end + + test "Raises if rule option is not an atom" do + assert_raise( + RuntimeError, + ~r/Query get_user is configured incorrectly, rule option must be an atom./, + fn -> + defmodule Schema do + use Absinthe.Schema + + def context(ctx), do: Map.put(ctx, :authorization, Authorization) + + def middleware(middleware, field, %{identifier: identifier}) + when identifier in [:query, :mutation] do + Rajska.add_query_authorization(middleware, field, Authorization) + end + + def middleware(middleware, _field, _object), do: middleware + + query do + field :get_user, :string do + middleware Rajska.QueryAuthorization, [permit: :user, scope: :source, rule: 4] + resolve fn _args, _info -> {:ok, "bob"} end + end + end + end + end + ) + end + test "Raises if no authorization module is found in absinthe's context" do assert_raise RuntimeError, ~r/Rajska authorization module not found in Absinthe's context/, fn -> defmodule Schema do From d461b365c5a80105c44e3e5840dbbe94b7db6ac4 Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Tue, 8 Oct 2019 21:07:35 -0300 Subject: [PATCH 5/9] Improve query scope authorization docs --- README.md | 8 ++++---- lib/middlewares/query_scope_authorization.ex | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c9467d7..85fa22c 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Usage: arg :id, non_null(:integer) arg :params, non_null(:user_params) - middleware Rajska.QueryAuthorization, [permit: :user, scope: User] # same as {User, :id} + middleware Rajska.QueryAuthorization, [permit: :user, scope: User] # same as [permit: :user, scope: User, args: :id] resolve &AccountsResolver.update_user/2 end @@ -132,9 +132,9 @@ Valid values for the `:scope` keyword are: Valid values for the `:args` keyword are: -- `:id`: where `:id` is the query argument that will also be passed to [has_user_access?/4](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/4) -- `%{id: [:params, :id]}`: where `id` is the query argument as above, but it's not defined directly as an `arg` for the query. Instead, it's nested inside the `params` argument. -- `[:code, :user_group_id]`: where `user_group_id` (it could also be a nested argument) is an optional argument for the query. If it's present, the scoping will be applied, otherwise no scoping is applied. +- `%{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) +- `[: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. ### Object Authorization diff --git a/lib/middlewares/query_scope_authorization.ex b/lib/middlewares/query_scope_authorization.ex index 8dde1e2..56ecf8c 100644 --- a/lib/middlewares/query_scope_authorization.ex +++ b/lib/middlewares/query_scope_authorization.ex @@ -49,9 +49,9 @@ defmodule Rajska.QueryScopeAuthorization do Valid values for the `:args` keyword are: - - `:id`: where `:id` is the query argument that will also be passed to `c:Rajska.Authorization.has_user_access?/4` - - `%{id: [:params, :id]}`: where `id` is the query argument as above, but it's not defined directly as an `arg` for the query. Instead, it's nested inside the `params` argument. - - `[:code, :user_group_id]`: where `user_group_id` (it could also be a nested argument) is an optional argument for the query. If it's present, the scoping will be applied, otherwise no scoping is applied. + - `%{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` + - `[: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. """ @behaviour Absinthe.Middleware From bc92a54bcae8e1a92cf19dbcd55dc1591813936b Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Wed, 9 Oct 2019 10:18:52 -0300 Subject: [PATCH 6/9] Fix docs when field_value is nil --- README.md | 2 +- lib/middlewares/object_scope_authorization.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 85fa22c..14c0cac 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,7 @@ defmodule Authorization do super_role: :admin @impl true - def has_user_access?(_user, _, _, nil), do: 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 diff --git a/lib/middlewares/object_scope_authorization.ex b/lib/middlewares/object_scope_authorization.ex index bce3f70..5eefbd3 100644 --- a/lib/middlewares/object_scope_authorization.ex +++ b/lib/middlewares/object_scope_authorization.ex @@ -61,7 +61,7 @@ defmodule Rajska.ObjectScopeAuthorization do valid_roles: [:user, :admin] @impl true - def has_user_access?(_user, _, nil, _, _), do: 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 From dc5cf6548b089235fb542fa6119a214902c1b09e Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Wed, 9 Oct 2019 10:46:21 -0300 Subject: [PATCH 7/9] Improve Query Scope Authorization docs --- README.md | 23 +++++++++++--------- lib/middlewares/query_scope_authorization.ex | 20 +++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 14c0cac..ea18eb1 100644 --- a/README.md +++ b/README.md @@ -125,16 +125,19 @@ Provides scoping to Absinthe's queries, as seen above in [Query Authorization](# In the above example, `:all` and `:admin` (`super_role`) permissions don't require the `:scope` 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 `:scope` keyword are: - -- `false`: disables scoping -- `User`: a module that will be passed to [has_user_access?/4](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/4). It must implement a [Authorization behaviour](https://hexdocs.pm/rajska/Rajska.Authorization.html) and a `__schema__(:source)` function (used to check if the module is valid in [validate_query_auth_config!/2](https://hexdocs.pm/rajska/Rajska.Schema.html#validate_query_auth_config!/2)) - -Valid values for the `:args` keyword are: - -- `%{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) -- `[: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. +## Options + +All the following options are sent to [has_user_access?/4](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/4): + +* `: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`) +* `: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) + - `[: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. ### Object Authorization diff --git a/lib/middlewares/query_scope_authorization.ex b/lib/middlewares/query_scope_authorization.ex index 56ecf8c..df66d42 100644 --- a/lib/middlewares/query_scope_authorization.ex +++ b/lib/middlewares/query_scope_authorization.ex @@ -40,18 +40,20 @@ defmodule Rajska.QueryScopeAuthorization do ``` In the above example, `:all` and `:admin` permissions don't require the `:scope` keyword, as defined in the `c:Rajska.Authorization.not_scoped_roles/0` function, but you can modify this behavior by overriding it. - The `rule` keyword is not mandatory and will be pattern matched in `c:Rajska.Authorization.has_user_access?/4`. This way different rules can be set to the same struct. - See `Rajska.Authorization` for `rule` default settings. - Valid values for the `:scope` keyword are: - - `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`) + ## Options - Valid values for the `:args` keyword are: + All the following options are sent to `c:Rajska.Authorization.has_user_access?/4`: - - `%{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` - - `[: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. + * `: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`) + * `: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` + - `[: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. """ @behaviour Absinthe.Middleware From 701d2df65e138370913d44cb26ca105403e9a7de Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Wed, 9 Oct 2019 10:49:31 -0300 Subject: [PATCH 8/9] Improve query authorization docs --- lib/middlewares/query_authorization.ex | 2 +- lib/middlewares/query_scope_authorization.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/middlewares/query_authorization.ex b/lib/middlewares/query_authorization.ex index d39cc3a..5743b74 100644 --- a/lib/middlewares/query_authorization.ex +++ b/lib/middlewares/query_authorization.ex @@ -19,7 +19,7 @@ defmodule Rajska.QueryAuthorization do arg :id, non_null(:integer) arg :params, non_null(:user_params) - middleware Rajska.QueryAuthorization, [permit: :user, scope: User] # same as {User, :id} + middleware Rajska.QueryAuthorization, [permit: :user, scope: User] # same as [permit: :user, scope: User, args: :id] resolve &AccountsResolver.update_user/2 end diff --git a/lib/middlewares/query_scope_authorization.ex b/lib/middlewares/query_scope_authorization.ex index df66d42..43d33a4 100644 --- a/lib/middlewares/query_scope_authorization.ex +++ b/lib/middlewares/query_scope_authorization.ex @@ -19,7 +19,7 @@ defmodule Rajska.QueryScopeAuthorization do arg :id, non_null(:integer) arg :params, non_null(:user_params) - middleware Rajska.QueryAuthorization, [permit: :user, scope: User] # same as {User, :id} + middleware Rajska.QueryAuthorization, [permit: :user, scope: User] # same as [permit: :user, scope: User, args: :id] resolve &AccountsResolver.update_user/2 end From 225d2e2313b053831390a7c11feefe32b2741600 Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Wed, 9 Oct 2019 11:11:07 -0300 Subject: [PATCH 9/9] Improve args option validations --- lib/schema.ex | 32 ++++++++++++++++++++++++-------- test/middlewares/schema_test.exs | 12 ++++++------ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/lib/schema.ex b/lib/schema.ex index 1195232..f4e9b62 100644 --- a/lib/schema.ex +++ b/lib/schema.ex @@ -81,18 +81,18 @@ defmodule Rajska.Schema do end end - defp validate_presence!(nil, option), do: raise "#{option} option must be present." + defp validate_presence!(nil, option), do: raise "#{inspect(option)} option must be present." defp validate_presence!(_value, _option), do: :ok defp validate_boolean!(value, _option) when is_boolean(value), do: :ok - defp validate_boolean!(_value, option), do: raise "#{option} option must be a boolean." + defp validate_boolean!(_value, option), do: raise "#{inspect(option)} option must be a boolean." defp validate_atom!(value, _option) when is_atom(value), do: :ok - defp validate_atom!(_value, option), do: raise "#{option} option must be an atom." + defp validate_atom!(_value, option), do: raise "#{inspect(option)} option must be an atom." defp validate_scope!(nil, role, authorization) do unless Enum.member?(authorization.not_scoped_roles(), role), - do: raise ":scope option must be present for role #{role}." + do: raise ":scope option must be present for role #{inspect(role)}." end defp validate_scope!(false, _role, _authorization), do: :ok @@ -102,11 +102,27 @@ defmodule Rajska.Schema do defp validate_scope!(scope, _role, _authorization) when is_atom(scope) do scope.__schema__(:source) rescue - UndefinedFunctionError -> reraise ":scope option #{scope} doesn't implement a __schema__(:source) function.", __STACKTRACE__ + UndefinedFunctionError -> reraise ":scope option #{inspect(scope)} doesn't implement a __schema__(:source) function.", __STACKTRACE__ end - defp validate_args!(args) when is_map(args), do: :ok - defp validate_args!(args) when is_list(args), do: :ok + defp validate_args!(args) when is_map(args) do + Enum.each(args, fn + {field, value} when is_atom(field) and is_atom(value) -> :ok + {field, values} when is_atom(field) and is_list(values) -> validate_list_of_atoms!(values) + field_value -> raise "the following args option is invalid: #{inspect(field_value)}. Since the provided args is a map, you should provide an atom key and an atom or list of atoms value." + end) + end + + defp validate_args!(args) when is_list(args), do: validate_list_of_atoms!(args) + defp validate_args!(args) when is_atom(args), do: :ok - defp validate_args!(args), do: raise "the following args option is invalid: #{args}" + + defp validate_args!(args), do: raise "the following args option is invalid: #{inspect(args)}" + + defp validate_list_of_atoms!(args) do + Enum.each(args, fn + arg when is_atom(arg) -> :ok + arg -> raise "the following args option is invalid: #{inspect(args)}. Expected a list of atoms, but found #{inspect(arg)}" + end) + end end diff --git a/test/middlewares/schema_test.exs b/test/middlewares/schema_test.exs index 5ab54a6..2984a4a 100644 --- a/test/middlewares/schema_test.exs +++ b/test/middlewares/schema_test.exs @@ -33,7 +33,7 @@ defmodule Rajska.SchemaTest do test "Raises in compile time if no scope key is specified for a scope role" do assert_raise( RuntimeError, - ~r/Query get_user is configured incorrectly, :scope option must be present for role user/, + ~r/Query get_user is configured incorrectly, :scope option must be present for role :user/, fn -> defmodule Schema do use Absinthe.Schema @@ -82,7 +82,7 @@ defmodule Rajska.SchemaTest do end test "Raises if no permit key is specified for a query" do - assert_raise RuntimeError, ~r/Query get_user is configured incorrectly, permit option must be present/, fn -> + assert_raise RuntimeError, ~r/Query get_user is configured incorrectly, :permit option must be present/, fn -> defmodule Schema do use Absinthe.Schema @@ -108,7 +108,7 @@ defmodule Rajska.SchemaTest do test "Raises if scope module doesn't implement a __schema__(:source) function" do assert_raise( RuntimeError, - ~r/Query get_user is configured incorrectly, :scope option invalid_module doesn't implement a __schema__/, + ~r/Query get_user is configured incorrectly, :scope option :invalid_module doesn't implement a __schema__/, fn -> defmodule Schema do use Absinthe.Schema @@ -136,7 +136,7 @@ defmodule Rajska.SchemaTest do test "Raises if args option is invalid" do assert_raise( RuntimeError, - ~r/Query get_user is configured incorrectly, the following args option is invalid: args/, + ~r/Query get_user is configured incorrectly, the following args option is invalid: "args"/, fn -> defmodule Schema do use Absinthe.Schema @@ -164,7 +164,7 @@ defmodule Rajska.SchemaTest do test "Raises if optional option is not a boolean" do assert_raise( RuntimeError, - ~r/Query get_user is configured incorrectly, optional option must be a boolean./, + ~r/Query get_user is configured incorrectly, :optional option must be a boolean./, fn -> defmodule Schema do use Absinthe.Schema @@ -192,7 +192,7 @@ defmodule Rajska.SchemaTest do test "Raises if rule option is not an atom" do assert_raise( RuntimeError, - ~r/Query get_user is configured incorrectly, rule option must be an atom./, + ~r/Query get_user is configured incorrectly, :rule option must be an atom./, fn -> defmodule Schema do use Absinthe.Schema