Skip to content

Commit

Permalink
Merge 701d2df into 21321cd
Browse files Browse the repository at this point in the history
  • Loading branch information
rschef committed Oct 9, 2019
2 parents 21321cd + 701d2df commit b4bb212
Show file tree
Hide file tree
Showing 12 changed files with 302 additions and 141 deletions.
36 changes: 21 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 [permit: :user, scope: User, args: :id]
resolve &AccountsResolver.update_user/2
end

Expand All @@ -123,15 +123,21 @@ 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:
## Options

- `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, [: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.
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

Expand Down Expand Up @@ -228,9 +234,9 @@ 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, _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
```

Expand All @@ -244,11 +250,11 @@ 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, _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, _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
```

Expand Down
7 changes: 6 additions & 1 deletion lib/authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ 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,
scope :: module(),
{field :: any(), field_value :: any()},
rule :: any()
) :: boolean()

@callback unauthorized_msg(resolution :: Resolution.t()) :: String.t()

Expand Down
30 changes: 15 additions & 15 deletions lib/middlewares/object_scope_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ 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, _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
```
Expand All @@ -61,11 +61,11 @@ 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_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, _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
```
Expand All @@ -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, _field_value}, :read_only), do: true
def has_user_access?(%{id: user_id}, Wallet, {_field, _field_value}, :default), do: false
end
```
Expand Down Expand Up @@ -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, 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?(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])
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
Expand Down
4 changes: 2 additions & 2 deletions lib/middlewares/query_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 [permit: :user, scope: User, args: :id]
resolve &AccountsResolver.update_user/2
end
Expand All @@ -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
Expand Down
101 changes: 53 additions & 48 deletions lib/middlewares/query_scope_authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 [permit: :user, scope: User, args: :id]
resolve &AccountsResolver.update_user/2
end
Expand All @@ -33,22 +33,27 @@ 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.
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.
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.
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, [: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.
## Options
All the following options are sent to `c:Rajska.Authorization.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 `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
Expand All @@ -59,61 +64,61 @@ 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} | 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(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(resolution, 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_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)
defp get_arguments_source!(%Resolution{arguments: args}, _scope), do: args

def apply_scope_authorization(%Resolution{definition: %{name: name}}, nil, _scoped_struct, _) do
raise "Error in query #{name}: no argument found in middleware Scope Authorization"
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

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_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])
end

defp update_result(true, resolution), do: resolution
Expand Down
11 changes: 7 additions & 4 deletions lib/rajska.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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, scope, {field, field_value}, 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 === scope)
&& (field === :id)
&& (user_id === field_value)

super_user? || owner?
end
Expand All @@ -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, scope, {scope_field, field_value}, rule) do
context
|> get_current_user()
|> has_user_access?(scoped_struct, field_value, rule)
|> has_user_access?(scope, {scope_field, field_value}, rule)
end

defoverridable Authorization
Expand Down
Loading

0 comments on commit b4bb212

Please sign in to comment.