├── .formatter.exs ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── config └── config.exs ├── lib ├── canada.ex └── canada │ └── can.ex ├── mix.exs └── test ├── canada_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | canada-*.tar 24 | 25 | # Temporary files for e.g. tests. 26 | /tmp 27 | 28 | # Misc. 29 | mix.lock 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.6 4 | - 1.5 5 | - 1.4 6 | - 1.3 7 | otp_release: 8 | - 21.0 9 | - 20.3 10 | - 19.3 11 | - 18.3 12 | matrix: 13 | exclude: 14 | - elixir: 1.6 15 | otp_release: 18.3 16 | - elixir: 1.5 17 | otp_release: 21.0 18 | - elixir: 1.4 19 | otp_release: 21.0 20 | - elixir: 1.3 21 | otp_release: 21.0 22 | - elixir: 1.3 23 | otp_release: 20.3 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v2.0.0 - 2019-05-07 4 | 5 | - Use @fallback_to_any to provide a default value of `false` for permission checks that don't match any implementations. ([@samullen](https://github.com/samullen) [#26](https://github.com/jarednorman/canada/pull/26)) 6 | 7 | ## v1.0.2 - 2017-07-23 8 | 9 | - Probably fixed more warnings 10 | 11 | ## v1.0.1 - 2016-08-23 12 | 13 | - I think just fixed warnings 14 | 15 | ## v1.0.0 - 2014-09-07 16 | 17 | - Reversed version number 18 | 19 | ## v0.0.1 - 2014-08-25 20 | 21 | - Initial release 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Jared Norman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Canada: _Define you some permissions_ 2 | ===================================== 3 | 4 | [![Module Version](https://img.shields.io/hexpm/v/canada.svg)](https://hex.pm/packages/canada) 5 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/canada/) 6 | [![Total Download](https://img.shields.io/hexpm/dt/canada.svg)](https://hex.pm/packages/canada) 7 | [![License](https://img.shields.io/hexpm/l/canada.svg)](https://github.com/jarednorman/canada/blob/master/LICENSE) 8 | [![Last Updated](https://img.shields.io/github/last-commit/jarednorman/canada.svg)](https://github.com/jarednorman/canada/commits/master) 9 | 10 | > **NOTE:** If you're concerned by the fact that this repository has very 11 | > little activity, don't be. The functionality this package provides is very 12 | > simple, it has no dependencies, and the Elixir language hasn't changed in any 13 | > way that would break it. It still works just as well as when I first wrote 14 | > it. :smiley: 15 | 16 | Canada provides a friendly interface for making easy use of 17 | [Elixir](http://elixir-lang.org/)'s excellent pattern matching to create 18 | readable declarative permission rules. 19 | 20 | If you're looking for something that fills more of what CanCan would provide 21 | you in a Rails application you should have a look at 22 | [Canary](https://github.com/cpjk/canary) which adds Ecto/Plug support. 23 | 24 | Installation 25 | ------------ 26 | 27 | Add it to your deps list in your `mix.exs` if you want the latest release? 28 | 29 | ```elixir 30 | defp deps do 31 | [ 32 | {:canada, "~> 2.0"} 33 | ] 34 | end 35 | ``` 36 | 37 | Or you want the latest greatest master? 38 | 39 | ```elixir 40 | defp deps do 41 | [ 42 | {:canada, github: "jarednorman/canada"} 43 | ] 44 | end 45 | ``` 46 | 47 | Becoming Canadian 48 | ----------------- 49 | 50 | Becoming Canadian is easy. Presumably you have some kind of resource like a 51 | user, and probably some kind of resource that belongs to users. Let's call that 52 | hypothetical resource a "post". Let's say they're structs. 53 | 54 | ```elixir 55 | defmodule User do 56 | defstruct id: nil, name: nil, admin: false 57 | end 58 | 59 | defmodule Post do 60 | defstruct user_id: nil, content: nil 61 | end 62 | ``` 63 | 64 | To make use of Canada, you need to implement the `Canada.Can` protocol 65 | (defining whatever rules you need) for the "subject" resource (your User struct 66 | in this case). 67 | 68 | ```elixir 69 | defimpl Canada.Can, for: User do 70 | def can?(%User{id: user_id}, action, %Post{user_id: user_id}) 71 | when action in [:update, :read, :destroy, :touch], do: true 72 | 73 | def can?(%User{admin: admin}, action, _) 74 | when action in [:update, :read, :destroy, :touch], do: admin 75 | 76 | def can?(%User{}, :create, Post), do: true 77 | end 78 | ``` 79 | 80 | With this in place, you're good to start testing permissions wherever you need 81 | to, just remember to import the can? macro. 82 | 83 | ```elixir 84 | import Canada, only: [can?: 2] 85 | 86 | if some_user |> can? read(some_post) do 87 | # render the post 88 | else 89 | # sorry (raise a 403) 90 | end 91 | ``` 92 | 93 | A note from the author 94 | ---------------------- 95 | 96 | This is very much what happened when I said to myself, "I want the thing I had 97 | in Ruby, but in Elixir." I would be entirely unsurprised if myself or someone 98 | else comes up with a more "functional" solution. That said, permissions are 99 | necessarily a matter that governs conditional logic, so I currently see this as 100 | a reasonable solution. 101 | 102 | ## Copyright and License 103 | 104 | Copyright (c) 2014 Jared Norman 105 | 106 | This software is licensed under [the MIT license](./LICENSE.md). 107 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, 14 | # level: :info, 15 | # format: "$time $metadata[$level] $levelpad$message\n" 16 | 17 | # It is also possible to import configuration files, relative to this 18 | # directory. For example, you can emulate configuration per environment 19 | # by uncommenting the line below and defining dev.exs, test.exs and such. 20 | # Configuration from the imported file will override the ones defined 21 | # here (which is why it is important to import them last). 22 | # 23 | # import_config "#{Mix.env}.exs" 24 | -------------------------------------------------------------------------------- /lib/canada.ex: -------------------------------------------------------------------------------- 1 | defmodule Canada do 2 | defmacro can?(subject, {action, _, [argument]}) do 3 | quote do 4 | Canada.Can.can? unquote(subject), unquote(action), unquote(argument) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/canada/can.ex: -------------------------------------------------------------------------------- 1 | defprotocol Canada.Can do 2 | @fallback_to_any true 3 | 4 | @doc "Evaluates permissions" 5 | def can?(subject, action, resource) 6 | end 7 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Canada.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/jarednorman/canada" 5 | @version "2.0.0" 6 | 7 | def project do 8 | [ 9 | app: :canada, 10 | version: @version, 11 | elixir: "~> 1.0", 12 | consolidate_protocols: Mix.env() != :test, 13 | package: package(), 14 | deps: deps(), 15 | docs: docs() 16 | ] 17 | end 18 | 19 | def package do 20 | [ 21 | description: "A DSL for declarative permissions", 22 | maintainers: ["Jared Norman"], 23 | contributors: ["Jared Norman"], 24 | licenses: ["MIT"], 25 | links: %{ 26 | GitHub: @source_url 27 | } 28 | ] 29 | end 30 | 31 | def application do 32 | [ 33 | applications: [] 34 | ] 35 | end 36 | 37 | defp deps do 38 | [ 39 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 40 | ] 41 | end 42 | 43 | defp docs do 44 | [ 45 | extras: [ 46 | "CHANGELOG.md", 47 | "LICENSE.md": [title: "License"], 48 | "README.md": [title: "Readme"] 49 | ], 50 | main: "readme", 51 | source_url: @source_url, 52 | source_ref: "v#{@version}", 53 | api_reference: false, 54 | formatters: ["html"] 55 | ] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/canada_test.exs: -------------------------------------------------------------------------------- 1 | defmodule User do 2 | defstruct admin: false, id: nil, verified: false 3 | end 4 | 5 | defmodule Post do 6 | defstruct user_id: nil 7 | end 8 | 9 | defimpl Canada.Can, for: User do 10 | def can?(%User{id: user_id}, action, %Post{user_id: user_id}) 11 | when action in [:update, :read, :destroy, :touch], do: true 12 | 13 | def can?(%User{admin: admin}, action, _) 14 | when action in [:update, :read, :destroy, :touch], do: admin 15 | 16 | def can?(%User{verified: verified}, :create, Post), do: verified 17 | end 18 | 19 | defimpl Canada.Can, for: Any do 20 | def can?(_subject, _action, _resource), do: false 21 | end 22 | 23 | defmodule CanadaTest do 24 | use ExUnit.Case 25 | import Canada, only: [can?: 2] 26 | 27 | def admin_user(), do: %User{admin: true, id: 1, verified: true} 28 | def user(), do: %User{id: 2, verified: true} 29 | def other_user(), do: %User{id: 3} 30 | 31 | def post(), do: %Post{user_id: user().id} 32 | 33 | test "it identifies permissions based on custom actions" do 34 | assert admin_user() |> can?(touch(post())) 35 | assert user() |> can?(touch(post())) 36 | refute other_user() |> can?(touch(post())) 37 | end 38 | 39 | test "it identifies whether subject can read a resource" do 40 | assert admin_user() |> can?(read(post())) 41 | assert user() |> can?(read(post())) 42 | refute other_user() |> can?(read(post())) 43 | end 44 | 45 | test "it identifies whether a subject can update a resource" do 46 | assert admin_user() |> can?(update(post())) 47 | assert user() |> can?(update(post())) 48 | refute other_user() |> can?(update(post())) 49 | end 50 | 51 | test "it identifies whether a subject can destroy a resource" do 52 | assert admin_user() |> can?(destroy(post())) 53 | assert user() |> can?(destroy(post())) 54 | refute other_user() |> can?(destroy(post())) 55 | end 56 | 57 | test "it identifies whether a subject can create a type of resource" do 58 | assert admin_user() |> can?(create(Post)) 59 | assert user() |> can?(create(Post)) 60 | refute other_user() |> can?(create(Post)) 61 | end 62 | 63 | describe "authorizing 'Any' other resource" do 64 | test "accepts any other resource" do 65 | refute nil |> can?(touch(Post)) 66 | refute "" |> can?(touch(Post)) 67 | refute %{} |> can?(touch(Post)) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------