Giter Site home page Giter Site logo

subvisual / dictator Goto Github PK

View Code? Open in Web Editor NEW
78.0 7.0 4.0 92 KB

Dictates what your users see. Plug-based authorization.

License: ISC License

Elixir 94.92% Shell 5.08%
elixir plug authorization authorization-middleware middleware hacktoberfest

dictator's Introduction

Dictator

Dictator is a plug-based authorization mechanism.

Dictate what your users can access in fewer than 10 lines of code:

# config/config.exs
config :dictator, repo: Client.Repo

# lib/client_web/controllers/thing_controller.ex
defmodule ClientWeb.ThingController do
  use ClientWeb, :controller

  plug Dictator

  # ...
end

# lib/client_web/policies/thing.ex
defmodule ClientWeb.Policies.Thing do
  alias Client.Context.Thing

  use Dictator.Policies.BelongsTo, for: Thing
end

And that's it! Just like that your users can edit, see and delete their own Things but not Things belonging to other users.


Installation

First, you need to add :dictator to your list of dependencies on your mix.exs:

def deps do
  [{:dictator, "~> 1.1"}]
end

Usage

For in-depth usage, refer to this blog post.

To authorize your users, just add in your controller:

defmodule ClientWeb.ThingController do
  use ClientWeb, :controller

  plug Dictator

  # ...
end

Alternatively, you can also do it at the router level:

defmodule ClientWeb.Router do
  pipeline :authorised do
    plug Dictator
  end
end

That plug will automatically look for a ClientWeb.Policies.Thing module, which should use Dictator.Policy. It is a simple module that should implement can?/3. It receives the current user, the action it is trying to perform and a map containing the conn.params, the resource being acccessed and any options passed when plug-ing Dictator.

In lib/client_web/policies/thing.ex:

defmodule ClientWeb.Policies.Thing do
  alias Client.Context.Thing

  use Dictator.Policies.EctoSchema, for: Thing

  # User can edit, update, delete and show their own things
  def can?(%User{id: user_id}, action, %{resource: %Thing{user_id: user_id}})
    when action in [:edit, :update, :delete, :show], do: true

  # Any user can index, new and create things
  def can?(_, action, _) when action in [:index, :new, :create], do: true

  # Users can't do anything else (users editing, updating, deleting and showing)
  # on things they don't own
  def can?(_, _, _), do: false
end

This exact scenario is, in fact, so common that already comes bundled as Dictator.Policies.BelongsTo. This is equivalent to the previous definition:

defmodule ClientWeb.Policies.Thing do
  alias Client.Context.Thing

  use Dictator.Policies.BelongsTo, for: Thing
end

IMPORTANT: Dictator assumes you have your current user in your conn.assigns. See our demo app for an example on integrating with guardian.


Custom Policies

Dictator comes bundled with three different types of policies:

  • Dictator.Policies.EctoSchema: most common behaviour. When you use it, Dictator will try to call a load_resource/1 function by passing the HTTP params. This function is overridable, along with can?/3
  • Dictator.Policies.BelongsTo: abstraction on top of Dictator.Policies.EctoSchema, for the most common use case: when a user wants to read and write resources they own, but read access is provided to everyone else. This policy makes some assumptions regarding your implementation, all of those highly customisable.
  • Dictator.Policy: most basic policy possible. use it if you don't want to load resources from the database (e.g to check if a user has an is_admin field set to true)

Dictator.Policies.EctoSchema

Most common behaviour. When you use it, Dictator will try to call a load_resource/1 function by passing the HTTP params. This allows you to access the resource in the third parameter of can/3?. The load_resource/1 function is overridable, along with can?/3.

Take the following example:

defmodule ClientWeb.Policies.Thing do
  alias Client.Context.Thing

  use Dictator.Policies.EctoSchema, for: Thing

  # User can edit, update, delete and show their own things
  def can?(%User{id: user_id}, action, %{resource: %Thing{user_id: user_id}})
    when action in [:edit, :update, :delete, :show], do: true

  # Any user can index, new and create things
  def can?(_, action, _) when action in [:index, :new, :create], do: true

  # Users can't do anything else (users editing, updating, deleting and showing)
  # on things they don't own
  def can?(_, _, _), do: false
end

In the example above, Dictator takes care of loading the Thing resource through the HTTP params. However, you might want to customise the way the resource is loaded. To do that, you should override the load_resource/1 function.

As an example:

