From 4fb2f012b9cca7531b6a37d996a8e49e5a72b615 Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Wed, 6 Nov 2019 23:56:19 -0300 Subject: [PATCH 01/12] Merge has_user_access/4 arguments --- README.md | 26 +++++++++---------- lib/authorization.ex | 5 ++-- lib/middlewares/object_scope_authorization.ex | 24 ++++++++--------- lib/middlewares/query_scope_authorization.ex | 6 ++--- lib/rajska.ex | 11 +++----- lib/schema.ex | 7 ++--- mix.exs | 13 +++++++++- test/middlewares/field_authorization_test.exs | 8 +++--- .../object_scope_authorization_test.exs | 21 ++++++++------- .../query_scope_authorization_test.exs | 10 +++---- 10 files changed, 66 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 2c7311d..515fc46 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?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3) 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 @@ -127,14 +127,14 @@ In the above example, `:all` and `:admin` (`super_role`) permissions don't requi ## Options -All the following options are sent to [has_user_access?/4](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/4): +All the following options are sent to [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3): * `:scope` - `false`: disables scoping - - `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/4`. It must define a struct. + - `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/3`. It must define a struct. * `:args` - `%{user_id: [:params, :id]}`: where `user_id` is the scoped field and `id` is an argument nested inside the `params` argument. - - `:id`: this is the same as `%{id: :id}`, where `:id` is both the query argument and the scoped field that will be passed to [has_user_access?/4](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/4) + - `: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?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3) - `[: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. @@ -225,7 +225,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?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3). For example: ```elixir defmodule Authorization do @@ -234,13 +234,13 @@ defmodule Authorization do super_role: :admin @impl 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 - def has_user_access?(_current_user, User, _field, _rule), do: false + def has_user_access?(%{role: :admin}, %User{}, _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{}, _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?/3` can be `nil`. This case can be handled as you wish. For example, to not raise any authorization errors and just return `nil`: ```elixir @@ -250,11 +250,9 @@ defmodule Authorization do super_role: :admin @impl 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 - def has_user_access?(_current_user, User, _field, _rule), do: false + def has_user_access?(%{role: :admin}, %User{}, _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{}, _rule), do: false end ``` diff --git a/lib/authorization.ex b/lib/authorization.ex index 6033f52..a0dc226 100644 --- a/lib/authorization.ex +++ b/lib/authorization.ex @@ -21,8 +21,7 @@ defmodule Rajska.Authorization do @callback has_user_access?( current_user, - scope :: module(), - {field :: any(), field_value :: any()}, + scoped_struct :: struct(), rule :: any() ) :: boolean() @@ -33,6 +32,6 @@ defmodule Rajska.Authorization do not_scoped_roles: 0, role_authorized?: 2, field_authorized?: 3, - has_user_access?: 4, + has_user_access?: 3, unauthorized_msg: 1 end diff --git a/lib/middlewares/object_scope_authorization.ex b/lib/middlewares/object_scope_authorization.ex index 11c3611..2447352 100644 --- a/lib/middlewares/object_scope_authorization.ex +++ b/lib/middlewares/object_scope_authorization.ex @@ -39,7 +39,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?/3`. For example: ```elixir defmodule Authorization do @@ -47,13 +47,13 @@ defmodule Rajska.ObjectScopeAuthorization do valid_roles: [:user, :admin] @impl true - 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 + def has_user_access?(%{role: :admin}, _, _scoped_struct, _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{}, _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?/3` can be `nil`. This case can be handled as you wish. For example, to not raise any authorization errors and just return `nil`: ```elixir @@ -62,15 +62,13 @@ defmodule Rajska.ObjectScopeAuthorization do valid_roles: [:user, :admin] @impl 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 - def has_user_access?(_current_user, User, {_field, _field_value}, _rule), do: false + def has_user_access?(%User{role: :admin}, _scoped_struct, _rule), do: true + def has_user_access?(%User{id: user_id}, %User{id: id}, _rule) when user_id === id, do: true + def has_user_access?(_current_user, %User{}, _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?/3`: ```elixir defmodule Authorization do @@ -78,8 +76,8 @@ defmodule Rajska.ObjectScopeAuthorization do valid_roles: [:user, :admin] @impl true - 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 + def has_user_access?(%{id: user_id}, %Wallet{}, :read_only), do: true + def has_user_access?(%{id: user_id}, %Wallet{}, :default), do: false end ``` diff --git a/lib/middlewares/query_scope_authorization.ex b/lib/middlewares/query_scope_authorization.ex index ab18bb3..efaf907 100644 --- a/lib/middlewares/query_scope_authorization.ex +++ b/lib/middlewares/query_scope_authorization.ex @@ -43,14 +43,14 @@ defmodule Rajska.QueryScopeAuthorization do ## Options - All the following options are sent to `c:Rajska.Authorization.has_user_access?/4`: + All the following options are sent to `c:Rajska.Authorization.has_user_access?/3`: * `:scope` - `false`: disables scoping - - `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/4`. It must define a struct. + - `User`: a module that will be passed to `c:Rajska.Authorization.has_user_access?/3`. It must define a struct. * `:args` - `%{user_id: [:params, :id]}`: where `user_id` is the scoped field and `id` is an argument nested inside the `params` argument. - - `:id`: this is the same as `%{id: :id}`, where `:id` is both the query argument and the scoped field that will be passed to `c:Rajska.Authorization.has_user_access?/4` + - `: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?/3` - `[: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. diff --git a/lib/rajska.ex b/lib/rajska.ex index 075ad29..c272c8d 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?/3` and `c:Rajska.Authorization.field_authorized?/3`, but you can override them with your application needs. ```elixir defmodule Authorization do @@ -100,12 +100,9 @@ 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 has_user_access?(%user_struct{id: user_id} = current_user, scope, {field, field_value}, unquote(default_rule)) do + def has_user_access?(%user_struct{id: user_id} = current_user, %scope{} = struct, unquote(default_rule)) do super_user? = current_user |> get_user_role() |> super_role?() - owner? = - (user_struct === scope) - && (field === :id) - && (user_id === field_value) + owner? = (user_struct === scope) && (user_id === struct.id) super_user? || owner? end @@ -129,7 +126,7 @@ defmodule Rajska do def has_context_access?(context, scope, {scope_field, field_value}, rule) do context |> get_current_user() - |> has_user_access?(scope, {scope_field, field_value}, rule) + |> has_user_access?(scope.__struct__([{scope_field, field_value}]), rule) end defoverridable Authorization diff --git a/lib/schema.ex b/lib/schema.ex index c056f92..ad04e32 100644 --- a/lib/schema.ex +++ b/lib/schema.ex @@ -3,11 +3,8 @@ defmodule Rajska.Schema do Concatenates Rajska middlewares with Absinthe middlewares and validates Query Authorization configuration. """ - alias Absinthe.Type.{ - Field, - Middleware, - Object - } + alias Absinthe.Middleware + alias Absinthe.Type.{Field, Object} alias Rajska.{ FieldAuthorization, diff --git a/mix.exs b/mix.exs index 8421d63..1f53c4a 100644 --- a/mix.exs +++ b/mix.exs @@ -13,12 +13,14 @@ defmodule Rajska.MixProject do description: "Rajska is an authorization library for Absinthe.", package: package(), elixirc_paths: elixirc_paths(Mix.env()), + aliases: aliases(), test_coverage: [tool: ExCoveralls], preferred_cli_env: [ coveralls: :test, "coveralls.detail": :test, "coveralls.post": :test, - "coveralls.html": :test + "coveralls.html": :test, + "test.all": :test ] ] end @@ -51,4 +53,13 @@ defmodule Rajska.MixProject do {:excoveralls, "~> 0.11", only: :test}, ] end + + defp aliases do + [ + "test.all": [ + "credo --strict", + "test" + ] + ] + end end diff --git a/test/middlewares/field_authorization_test.exs b/test/middlewares/field_authorization_test.exs index c9fff0c..87ee38b 100644 --- a/test/middlewares/field_authorization_test.exs +++ b/test/middlewares/field_authorization_test.exs @@ -17,10 +17,10 @@ defmodule Rajska.FieldAuthorizationTest do 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 + def has_user_access?(_current_user, %User{}, :private), do: false + def has_user_access?(%{role: :admin}, %User{}, :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{}, :default), do: false end defmodule Schema do diff --git a/test/middlewares/object_scope_authorization_test.exs b/test/middlewares/object_scope_authorization_test.exs index 7b62109..9c61d25 100644 --- a/test/middlewares/object_scope_authorization_test.exs +++ b/test/middlewares/object_scope_authorization_test.exs @@ -5,6 +5,7 @@ defmodule Rajska.ObjectScopeAuthorizationTest do defstruct [ id: 1, total: 10, + user_id: nil ] end @@ -39,18 +40,18 @@ defmodule Rajska.ObjectScopeAuthorizationTest do valid_roles: [:user, :admin], super_role: :admin - 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, User, _field, :object), do: false + def has_user_access?(%{role: :admin}, %User{}, :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{}, :default), do: false + def has_user_access?(_current_user, %User{}, :object), do: false - def has_user_access?(%{role: :admin}, Company, _field, :default), do: true - def has_user_access?(%{id: user_id}, Company, {:user_id, company_user_id}, :default) when user_id === company_user_id, do: true - def has_user_access?(_current_user, Company, _field, :default), do: false + def has_user_access?(%{role: :admin}, %Company{}, :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{}, :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 + def has_user_access?(%{role: :admin}, %Wallet{}, :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{}, :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 bf003da..4e88c18 100644 --- a/test/middlewares/query_scope_authorization_test.exs +++ b/test/middlewares/query_scope_authorization_test.exs @@ -21,12 +21,12 @@ defmodule Rajska.QueryScopeAuthorizationTest do valid_roles: [:user, :admin], super_role: :admin - 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}, %User{}, :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{}, :default), do: false - def has_user_access?(_current_user, BankAccount, _field, :edit), do: false - def has_user_access?(_current_user, BankAccount, _field, :read_only), do: true + def has_user_access?(_current_user, %BankAccount{}, :edit), do: false + def has_user_access?(_current_user, %BankAccount{}, :read_only), do: true end defmodule Schema do From 0365bc94f8ab527354cd58fdb0258268ee4b593e Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Wed, 6 Nov 2019 23:56:50 -0300 Subject: [PATCH 02/12] Bump version to 0.9.0 --- README.md | 2 +- lib/rajska.ex | 2 +- mix.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 515fc46..ef5ef1c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The package can be installed by adding `rajska` to your list of dependencies in ```elixir def deps do [ - {:rajska, "~> 0.8.1"}, + {:rajska, "~> 0.9.0"}, ] end ``` diff --git a/lib/rajska.ex b/lib/rajska.ex index c272c8d..b2cfb79 100644 --- a/lib/rajska.ex +++ b/lib/rajska.ex @@ -16,7 +16,7 @@ defmodule Rajska do ```elixir def deps do [ - {:rajska, "~> 0.8.1"}, + {:rajska, "~> 0.9.0"}, ] end ``` diff --git a/mix.exs b/mix.exs index 1f53c4a..7fbf4f5 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Rajska.MixProject do def project do [ app: :rajska, - version: "0.8.1", + version: "0.9.0", elixir: "~> 1.8", start_permanent: Mix.env() == :prod, deps: deps(), From a94898158b7297307da11a332472677e15d63c24 Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Thu, 7 Nov 2019 13:06:05 -0300 Subject: [PATCH 03/12] Refactor query scope authorization to call has_user_access?/3 instead of has_context_access?/4 --- lib/middlewares/query_scope_authorization.ex | 26 ++++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/middlewares/query_scope_authorization.ex b/lib/middlewares/query_scope_authorization.ex index efaf907..1c20fa1 100644 --- a/lib/middlewares/query_scope_authorization.ex +++ b/lib/middlewares/query_scope_authorization.ex @@ -84,7 +84,9 @@ defmodule Rajska.QueryScopeAuthorization do arguments_source = get_arguments_source!(resolution, scope) arg_fields - |> Enum.all?(& apply_scope_authorization(resolution, scope, arguments_source, &1, rule, optional)) + |> Enum.map(& get_scoped_struct_field(arguments_source, &1, optional, resolution.definition.name)) + |> Enum.filter(& !!&1) + |> has_user_access?(scope, resolution.context, rule, optional) |> update_result(resolution) end @@ -98,25 +100,23 @@ defmodule Rajska.QueryScopeAuthorization do defp get_arguments_source!(%Resolution{arguments: args}, _scope), do: args - def apply_scope_authorization( - %Resolution{definition: definition, context: context}, - scope, - arguments_source, - {scope_field, arg_field}, - rule, - optional - ) do + def get_scoped_struct_field(arguments_source, {scope_field, arg_field}, optional, query_name) 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) + nil when optional === true -> nil + nil when optional === false -> raise "Error in query #{query_name}: no argument #{inspect arg_field} found in #{inspect arguments_source}" + field_value -> {scope_field, field_value} 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, {scope_field, field_value}, rule) do - Rajska.apply_auth_mod(context, :has_context_access?, [context, scope, {scope_field, field_value}, rule]) + defp has_user_access?([], _scope, _context, _rule, true), do: true + + defp has_user_access?(scoped_struct_fields, scope, context, rule, _optional) do + scoped_struct = scope.__struct__(scoped_struct_fields) + + Rajska.apply_auth_mod(context, :has_user_access?, [context.current_user, scoped_struct, rule]) end defp update_result(true, resolution), do: resolution From fbdf4eda0b66b193ed296d160680b73fdc78f1aa Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Thu, 7 Nov 2019 13:59:08 -0300 Subject: [PATCH 04/12] Refactor object scope authorization: pass entire object to has_user_access?/3 and replace scope_by and scope_object_by metas by scope? and scope_object? --- lib/middlewares/object_scope_authorization.ex | 47 +++++-------- .../middlewares/object_authorization_test.exs | 21 ++++++ .../object_scope_authorization_test.exs | 69 +++---------------- 3 files changed, 48 insertions(+), 89 deletions(-) diff --git a/lib/middlewares/object_scope_authorization.ex b/lib/middlewares/object_scope_authorization.ex index 2447352..6af4941 100644 --- a/lib/middlewares/object_scope_authorization.ex +++ b/lib/middlewares/object_scope_authorization.ex @@ -10,7 +10,6 @@ defmodule Rajska.ObjectScopeAuthorization do ```elixir object :user do - meta :scope_by, :id meta :rule, :default field :id, :integer @@ -21,7 +20,6 @@ defmodule Rajska.ObjectScopeAuthorization do end object :company do - meta :scope_by, :user_id meta :rule, :default field :id, :integer @@ -31,12 +29,18 @@ defmodule Rajska.ObjectScopeAuthorization do end object :wallet do - meta :scope_by, :id meta :rule, :read_only field :id, :integer field :total, :integer end + + object :available_dates do + meta :scope?, false + + field :id, :integer + field :date, :date + end ``` To define custom rules for the scoping, use `c:Rajska.Authorization.has_user_access?/3`. For example: @@ -112,13 +116,11 @@ defmodule Rajska.ObjectScopeAuthorization do # Object defp result(%{fields: fields, emitter: %{schema_node: schema_node} = emitter, root_value: root_value} = result, context) do type = Introspection.get_object_type(schema_node.type) - scope_by = get_scope_by!(type) - scope = get_scope!(scope_by, result) - + scope? = get_scope!(type) default_rule = Rajska.apply_auth_mod(context, :default_rule) rule = Type.meta(type, :rule) || default_rule - case authorized?(scope, scope_by, root_value, context, rule, type) do + case !scope? || authorized?(context, root_value, rule) do true -> %{result | fields: walk_result(fields, context)} false -> Map.put(result, :errors, [error(emitter)]) end @@ -141,31 +143,20 @@ defmodule Rajska.ObjectScopeAuthorization do walk_result(fields, context, new_fields) end - defp get_scope_by!(object) do - general_scope_by = Type.meta(object, :scope_by) - object_scope_by = Type.meta(object, :scope_object_by) + defp get_scope!(object) do + scope? = Type.meta(object, :scope?) + scope_object? = Type.meta(object, :scope_object?) - 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" + case {scope?, scope_object?} do + {nil, nil} -> true + {nil, scope_object?} -> scope_object? + {scope?, nil} -> scope? + {_, _} -> raise "Error in #{inspect object.identifier}. If scope_object? is defined, then scope? must not be defined" end end - defp get_scope!(false, _result), do: false - defp get_scope!(_scope_by, %{root_value: %scope{}}), do: scope - defp get_scope!(_scope_by, %{emitter: %{schema_node: schema_node}, root_value: root_value}) do - type = Introspection.get_object_type(schema_node.type) - raise "Expected a Struct for object #{inspect(type.identifier)}, got #{inspect(root_value)}" - end - - defp authorized?(_scope, false, _values, _context, _, _object), do: true - - 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]) + defp authorized?(context, scoped_struct, rule) do + Rajska.apply_auth_mod(context, :has_user_access?, [context.current_user, scoped_struct, rule]) end defp error(%{source_location: location, schema_node: %{type: type}}) do diff --git a/test/middlewares/object_authorization_test.exs b/test/middlewares/object_authorization_test.exs index 2d18fa7..e51b3ab 100644 --- a/test/middlewares/object_authorization_test.exs +++ b/test/middlewares/object_authorization_test.exs @@ -43,6 +43,13 @@ defmodule Rajska.ObjectAuthorizationTest do }} end end + + field :enum_query, :role_enum do + middleware Rajska.QueryAuthorization, [permit: :all, scope: false] + resolve fn _, _ -> + {:ok, :user} + end + end end object :wallet_balance do @@ -59,6 +66,11 @@ defmodule Rajska.ObjectAuthorizationTest do field :wallet_balance, :wallet_balance end + enum :role_enum do + value :user + value :admin + end + object :user do meta :authorize, :all @@ -133,6 +145,13 @@ defmodule Rajska.ObjectAuthorizationTest do refute Map.has_key?(result, :errors) end + test "Query that returns an enum doesn't return errors" do + {:ok, result} = Absinthe.run(enum_query(), __MODULE__.Schema, context: %{current_user: %{role: :admin}}) + + assert %{data: %{"enumQuery" => "USER"}} = result + refute Map.has_key?(result, :errors) + end + defp all_query do """ { @@ -184,4 +203,6 @@ defmodule Rajska.ObjectAuthorizationTest do } """ end + + defp enum_query, do: "{ enumQuery }" end diff --git a/test/middlewares/object_scope_authorization_test.exs b/test/middlewares/object_scope_authorization_test.exs index 9c61d25..6d66cf8 100644 --- a/test/middlewares/object_scope_authorization_test.exs +++ b/test/middlewares/object_scope_authorization_test.exs @@ -105,20 +105,6 @@ defmodule Rajska.ObjectScopeAuthorizationTest do end end - field :object_not_scoped_query, :user do - arg :id, non_null(:integer) - resolve fn args, _ -> - {:ok, %User{id: args.id, name: "bob", not_scoped: %NotScoped{id: 1}}} - end - end - - field :object_not_struct_query, :user do - arg :id, non_null(:integer) - resolve fn args, _ -> - {:ok, %{id: args.id, name: "bob"}} - end - end - field :users_query, list_of(:user) do resolve fn _args, _ -> {:ok, [ @@ -157,7 +143,7 @@ defmodule Rajska.ObjectScopeAuthorizationTest do end object :user do - meta :scope_by, :id + meta :scope?, true field :id, :integer field :email, :string @@ -169,8 +155,6 @@ defmodule Rajska.ObjectScopeAuthorizationTest do end object :company do - meta :scope_by, :user_id - field :id, :integer field :user_id, :integer field :name, :string @@ -178,31 +162,29 @@ defmodule Rajska.ObjectScopeAuthorizationTest do end object :wallet do - meta :scope_by, :user_id - field :total, :integer end object :not_scoped do + meta :scope?, false field :name, :string end object :user_rule do - meta :scope_by, :id meta :rule, :object field :id, :integer end object :both_scopes do - meta :scope_by, :id - meta :scope_object_by, :id + meta :scope?, true + meta :scope_object?, false field :name, :string end object :object_scope_user do - meta :scope_object_by, :id + meta :scope_object?, true field :id, :integer end @@ -244,7 +226,7 @@ defmodule Rajska.ObjectScopeAuthorizationTest do ] == errors end - test "Works when defining scope_object_by instead of scope_by" do + test "Works when defining scope_object? and not scope?" do query = "{ getObjectScopeUser(id: 1) { id } }" {:ok, result} = run_pipeline(query, context(:user, 1)) assert %{data: %{"getObjectScopeUser" => %{}}} = result @@ -365,24 +347,12 @@ defmodule Rajska.ObjectScopeAuthorizationTest do refute Map.has_key?(result, :errors) end - test "Raises when no meta scope_by is defined for an object" do - 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 -> + test "Raises when both scope? metas are defined for an object" do + assert_raise RuntimeError, ~r/Error in :both_scopes. If scope_object\? is defined, then scope\? 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 -> - run_pipeline(object_not_struct_query(2), context(:user, 2)) - end - end - defp all_query(id) do """ { @@ -394,18 +364,6 @@ defmodule Rajska.ObjectScopeAuthorizationTest do """ end - defp object_not_scoped_query(id) do - """ - { - objectNotScopedQuery(id: #{id}) { - notScoped { - name - } - } - } - """ - end - defp all_query_with_company(id) do """ { @@ -497,17 +455,6 @@ defmodule Rajska.ObjectScopeAuthorizationTest do """ end - defp object_not_struct_query(id) do - """ - { - objectNotStructQuery(id: #{id}) { - name - email - } - } - """ - end - defp user_query_with_rule do """ { From c7ae4f9e26d06c2b20af6cdc9c9ef1df6b921fe7 Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Thu, 7 Nov 2019 14:15:26 -0300 Subject: [PATCH 05/12] Rename Rajska functions context_authorized? and has_context_access? to context_role_authorized? and context_user_authorized? --- lib/middlewares/object_authorization.ex | 2 +- lib/middlewares/object_scope_authorization.ex | 2 +- lib/middlewares/query_authorization.ex | 2 +- lib/middlewares/query_scope_authorization.ex | 2 +- lib/rajska.ex | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/middlewares/object_authorization.ex b/lib/middlewares/object_authorization.ex index 282963e..cad6c7a 100644 --- a/lib/middlewares/object_authorization.ex +++ b/lib/middlewares/object_authorization.ex @@ -94,7 +94,7 @@ defmodule Rajska.ObjectAuthorization do defp authorized?(nil, _, object), do: raise "No meta authorize defined for object #{inspect object.identifier}" defp authorized?(permission, context, _object) do - Rajska.apply_auth_mod(context, :context_authorized?, [context, permission]) + Rajska.apply_auth_mod(context, :context_role_authorized?, [context, permission]) end defp put_result(true, fields, resolution, _type), do: find_associations(fields, resolution) diff --git a/lib/middlewares/object_scope_authorization.ex b/lib/middlewares/object_scope_authorization.ex index 6af4941..dc00bfc 100644 --- a/lib/middlewares/object_scope_authorization.ex +++ b/lib/middlewares/object_scope_authorization.ex @@ -156,7 +156,7 @@ defmodule Rajska.ObjectScopeAuthorization do end defp authorized?(context, scoped_struct, rule) do - Rajska.apply_auth_mod(context, :has_user_access?, [context.current_user, scoped_struct, rule]) + Rajska.apply_auth_mod(context, :context_user_authorized?, [context, scoped_struct, 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 5743b74..080d0ec 100644 --- a/lib/middlewares/query_authorization.ex +++ b/lib/middlewares/query_authorization.ex @@ -44,7 +44,7 @@ defmodule Rajska.QueryAuthorization do validate_permission!(context, permission) context - |> Rajska.apply_auth_mod(:context_authorized?, [context, permission]) + |> Rajska.apply_auth_mod(:context_role_authorized?, [context, permission]) |> update_result(resolution) |> QueryScopeAuthorization.call(config) end diff --git a/lib/middlewares/query_scope_authorization.ex b/lib/middlewares/query_scope_authorization.ex index 1c20fa1..1d74e34 100644 --- a/lib/middlewares/query_scope_authorization.ex +++ b/lib/middlewares/query_scope_authorization.ex @@ -116,7 +116,7 @@ defmodule Rajska.QueryScopeAuthorization do defp has_user_access?(scoped_struct_fields, scope, context, rule, _optional) do scoped_struct = scope.__struct__(scoped_struct_fields) - Rajska.apply_auth_mod(context, :has_user_access?, [context.current_user, scoped_struct, rule]) + Rajska.apply_auth_mod(context, :context_user_authorized?, [context, scoped_struct, rule]) end defp update_result(true, resolution), do: resolution diff --git a/lib/rajska.ex b/lib/rajska.ex index b2cfb79..ebaadb7 100644 --- a/lib/rajska.ex +++ b/lib/rajska.ex @@ -116,17 +116,17 @@ defmodule Rajska do |> super_role?() end - def context_authorized?(context, allowed_role) do + def context_role_authorized?(context, allowed_role) do context |> get_current_user() |> get_user_role() |> role_authorized?(allowed_role) end - def has_context_access?(context, scope, {scope_field, field_value}, rule) do + def context_user_authorized?(context, scoped_struct, rule) do context |> get_current_user() - |> has_user_access?(scope.__struct__([{scope_field, field_value}]), rule) + |> has_user_access?(scoped_struct, rule) end defoverridable Authorization From 60d1a7efe3af70953acecb21dabd7ee558462b99 Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Thu, 7 Nov 2019 14:16:42 -0300 Subject: [PATCH 06/12] Refactor field authorization: pass entire object to has_user_access?/3 and replace scope_by and scope_field_by metas by scope? and scope_field? --- lib/middlewares/field_authorization.ex | 34 ++++++--------- test/middlewares/field_authorization_test.exs | 42 ++++++------------- 2 files changed, 25 insertions(+), 51 deletions(-) diff --git a/lib/middlewares/field_authorization.ex b/lib/middlewares/field_authorization.ex index 0cd8441..5f01374 100644 --- a/lib/middlewares/field_authorization.ex +++ b/lib/middlewares/field_authorization.ex @@ -32,14 +32,14 @@ defmodule Rajska.FieldAuthorization do def call(resolution, [object: %Type.Object{fields: fields} = object, field: field]) do field_private? = fields[field] |> Type.meta(:private) |> field_private?(resolution.source) - scope_by = get_scope_by_field!(object, field_private?) + scope? = get_scope!(object) 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, rule) + |> authorized?(scope? && field_private?, resolution.source, rule) |> put_result(resolution, field) end @@ -47,30 +47,22 @@ defmodule Rajska.FieldAuthorization do defp field_private?(private, source) when is_function(private), do: private.(source) defp field_private?(_private, _source), do: false - defp get_scope_by_field!(_object, false), do: :ok + defp get_scope!(object) do + scope? = Type.meta(object, :scope?) + scope_field? = Type.meta(object, :scope_field?) - defp get_scope_by_field!(object, _private) do - 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" + case {scope?, scope_field?} do + {nil, nil} -> true + {nil, scope_field?} -> scope_field? + {scope?, nil} -> scope? + {_, _} -> raise "Error in #{inspect object.identifier}. If scope_field? is defined, then scope? must not be defined" end end - defp authorized?(_context, false, _scope_by, _source, _rule), do: true - - 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, false, _source, _rule), do: true - 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)}" + defp authorized?(context, true, source, rule) do + Rajska.apply_auth_mod(context, :context_user_authorized?, [context, source, rule]) end defp put_result(true, resolution, _field), do: resolution diff --git a/test/middlewares/field_authorization_test.exs b/test/middlewares/field_authorization_test.exs index 87ee38b..f6515aa 100644 --- a/test/middlewares/field_authorization_test.exs +++ b/test/middlewares/field_authorization_test.exs @@ -69,14 +69,10 @@ defmodule Rajska.FieldAuthorizationTest do field :get_both_scopes, :both_scopes do resolve fn _args, _ -> {:ok, %{phone: "123456"}} end end - - field :not_struct, :user do - resolve fn _args, _ -> {:ok, %{id: 1}} end - end end object :user do - meta :scope_by, :id + meta :scope?, true field :name, :string field :is_email_public, :boolean @@ -87,19 +83,21 @@ defmodule Rajska.FieldAuthorizationTest do end object :field_scope_user do - meta :scope_field_by, :id + meta :scope_field?, true field :name, :string field :phone, :string, meta: [private: true] end object :not_scoped do + meta :scope?, false + field :phone, :string, meta: [private: true] end object :both_scopes do - meta :scope_by, :id - meta :scope_field_by, :id + meta :scope?, true + meta :scope_field?, false field :phone, :string, meta: [private: true] end @@ -175,24 +173,19 @@ defmodule Rajska.FieldAuthorizationTest do 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 + test "Works when scoping is disabled" do + {:ok, result} = Absinthe.run("{ getNotScoped { phone } }", __MODULE__.Schema, context(:user, 2)) + + assert %{data: %{"getNotScoped" => data}} = result + refute Map.has_key?(result, :errors) 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 -> + assert_raise RuntimeError, ~r/Error in :both_scopes. If scope_field\? is defined, then scope\? must not be defined/, fn -> Absinthe.run("{ getBothScopes { phone } }", __MODULE__.Schema, context(:user, 2)) end end - test "Raises when source object is not a struct" do - assert_raise RuntimeError, ~r/Expected a Struct for source object in field \"phone\", got %{id: 1}/, fn -> - Absinthe.run(not_struct_query(), __MODULE__.Schema, context(:user, 2)) - end - end - defp get_user_query(id, is_email_public) do """ { @@ -227,16 +220,5 @@ defmodule Rajska.FieldAuthorizationTest do """ end - defp not_struct_query do - """ - { - notStruct { - name - phone - } - } - """ - end - defp context(role, id), do: [context: %{current_user: %{role: role, id: id}}] end From 4c0c570fa28b1b6b4b76d2a3bf7ef7e776fa7b04 Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Thu, 7 Nov 2019 14:24:20 -0300 Subject: [PATCH 07/12] Remove field_authorized? function from authorization behaviour and improve documentation --- README.md | 8 +++----- lib/authorization.ex | 9 +-------- lib/middlewares/field_authorization.ex | 6 ++---- lib/rajska.ex | 2 +- test/middlewares/field_authorization_test.exs | 2 +- 5 files changed, 8 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ef5ef1c..33407e0 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?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3) 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) and [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3), but you can override them with your application needs. ```elixir defmodule Authorization do @@ -258,16 +258,14 @@ end ### Field Authorization -Authorizes Absinthe's object [field](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4) according to the result of the [field_authorized?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:field_authorized?/3) function, which receives the user role, the meta `scope_by` atom defined in the object schema and the `source` object that is resolving the field. +Authorizes Absinthe's object [field](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4) according to the result of the [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3) function, which receives the user role, the `source` object that is resolving the field and the object rule. Usage: -[Create your Authorization module and add it and FieldAuthorization to your Absinthe.Schema](#usage). Then add the meta `scope_by` to an object and meta `private` to your sensitive fields: +[Create your Authorization module and add it and FieldAuthorization to your Absinthe.Schema](#usage). ```elixir object :user do - meta :scope_by, :id - field :name, :string field :is_email_public, :boolean diff --git a/lib/authorization.ex b/lib/authorization.ex index a0dc226..d85a8dd 100644 --- a/lib/authorization.ex +++ b/lib/authorization.ex @@ -17,13 +17,7 @@ defmodule Rajska.Authorization do @callback role_authorized?(current_user_role, allowed_role :: role) :: boolean() - @callback field_authorized?(current_user_role, scope_by :: atom(), source :: map()) :: boolean() - - @callback has_user_access?( - current_user, - scoped_struct :: struct(), - rule :: any() - ) :: boolean() + @callback has_user_access?(current_user, scoped_struct :: struct(), rule :: any()) :: boolean() @callback unauthorized_msg(resolution :: Resolution.t()) :: String.t() @@ -31,7 +25,6 @@ defmodule Rajska.Authorization do get_user_role: 1, not_scoped_roles: 0, role_authorized?: 2, - field_authorized?: 3, has_user_access?: 3, unauthorized_msg: 1 end diff --git a/lib/middlewares/field_authorization.ex b/lib/middlewares/field_authorization.ex index 5f01374..8d8724f 100644 --- a/lib/middlewares/field_authorization.ex +++ b/lib/middlewares/field_authorization.ex @@ -2,16 +2,14 @@ defmodule Rajska.FieldAuthorization do @moduledoc """ Absinthe middleware to ensure field permissions. - Authorizes Absinthe's object [field](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4) according to the result of the `c:Rajska.Authorization.field_authorized?/3` function, which receives the user role, the meta `scope_by` atom defined in the object schema and the `source` object that is resolving the field. + Authorizes Absinthe's object [field](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4) according to the result of the `c:Rajska.Authorization.has_user_access?/3` function, which receives the user role, the `source` object that is resolving the field and the object rule. ## Usage - [Create your Authorization module and add it and FieldAuthorization to your Absinthe.Schema](https://hexdocs.pm/rajska/Rajska.html#module-usage). Then add the meta `scope_by` to an object and meta `private` to your sensitive fields: + [Create your Authorization module and add it and FieldAuthorization to your Absinthe.Schema](https://hexdocs.pm/rajska/Rajska.html#module-usage). ```elixir object :user do - meta :scope_by, :id - field :name, :string field :is_email_public, :boolean diff --git a/lib/rajska.ex b/lib/rajska.ex index ebaadb7..de0ca04 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?/3` 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` and `c:Rajska.Authorization.has_user_access?/3`, but you can override them with your application needs. ```elixir defmodule Authorization do diff --git a/test/middlewares/field_authorization_test.exs b/test/middlewares/field_authorization_test.exs index f6515aa..8cc03b5 100644 --- a/test/middlewares/field_authorization_test.exs +++ b/test/middlewares/field_authorization_test.exs @@ -157,7 +157,7 @@ defmodule Rajska.FieldAuthorizationTest do assert is_binary(data["phone"]) end - test "Works when defining scope_field_by" do + test "Works when defining scope_field?" do user = %{role: :user, id: 1} get_user_query = get_field_scope_user(2) From 835388c212c7e7512230cd400af941499d38d15c Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Thu, 7 Nov 2019 14:40:58 -0300 Subject: [PATCH 08/12] Rename unauthorized_msg/1 function to unauthorized_message --- lib/authorization.ex | 4 ++-- lib/middlewares/query_authorization.ex | 2 +- lib/rajska.ex | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/authorization.ex b/lib/authorization.ex index d85a8dd..bb6eb10 100644 --- a/lib/authorization.ex +++ b/lib/authorization.ex @@ -19,12 +19,12 @@ defmodule Rajska.Authorization do @callback has_user_access?(current_user, scoped_struct :: struct(), rule :: any()) :: boolean() - @callback unauthorized_msg(resolution :: Resolution.t()) :: String.t() + @callback unauthorized_message(resolution :: Resolution.t()) :: String.t() @optional_callbacks get_current_user: 1, get_user_role: 1, not_scoped_roles: 0, role_authorized?: 2, has_user_access?: 3, - unauthorized_msg: 1 + unauthorized_message: 1 end diff --git a/lib/middlewares/query_authorization.ex b/lib/middlewares/query_authorization.ex index 080d0ec..8130f17 100644 --- a/lib/middlewares/query_authorization.ex +++ b/lib/middlewares/query_authorization.ex @@ -71,6 +71,6 @@ defmodule Rajska.QueryAuthorization do defp update_result(true, resolution), do: resolution defp update_result(false, %{context: context} = resolution) do - Resolution.put_result(resolution, {:error, Rajska.apply_auth_mod(context, :unauthorized_msg, [resolution])}) + Resolution.put_result(resolution, {:error, Rajska.apply_auth_mod(context, :unauthorized_message, [resolution])}) end end diff --git a/lib/rajska.ex b/lib/rajska.ex index de0ca04..17a4786 100644 --- a/lib/rajska.ex +++ b/lib/rajska.ex @@ -107,7 +107,7 @@ defmodule Rajska do super_user? || owner? end - def unauthorized_msg(_resolution), do: "unauthorized" + def unauthorized_message(_resolution), do: "unauthorized" def super_user?(context) do context From e470e23693b6aea59b95993781288d6805fced08 Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Thu, 7 Nov 2019 15:49:57 -0300 Subject: [PATCH 09/12] Add context_role_authorized?/2 and context_user_authorized?/3 to authorization protocol --- lib/authorization.ex | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/authorization.ex b/lib/authorization.ex index bb6eb10..9d772d0 100644 --- a/lib/authorization.ex +++ b/lib/authorization.ex @@ -8,8 +8,11 @@ defmodule Rajska.Authorization do @type current_user :: any() @type role :: atom() @type current_user_role :: role + @type context :: map() + @type scoped_struct :: struct() + @type rule :: atom() - @callback get_current_user(context :: map()) :: current_user + @callback get_current_user(context) :: current_user @callback get_user_role(current_user) :: role @@ -17,14 +20,20 @@ defmodule Rajska.Authorization do @callback role_authorized?(current_user_role, allowed_role :: role) :: boolean() - @callback has_user_access?(current_user, scoped_struct :: struct(), rule :: any()) :: boolean() + @callback has_user_access?(current_user, scoped_struct, rule) :: boolean() @callback unauthorized_message(resolution :: Resolution.t()) :: String.t() + @callback context_role_authorized?(context, allowed_role :: role) :: boolean() + + @callback context_user_authorized?(context, scoped_struct, rule) :: boolean() + @optional_callbacks get_current_user: 1, get_user_role: 1, not_scoped_roles: 0, role_authorized?: 2, has_user_access?: 3, - unauthorized_message: 1 + unauthorized_message: 1, + context_role_authorized?: 2, + context_user_authorized?: 3 end From 46a2405f4292de88062acc3a9c396641109817a7 Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Thu, 7 Nov 2019 15:50:58 -0300 Subject: [PATCH 10/12] Replace Enum.filter by Enum.reject in query scope authorization module --- lib/middlewares/query_scope_authorization.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/middlewares/query_scope_authorization.ex b/lib/middlewares/query_scope_authorization.ex index 1d74e34..4a95e88 100644 --- a/lib/middlewares/query_scope_authorization.ex +++ b/lib/middlewares/query_scope_authorization.ex @@ -85,7 +85,7 @@ defmodule Rajska.QueryScopeAuthorization do arg_fields |> Enum.map(& get_scoped_struct_field(arguments_source, &1, optional, resolution.definition.name)) - |> Enum.filter(& !!&1) + |> Enum.reject(&is_nil/1) |> has_user_access?(scope, resolution.context, rule, optional) |> update_result(resolution) end From 78c318bab484b80c6a566fe53a338c9d8e118ceb Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Thu, 7 Nov 2019 15:52:59 -0300 Subject: [PATCH 11/12] Move scope? check to authorized? function in object scope authorization --- lib/middlewares/object_scope_authorization.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/middlewares/object_scope_authorization.ex b/lib/middlewares/object_scope_authorization.ex index dc00bfc..1c588d8 100644 --- a/lib/middlewares/object_scope_authorization.ex +++ b/lib/middlewares/object_scope_authorization.ex @@ -120,7 +120,7 @@ defmodule Rajska.ObjectScopeAuthorization do default_rule = Rajska.apply_auth_mod(context, :default_rule) rule = Type.meta(type, :rule) || default_rule - case !scope? || authorized?(context, root_value, rule) do + case authorized?(scope?, context, root_value, rule) do true -> %{result | fields: walk_result(fields, context)} false -> Map.put(result, :errors, [error(emitter)]) end @@ -155,7 +155,9 @@ defmodule Rajska.ObjectScopeAuthorization do end end - defp authorized?(context, scoped_struct, rule) do + defp authorized?(false, _context, _scoped_struct, _rule), do: true + + defp authorized?(true, context, scoped_struct, rule) do Rajska.apply_auth_mod(context, :context_user_authorized?, [context, scoped_struct, rule]) end From c04b4b1723564cb0d6f5babbd0f83596bfd96213 Mon Sep 17 00:00:00 2001 From: Rafael Scheffer Date: Thu, 7 Nov 2019 15:54:20 -0300 Subject: [PATCH 12/12] Fix field authorization documentation --- README.md | 2 +- lib/middlewares/field_authorization.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 33407e0..046ef2e 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ end ### Field Authorization -Authorizes Absinthe's object [field](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4) according to the result of the [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3) function, which receives the user role, the `source` object that is resolving the field and the object rule. +Authorizes Absinthe's object [field](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4) according to the result of the [has_user_access?/3](https://hexdocs.pm/rajska/Rajska.Authorization.html#c:has_user_access?/3) function, which receives the user role, the `source` object that is resolving the field and the field rule. Usage: diff --git a/lib/middlewares/field_authorization.ex b/lib/middlewares/field_authorization.ex index 8d8724f..2c09d4d 100644 --- a/lib/middlewares/field_authorization.ex +++ b/lib/middlewares/field_authorization.ex @@ -2,7 +2,7 @@ defmodule Rajska.FieldAuthorization do @moduledoc """ Absinthe middleware to ensure field permissions. - Authorizes Absinthe's object [field](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4) according to the result of the `c:Rajska.Authorization.has_user_access?/3` function, which receives the user role, the `source` object that is resolving the field and the object rule. + Authorizes Absinthe's object [field](https://hexdocs.pm/absinthe/Absinthe.Schema.Notation.html#field/4) according to the result of the `c:Rajska.Authorization.has_user_access?/3` function, which receives the user role, the `source` object that is resolving the field and the field rule. ## Usage