defmodule ClientWeb.Policies.Thing do
  alias Client.Context.Thing

  use Dictator.Policies.EctoSchema, for: Thing

  def load_resource(%{"owner_id" => owner_id, "uuid" => uuid}) do
    ClientWeb.Repo.get_by(Thing, owner_id: owner_id, uuid: uuid)
  end

  def can?(_, action, _) when action in [:index, :show, :new, :create], do: true

  def can?(%{id: owner_id}, action, %{resource: %Thing{owner_id: owner_id}})
    when action in [:edit, :update, :delete],
    do: true

  def can?(_user, _action, _params), do: false
end

The following custom options are available:

  • key: defaults to :id, primary key of the resource being accessed.
  • repo: overrides the repo set by the config.

Dictator.Policies.BelongsTo

Policy definition commonly used in typical belongs_to associations. It is an abstraction on top of Dictator.Policies.EctoSchema.

This policy assumes the users can read (:show, :index, :new, :create) any information but only write (:edit, :update, :delete) their own.

As an example, in a typical Twitter-like application, a user has_many posts and a post belongs_to a user. You can define a policy to let users manage their own posts but read all others by doing the following:

defmodule MyAppWeb.Policies.Post do
  alias MyApp.{Post, User}

  use Dictator.Policies.EctoSchema, for: Post

  def can?(_, action, _) when action in [:index, :show, :new, :create], do: true

  def can?(%User{id: id}, action, %{resource: %Post{user_id: id}})
      when action in [:edit, :update, :delete],
      do: true

  def can?(_, _, _), do: false
end

This scenario is so common, it is abstracted completely through this module and you can simply use Dictator.Policies.BelongsTo, for: Post to make use of it. The following example is equivalent to the previous one:

defmodule MyAppWeb.Policies.Post do
  use Dictator.Policies.BelongsTo, for: MyApp.Post
end

The assumptions made are that:

  • your resource has a user_id foreign key (you can change this with the :foreign_key option)
  • your user has an id primary key (you can change this with the :owner_id option)

If your user has a uuid primary key and the post identifies the user through a :poster_id foreign key, you can do the following:

defmodule MyAppWeb.Policies.Post do
  use Dictator.Policies.BelongsTo, for: MyApp.Post,
    foreign_key: :poster_id, owner_id: :uuid
end

The key and repo options supported by Dictator.Policies.EctoSchema are also supported by Dictator.Policies.BelongsTo.

Plug Options

plug Dictator supports 3 options:

  • only/except: (optional) - actions subject to authorization.
  • policy: (optional, infers the policy) - policy to be used
  • resource_key: (optional, default: :current_user) - key to use in the conn.assigns to load the currently logged in resource.

Limitting the actions to be authorized

If you want to only limit authorization to a few actions you can use the :only or :except options when calling the plug in your controller:

defmodule ClientWeb.ThingController do
  use ClientWeb, :controller

  plug Dictator, only: [:create, :update, :delete]
  # plug Dictator, except: [:show, :index, :new, :edit]

  # ...
end

In both cases, all other actions will not go through the authorization plug and the policy will only be enforced for the create,update and delete actions.

Overriding the policy to be used

By default, the plug will automatically infer the policy to be used. MyWebApp.UserController would mean a MyWebApp.Policies.User policy to use.

However, by using the :policy option, that can be overriden

defmodule ClientWeb.ThingController do
  use ClientWeb, :controller

  plug Dictator, policy: MyPolicy

  # ...
end

Overriding the current user key

By default, the plug will automatically search for a current_user in the conn.assigns. You can change this behaviour by using the key option in the plug call. This will override the key option set in config.exs.

defmodule ClientWeb.ThingController do
  use ClientWeb, :controller

  plug Dictator, key: :current_organization

  # ...
end

Overriding the current user fetch strategy

By default, the plug will assume you want to search for the key set in the previous option in the conn.assigns. However, you may have it set in the session or want to use a custom strategy. You can change this behaviour by using the fetch_strategy option in the plug call. This will override the fetch_strategy option set in config.exs.

There are two strategies available by default:

  • Dictator.FetchStrategies.Assigns - fetches the given key from conn.assigns
  • Dictator.FetchStrategies.Session - fetches the given key from the session
defmodule ClientWeb.ThingController do
  use ClientWeb, :controller

  plug Dictator, fetch_strategy: Dictator.FetchStrategies.Session

  # ...
end

Configuration Options

Dictator supports three options to be placed in config/config.exs:

  • repo - default repo to be used by Dictator.Policies.EctoSchema. If not set, you need to define what repo to use in the policy through the :repo option.
  • key (optional, defaults to :key) - key to be used to find the current user in conn.assigns.
  • unauthorized_handler (optional, default: Dictator.UnauthorizedHandlers.Default) - module to call to handle unauthorisation errors.

Setting a default repo

Dictator.Policies.EctoSchema requires a repo to be set to load resource from.

It is recommended that you set it in config/config.exs:

config :dictator, repo: Client.Repo

If not configured, it must be provided in each policy. The repo option when use-ing the policy takes precedence. So you can also set a custom repo for certain resources:

defmodule ClientWeb.Policies.Thing do
  alias Client.Context.Thing
  alias Client.FunkyRepoForThings

  use Dictator.Policies.BelongsTo, for: Thing, repo: FunkyRepoForThings
end

Setting a default current user key

By default, the plug will automatically search for a current_user in the conn.assigns. The default value is :current_user but this can be overriden by changing the config:

config :dictator, key: :current_company

The value set by the key option when plugging Dictator overrides this one.

Setting the fetch strategy

By default, the plug will assume you want to search for the key set in the previous option in the conn.assigns. However, you may have it set in the session or want to use a custom strategy. You can change this behaviour across the whole application by setting the fetch_strategy key in the config.

There are two strategies available by default:

  • Dictator.FetchStrategies.Assigns - fetches the given key from conn.assigns
  • Dictator.FetchStrategies.Session - fetches the given key from the session
config :dictator, fetch_strategy: Dictator.FetchStrategies.Session

The value set by the key option when plugging Dictator overrides this one.

Setting the unauthorized handler

When a user does not have access to a given resource, an unauthorized handler is called. By default this is Dictator.UnauthorizedHandlers.Default which sends a simple 401 with the body set to "you are not authorized to do that".

You can also make use of the JSON API compatible Dictator.UnauthorizedHandlers.JsonApi or provide your own:

config :dictator, unauthorized_handler: MyUnauthorizedHandler

Contributing

Feel free to contribute.

If you found a bug, open an issue. You can also open a PR for bugs or new features. Your PRs will be reviewed and subject to our style guide and linters.

All contributions must follow the Code of Conduct and Subvisual's guides.

Setup

To clone and setup the repo:

git clone [email protected]:subvisual/dictator.git
cd dictator
bin/setup

And everything should automatically be installed for you.

To run the development server:

bin/server

Other projects

Not your cup of tea? ๐Ÿต Here are some other Elixir alternatives we like:

About

Dictator is maintained by Subvisual.

Subvisual logo

dictator's People

Contributors

frm avatar joseemds avatar marinho10 avatar naps62 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

dictator's Issues

Support LiveView

We're currently not supporting any type of authorisation for LiveView.

Should we add a plug that makes use of conn.private[:phoenix_live_view] and calls certain policies?

What about inferring the action and resource? How could that be achieved?

Should we add a new Dictator.Policies.LiveView module since it will essentially be a different type of policy?

Allow the unauthorised handler to be configured when plugged

Currently, you can only configure the unauthorised handler globally by setting it in config/*.exs files.

However, it makes sense to have the following scenario:

pipeline :api do
  plug Dictator, unauthorized_handler: JSONApiHandler
end

pipeline :browser do
  plug Dictator, unauthorized_handler: Render404HTML
end

We should add the unauthorized_handler to the opts sent to the plug.

Add a handler for resources not found

If a resource is not found, we're currently calling the policy with nil for the resource value. This means that a lot of times, the policies will have a function branch pattern matching on nil and calling a FallbackController or specific handler.

We should add that as an alternative, similar to the current unauthorised handler.

Since both the new handler and the unauthorised one are plugs, we should also update the README with an example using a MyAppWeb.FallbackController as the handler

Add view helpers

Sometimes we want to conditionally render elements based on the user having permissions or not. We should implement this somehow.

A basic idea:

= if has_permissions?(@conn, :show, @post)

Any thoughts on this?

Support GraphQL

Currently we depend on a conn being present and this isn't the case when handling GraphQL.

In these scenarios how should we act? Should we allow the policy to be called directly? With what parameters?

Update documentation

The functionality has drastically changed with the newest additions.

  • Update README
  • Write module and function docs

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.