├── test ├── test_helper.exs └── permission_ex_test.exs ├── .gitignore ├── lib ├── test │ └── structs.ex └── permission_ex.ex ├── config └── config.exs ├── mix.lock ├── mix.exs └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /doc 5 | erl_crash.dump 6 | *.ez 7 | -------------------------------------------------------------------------------- /test/permission_ex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PermissionExTest do 2 | use ExUnit.Case 3 | doctest PermissionEx 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/test/structs.ex: -------------------------------------------------------------------------------- 1 | if Mix.env() == :test do 2 | defmodule PermissionEx.Test.Structs.User do 3 | @moduledoc false 4 | # @derive [Poison.Encoder] 5 | defstruct name: nil 6 | end 7 | 8 | defmodule PermissionEx.Test.Structs.Page do 9 | @moduledoc false 10 | # @derive [Poison.Encoder] 11 | defstruct action: nil 12 | end 13 | 14 | defmodule PermissionEx.Test.Structs.PageReq do 15 | @moduledoc false 16 | # @derive [Poison.Encoder] 17 | defstruct action: nil 18 | end 19 | 20 | defmodule PermissionEx.Test.Structs.PagePerm do 21 | @moduledoc false 22 | # @derive [Poison.Encoder] 23 | defstruct action: nil 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /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 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :permission_ex, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:permission_ex, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 3 | "credo": {:hex, :credo, "0.10.2", "03ad3a1eff79a16664ed42fc2975b5e5d0ce243d69318060c626c34720a49512", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, 5 | "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"}, 6 | "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 8 | "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, 11 | "poison": {:hex, :poison, "2.1.0", "f583218ced822675e484648fa26c933d621373f01c6c76bd00005d7bd4b82e27", [:mix], []}, 12 | } 13 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PermissionEx.Mixfile do 2 | use Mix.Project 3 | 4 | @description """ 5 | Permission management and checking library for Elixir. 6 | """ 7 | 8 | def project do 9 | [ app: :permission_ex, 10 | version: "0.6.0", 11 | description: @description, 12 | package: package(), 13 | elixir: "~> 1.2", 14 | build_embedded: Mix.env == :prod, 15 | start_permanent: Mix.env == :prod, 16 | source_url: "https://github.com/OvermindDL1/permission_ex", 17 | #homepage_url: "http://YOUR_PROJECT_HOMEPAGE", 18 | docs: [ 19 | extras: ["README.md"], 20 | main: "readme" 21 | ], 22 | dialyzer: [ 23 | #plt_add_apps: [:plug], 24 | #flags: ["-Wno_undefined_callbacks"] 25 | ], 26 | deps: deps(), 27 | ] 28 | end 29 | 30 | defp package do 31 | [ licenses: ["MIT"], 32 | name: :permission_ex, 33 | maintainers: ["OvermindDL1"], 34 | links: %{"Github" => "https://github.com/OvermindDL1/permission_ex"} ] 35 | end 36 | 37 | # Configuration for the OTP application 38 | # 39 | # Type "mix help compile.app" for more information 40 | def application do 41 | [applications: [:logger]] 42 | end 43 | 44 | # Dependencies can be Hex packages: 45 | # 46 | # {:mydep, "~> 0.3.0"} 47 | # 48 | # Or git/path repositories: 49 | # 50 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 51 | # 52 | # Type "mix help deps" for more examples and options 53 | defp deps do 54 | [ {:credo, "~> 0.3", only: [:dev], runtime: false}, 55 | {:dialyxir, "~> 0.3", only: [:dev], runtime: false}, 56 | #{:earmark, "~> 0.2.1", only: [:dev], runtime: false}, 57 | {:ex_doc, "~> 0.20.0", only: [:dev], runtime: false}, 58 | # {:poison, "~> 2.0"}, 59 | ] 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PermissionEx 2 | 3 | A simple Struct-based Permission system for Elixir. Created to be used with 4 | Phoenix but has no requirement or any real integration with it as this is 5 | designed to be entirely generic. 6 | 7 | ## Installation 8 | 9 | [Available](https://hex.pm/packages/permission_ex) on Hex, the package can be 10 | installed by adding `permission_ex` to the list of dependencies in `mix.exs`: 11 | 12 | ```elixir 13 | def deps do 14 | [{:permission_ex, "~> 0.6.0"}] 15 | end 16 | ``` 17 | 18 | ## Features 19 | 20 | This is the current feature set of what is done, planned, and thought about. If 21 | any feature is not done yet or if any feature wants to be added that is not on 22 | this list then please open an issue and/or pull request to have it get done 23 | faster. 24 | 25 | - [x] Permission Matcher to test permissions against a requirement. 26 | - [x] Admin Permission Matcher to pre-authorize before testing normal permissions. 27 | - [ ] This currently works well with the `canada` library, but is there anything that can be done to make it even more simple? 28 | - [ ] Maybe add some more Permission specialties, such as maybe a `{:range, lower, upper}` test, maybe a function call test? 29 | - [ ] Maybe add helpers for serializing the structs to/from json by using Poison. 30 | - [ ] Maybe add helpers to serialize the structs in other ways? If so then into what ways? 31 | - [ ] Maybe a deny Permission Matcher to hard deny before admin is tested. 32 | - [ ] Maybe add support to take a list of requirements and test each so all must have a match. 33 | - [ ] Maybe Create plugs to test permissions and either set a variable or kill/redirect the plug chain. 34 | 35 | ## Usage 36 | 37 | General usage will usually be something like reading either a tagged map or a 38 | specific permission set from, say, a database or elsewhere, then comparing it to 39 | a specific requirement. 40 | 41 | For example, say you have this phoenix controller method: 42 | ```elixir 43 | def show(conn, _params) do 44 | conn 45 | |> render("index.html") 46 | end 47 | ``` 48 | 49 | And if you have a permission set from the logged in user (or you can pre-fill an 50 | anonymous user permission set, or leave empty if anon should have no access to 51 | anything), say you have it on `conn.assigns.perms` and it is a tagged map, then 52 | you could test it like this: 53 | ```elixir 54 | def show(conn, _params) do 55 | if PermissionEx.test_tagged_permissions(MyApp.Perms.IndexPage{action: :show}, conn.assigns.perms) do 56 | conn 57 | |> render("index.html") 58 | else 59 | conn 60 | |> render("unauthorized.html") 61 | end 62 | end 63 | ``` 64 | 65 | ## Examples 66 | 67 | Please see `PermissionEx` for detailed examples. 68 | 69 | All of the examples use these as the example structs: 70 | 71 | ```elixir 72 | defmodule PermissionEx.Test.Structs.User do 73 | @moduledoc false 74 | @derive [Poison.Encoder] 75 | defstruct name: nil 76 | end 77 | 78 | defmodule PermissionEx.Test.Structs.Page do 79 | @moduledoc false 80 | @derive [Poison.Encoder] 81 | defstruct action: nil 82 | end 83 | 84 | defmodule PermissionEx.Test.Structs.PageReq do 85 | @moduledoc false 86 | @derive [Poison.Encoder] 87 | defstruct action: nil 88 | end 89 | 90 | defmodule PermissionEx.Test.Structs.PagePerm do 91 | @moduledoc false 92 | @derive [Poison.Encoder] 93 | defstruct action: nil 94 | end 95 | ``` 96 | 97 | ### Testing a specific permission: `PermissionEx.test_permission/2` 98 | 99 | The required permission is the first argument, the allowed permission is on the 100 | right. 101 | 102 | * Normal usage would be something like: 103 | 104 | ```elixir 105 | permissions = [:show, :edit] # From somewhere else 106 | PermissionEx.test_permission(:show, permissions) # => true 107 | PermissionEx.test_permission(:admin, permissions) # => true 108 | ``` 109 | 110 | * Identical things match: 111 | 112 | ```elixir 113 | PermissionEx.test_permission(:anything_identical, :anything_identical) # => true 114 | PermissionEx.test_permission("anything identical", "anything identical") # => true 115 | ``` 116 | 117 | * Anything matches the atom `:_`: 118 | 119 | ```elixir 120 | PermissionEx.test_permission(:_, :_) # => true 121 | PermissionEx.test_permission(:_, :anything) # => true 122 | PermissionEx.test_permission(:anything, :_) # => true 123 | ``` 124 | 125 | * If the permission is a `[:any | permissions]` then each permission in the 126 | list will be tested individually for if they match the requirement, and if 127 | any test true then this will be true: 128 | 129 | ```elixir 130 | PermissionEx.test_permission(:show, [:any]) # => false 131 | PermissionEx.test_permission(:show, [:any, :show]) # => true 132 | PermissionEx.test_permission(:show, [:any, :show, :edit]) # => true 133 | PermissionEx.test_permission(:show, [:any, :edit, :show]) # => true 134 | PermissionEx.test_permission(:show, [:any, :edit, :otherwise]) # => false 135 | ``` 136 | 137 | * If an atom and binary fail to match, they will be tested again with the 138 | required atom being `to_string`'d, good for if loading from JSON or so, such 139 | as in: 140 | 141 | ```elixir 142 | PermissionEx.test_permission(:show, ["any", :edit, :show]) # => true 143 | PermissionEx.test_permission(:show, ["any", "edit", "show"]) # => true 144 | PermissionEx.test_permission(:show, "show") # => true 145 | ``` 146 | 147 | ### Testing a permission set against a requirement struct: `PermissionEx.test_permissions/2` 148 | 149 | You can test a struct requirement against a permission map or list or maps or 150 | even against override values such as in: 151 | 152 | * Via an override, where `true` or `:_` allows the entire requirement, 153 | and where `false`, `nil`, an empty list `[]`, or an empty map or struct `%{}` 154 | return `false`: 155 | 156 | ```elixir 157 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, :_) # => true 158 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, true) # => true 159 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, false) # => false 160 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, nil) # => false 161 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, []) # => false 162 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{}) # => false 163 | ``` 164 | 165 | * Or via a map or struct, structs tend to be better if the default values 166 | for the struct align with the needs better, if anything in a map is 167 | missing that a requirement tests for then it will return false: 168 | 169 | ```elixir 170 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{action: :_}) # => true 171 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{action: true}) # => false 172 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{action: [:any, :edit, :show]}) # => true 173 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %PermissionEx.Test.Structs.Page{}) # => false 174 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %PermissionEx.Test.Structs.Page{action: :edit}) # => false 175 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %PermissionEx.Test.Structs.Page{action: :show}) # => true 176 | ``` 177 | 178 | * Or a list of any of the above, any overrides, maps, or structs: 179 | 180 | ```elixir 181 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [true]) # => true 182 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [false]) # => false 183 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%{action: :edit}]) # => false 184 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%{action: :show}]) # => true 185 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%PermissionEx.Test.Structs.Page{action: :edit}]) # => false 186 | PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%PermissionEx.Test.Structs.Page{action: :show}]) # => true 187 | ``` 188 | 189 | ### Testing a tagged permission set against a requirement struct: `PermissionEx.test_tagged_permissions/2` 190 | 191 | You can test a struct requirement against a map of permissions keyed on the 192 | requirement structs `:__struct__` value. 193 | 194 | There is also an override key of `:admin`, this is another tagged permission map 195 | or an override that is tested before the main permissions are tested. 196 | 197 | * The permission map is just a map of the permission sets, so for example: 198 | 199 | ```elixir 200 | PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{PermissionEx.Test.Structs.Page => %{}}) # => false 201 | PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{PermissionEx.Test.Structs.Page => %PermissionEx.Test.Structs.Page{action: :show}}) # =>true 202 | PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{PermissionEx.Test.Structs.Page => %PermissionEx.Test.Structs.Page{action: :_}}) # => true 203 | PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{PermissionEx.Test.Structs.Page => %PermissionEx.Test.Structs.Page{action: nil}}) # => false 204 | ``` 205 | 206 | * Do note, the permission map is keyed on the requirement struct, not on the 207 | struct of its value, this allows you to define a different struct for the 208 | permission side that could have certain default values to be set to what you 209 | want, such as in: 210 | 211 | ```elixir 212 | PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.PageReq{action: :show}, %{PermissionEx.Test.Structs.PageReq => [%PermissionEx.Test.Structs.PagePerm{action: :_}]}) # => true 213 | PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.PageReq{action: :show}, %{PermissionEx.Test.Structs.PagePerm => [%PermissionEx.Test.Structs.PagePerm{action: :_}]}) # => false 214 | ``` 215 | 216 | * If there is an `:admin` key on the struct, then it is checked first, this 217 | allows you to set up easy overrides for all or specific matches: 218 | 219 | ```elixir 220 | # Can override and allow absolutely everything by just setting admin: true 221 | PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{}, %{admin: true}) # => true 222 | 223 | # Or can set it on a specific struct, it will not affect others then: 224 | PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{}, %{admin: %{PermissionEx.Test.Structs.Page => true}}) # => true 225 | 226 | PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.User{}, %{admin: %{PermissionEx.Test.Structs.Page => true}}) # => false 227 | 228 | # Can do fine-tuned matching as well if an override is needed, it will not allow non-matches 229 | PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{admin: %{PermissionEx.Test.Structs.Page => %{action: :show}}}) # => true 230 | PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :edit}, %{admin: %{PermissionEx.Test.Structs.Page => %{action: :show}}}) # => false 231 | ``` 232 | -------------------------------------------------------------------------------- /lib/permission_ex.ex: -------------------------------------------------------------------------------- 1 | defmodule PermissionEx do 2 | @moduledoc """ 3 | Main module and interface to testing permissions. 4 | 5 | If you wish to test a single permission for equality, then you would use 6 | `PermissionEx.test_permission/2`. 7 | 8 | If you wish to test an entire permission struct for matching allowed 9 | permissions then you would use `PermissionEx.test_permissions/2`. 10 | 11 | If you wish to test an entire permission struct for matching allowed 12 | permissions on a struct tagged map then you would use 13 | `PermissionEx.test_tagged_permissions/2`. 14 | 15 | The examples in this module use these definitions of structs for testing 16 | permissions: 17 | 18 | ```elixir 19 | 20 | defmodule PermissionEx.Test.Structs.User do 21 | @moduledoc false 22 | @derive [Poison.Encoder] 23 | defstruct name: nil 24 | end 25 | 26 | defmodule PermissionEx.Test.Structs.Page do 27 | @moduledoc false 28 | @derive [Poison.Encoder] 29 | defstruct action: nil 30 | end 31 | 32 | defmodule PermissionEx.Test.Structs.PageReq do 33 | @moduledoc false 34 | @derive [Poison.Encoder] 35 | defstruct action: nil 36 | end 37 | 38 | defmodule PermissionEx.Test.Structs.PagePerm do 39 | @moduledoc false 40 | @derive [Poison.Encoder] 41 | defstruct action: nil 42 | end 43 | 44 | ``` 45 | """ 46 | 47 | 48 | @typedoc """ 49 | A Permission matcher is either anything, of which it must then match the 50 | required permission precisely, or it is a list starting with `:any` such as 51 | `[:any | permissions]` where each item in the list will be tested against 52 | the requirement as a base permission, if any are true then this matches. 53 | """ 54 | @type permission :: [:any | permission] | any 55 | 56 | 57 | @typedoc """ 58 | This is a set of permissions such as %{}, [%{}], etc... 59 | 60 | A `:_` or `true` matches any entire requirement set. 61 | 62 | A `false` or `nil` will always not match. 63 | 64 | A list of `permissions` will each be checked individually against the 65 | requirement, if any are a true match then it returns true. 66 | 67 | A struct or map will be tested against the requirements directly, a struct is 68 | treated like a map except the `:__struct__` field will not be tested, useful 69 | if you want a requirement and permission to use different structs. Do note 70 | that the `tagged_permissions` keys should match the `:__struct__` of the 71 | requirement struct, not of the permission struct. I.E. given a 72 | `PermissionEx.Test.Structs.PageReq` and a 73 | `PermissionEx.Test.Structs.PagePerm`, such as if you want the default values 74 | for the requirement stuct to be by default tight or lenient, and the opposite 75 | for the permission struct, then calling `PermissionEx.test_permissions/2` will 76 | be like: 77 | 78 | ```elixir 79 | 80 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.PageReq{action: :show}, %{PermissionEx.Test.Structs.PageReq => [%PermissionEx.Test.Structs.PagePerm{action: :_}]}) 81 | true 82 | 83 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.PageReq{action: :show}, %{PermissionEx.Test.Structs.PagePerm => [%PermissionEx.Test.Structs.PagePerm{action: :_}]}) 84 | false 85 | 86 | ``` 87 | """ 88 | @type permissions :: :_ | boolean | nil | [permissions] | %{any => permission} | struct 89 | 90 | 91 | @typedoc """ 92 | This is a map that has a mapping of a %{Struct => %Struct{}}. 93 | 94 | The value being an actual struct is optional, it can also be a map on its own, 95 | as long as it has matching keys => values of the struct, any missing will have 96 | the requirement be false unless the requirement is `:_`. 97 | 98 | If there is an `:admin` key in the permissions, then this is checked first and 99 | can act as an easy override for the main permission set. 100 | """ 101 | @type tagged_permissions :: struct | %{:admin => tagged_permissions, atom => permissions} | %{atom => permissions} 102 | 103 | 104 | @doc ~S""" 105 | This takes a `map` of permissions with the keys being a tag to be looked up 106 | on. The required permission struct type is the tag to match on. 107 | 108 | In the key of `:admin`, if `true`, will always return true no matter what the 109 | required matcher wants, it is an override that allows all permissions. 110 | 111 | If the key of `:admin` contains a map, then the tag will be looked up in it 112 | and tested against, such as if `true` then they will get permission for that 113 | tag regardless. 114 | 115 | If a given tag has the boolean of 'true' then it will always return true, 116 | basically giving admin just for that specific tag. 117 | 118 | See `PermissionEx.test_permissions/2` for how a permission struct/map is matched. 119 | 120 | See `PermissionEx.test_permission/2` for possible permission formats. 121 | 122 | ## Examples 123 | 124 | ```elixir 125 | 126 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{}, %{admin: true}) 127 | true 128 | 129 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{}, %{admin: %{PermissionEx.Test.Structs.Page => true}}) 130 | true 131 | 132 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.User{}, %{admin: %{PermissionEx.Test.Structs.Page => true}}) 133 | false 134 | 135 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{admin: %{PermissionEx.Test.Structs.Page => %{action: :show}}}) 136 | true 137 | 138 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :edit}, %{admin: %{PermissionEx.Test.Structs.Page => %{action: :show}}}) 139 | false 140 | 141 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{PermissionEx.Test.Structs.Page => true}) 142 | true 143 | 144 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{PermissionEx.Test.Structs.Page => %{}}) 145 | false 146 | 147 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{PermissionEx.Test.Structs.Page => %{}}) 148 | false 149 | 150 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{PermissionEx.Test.Structs.Page => %PermissionEx.Test.Structs.Page{action: :show}}) 151 | true 152 | 153 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{PermissionEx.Test.Structs.Page => %PermissionEx.Test.Structs.Page{action: :_}}) 154 | true 155 | 156 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{PermissionEx.Test.Structs.Page => %PermissionEx.Test.Structs.Page{action: nil}}) 157 | false 158 | 159 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{PermissionEx.Test.Structs.Page => [%PermissionEx.Test.Structs.Page{action: :show}]}) 160 | true 161 | 162 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{PermissionEx.Test.Structs.Page => [%PermissionEx.Test.Structs.Page{action: :_}]}) 163 | true 164 | 165 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{PermissionEx.Test.Structs.Page => [%PermissionEx.Test.Structs.Page{action: nil}]}) 166 | false 167 | 168 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.PageReq{action: :show}, %{PermissionEx.Test.Structs.PageReq => [%PermissionEx.Test.Structs.PagePerm{action: :_}]}) 169 | true 170 | 171 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.PageReq{action: :show}, %{PermissionEx.Test.Structs.PagePerm => [%PermissionEx.Test.Structs.PagePerm{action: :_}]}) 172 | false 173 | 174 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.PageReq{action: :show}, %{"Elixir.PermissionEx.Test.Structs.PageReq" => [%PermissionEx.Test.Structs.PagePerm{action: :_}]}) 175 | true 176 | 177 | iex> PermissionEx.test_tagged_permissions(%PermissionEx.Test.Structs.PageReq{action: :show}, %{"Elixir.PermissionEx.Test.Structs.PagePerm" => [%PermissionEx.Test.Structs.PagePerm{action: :_}]}) 178 | false 179 | 180 | ``` 181 | 182 | """ 183 | @spec test_tagged_permissions(struct, tagged_permissions) :: boolean 184 | def test_tagged_permissions(required, tagged_perm_map) 185 | def test_tagged_permissions(_required, %{admin: true}), do: true 186 | def test_tagged_permissions(%{__struct__: tag} = required, %{admin: %{} = admin_tags} = tagged_perm_map) do 187 | #perms = Map.get_lazy(admin_tags, tag, fn -> Map.get(tagged_perm_map, tag, %{}) end) 188 | # TODO: Perhaps add a blacklist permission here too? 189 | case test_permissions(required, Map.get(admin_tags, tag, nil)) do 190 | true -> true 191 | false -> 192 | perms = Map.get_lazy(tagged_perm_map, tag, fn -> 193 | Map.get(tagged_perm_map, to_string(tag), nil) end) 194 | test_permissions(required, perms) 195 | end 196 | end 197 | def test_tagged_permissions(%{__struct__: tag} = required, %{} = tagged_perm_map) do 198 | # test_permissions(required, Map.get(tagged_perm_map, tag, nil)) 199 | perms = Map.get_lazy(tagged_perm_map, tag, fn -> 200 | Map.get(tagged_perm_map, to_string(tag), nil) end) 201 | test_permissions(required, perms) 202 | end 203 | 204 | 205 | @doc ~S""" 206 | This tests a specific requirement against a set of permission. 207 | 208 | See `PermissionEx.test_permission/2` for possible permission formats. 209 | 210 | ## Examples 211 | 212 | ```elixir 213 | 214 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, :_) 215 | true 216 | 217 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, true) 218 | true 219 | 220 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, false) 221 | false 222 | 223 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, nil) 224 | false 225 | 226 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, []) 227 | false 228 | 229 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{}) 230 | false 231 | 232 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{action: :edit}) 233 | false 234 | 235 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{action: :show}) 236 | true 237 | 238 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{action: :_}) 239 | true 240 | 241 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{action: true}) 242 | false 243 | 244 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %{action: [:any, :edit, :show]}) 245 | true 246 | 247 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %PermissionEx.Test.Structs.Page{}) 248 | false 249 | 250 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %PermissionEx.Test.Structs.Page{action: :edit}) 251 | false 252 | 253 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %PermissionEx.Test.Structs.Page{action: :show}) 254 | true 255 | 256 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %PermissionEx.Test.Structs.Page{action: :_}) 257 | true 258 | 259 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %PermissionEx.Test.Structs.Page{action: true}) 260 | false 261 | 262 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, %PermissionEx.Test.Structs.Page{action: [:any, :edit, :show]}) 263 | true 264 | 265 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [:_]) 266 | true 267 | 268 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [true]) 269 | true 270 | 271 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [false]) 272 | false 273 | 274 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [nil]) 275 | false 276 | 277 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [[]]) 278 | false 279 | 280 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%{}]) 281 | false 282 | 283 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%{action: :edit}]) 284 | false 285 | 286 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%{action: :show}]) 287 | true 288 | 289 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%{action: :_}]) 290 | true 291 | 292 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%{action: true}]) 293 | false 294 | 295 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%{action: [:any, :edit, :show]}]) 296 | true 297 | 298 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%PermissionEx.Test.Structs.Page{}]) 299 | false 300 | 301 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%PermissionEx.Test.Structs.Page{action: :edit}]) 302 | false 303 | 304 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%PermissionEx.Test.Structs.Page{action: :show}]) 305 | true 306 | 307 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%PermissionEx.Test.Structs.Page{action: :_}]) 308 | true 309 | 310 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%PermissionEx.Test.Structs.Page{action: true}]) 311 | false 312 | 313 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%PermissionEx.Test.Structs.Page{action: [:any, :edit, :show]}]) 314 | true 315 | 316 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%{"action" => :edit}]) 317 | false 318 | 319 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%{"action" => :show}]) 320 | true 321 | 322 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%{"action" => :_}]) 323 | true 324 | 325 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: :show}, [%{"action" => "_"}]) 326 | true 327 | 328 | iex> PermissionEx.test_permissions(%PermissionEx.Test.Structs.Page{action: [:something, 123]}, [%{"action" => ["something", 123]}]) 329 | true 330 | 331 | ``` 332 | """ 333 | @spec test_permissions(struct, permissions) :: boolean 334 | def test_permissions(required, permissions) 335 | def test_permissions(_required, :_) ,do: true 336 | def test_permissions(_required, true) ,do: true 337 | def test_permissions(_required, false) ,do: false 338 | def test_permissions(_required, nil) ,do: false 339 | def test_permissions(_required, []) ,do: false 340 | def test_permissions(_required, %{} = m) when map_size(m) == 0, do: false 341 | def test_permissions(required, [permissions | rest]) do 342 | case test_permissions(required, permissions) do 343 | true -> true 344 | false -> test_permissions(required, rest) 345 | end 346 | end 347 | def test_permissions(required, %{} = perms) do 348 | required 349 | |> Map.from_struct 350 | |> Enum.all?(fn {tag, req} -> 351 | perm = Map.get_lazy(perms, tag, fn -> 352 | Map.get(perms, to_string(tag), nil) end) 353 | test_permission(req, perm) 354 | end) 355 | end 356 | # def test_permissions(required, [permission|rest]) do 357 | # case test_permission(required, permission) do 358 | # true -> true 359 | # false -> test_permissions(required, rest) 360 | # end 361 | # end 362 | 363 | 364 | @doc ~S""" 365 | This tests a specific required permission against a specific permission. 366 | 367 | * If either are `:_` then it is true. 368 | * If both are identical, then it is true. 369 | * If the permission is the list starting with `:any` such as `[:any | ]` then each 370 | permission in the list is tested against the requirement 371 | * The permission "_" is equivilent to `:_` 372 | 373 | ## Examples 374 | 375 | ```elixir 376 | 377 | iex> PermissionEx.test_permission(:_, :_) 378 | true 379 | 380 | iex> PermissionEx.test_permission(:_, "_") 381 | true 382 | 383 | iex> PermissionEx.test_permission("_", :_) 384 | true 385 | 386 | iex> PermissionEx.test_permission("_", "_") 387 | true 388 | 389 | iex> PermissionEx.test_permission(:_, nil) 390 | true 391 | 392 | iex> PermissionEx.test_permission(nil, :_) 393 | true 394 | 395 | iex> PermissionEx.test_permission("_", nil) 396 | true 397 | 398 | iex> PermissionEx.test_permission(nil, "_") 399 | true 400 | 401 | iex> PermissionEx.test_permission(nil, nil) 402 | true 403 | 404 | iex> PermissionEx.test_permission(nil, :notnil) 405 | false 406 | 407 | iex> PermissionEx.test_permission(:notnil, nil) 408 | false 409 | 410 | iex> PermissionEx.test_permission(1, 1) 411 | true 412 | 413 | iex> PermissionEx.test_permission(1, 1.0) 414 | false 415 | 416 | iex> PermissionEx.test_permission('test', 'test') 417 | true 418 | 419 | iex> PermissionEx.test_permission("test", "test") 420 | true 421 | 422 | iex> PermissionEx.test_permission('test', "test") 423 | false 424 | 425 | iex> PermissionEx.test_permission(:show, :show) 426 | true 427 | 428 | iex> PermissionEx.test_permission(:show, :edit) 429 | false 430 | 431 | iex> PermissionEx.test_permission(:show, [:any]) 432 | false 433 | 434 | iex> PermissionEx.test_permission(:show, [:any, :show]) 435 | true 436 | 437 | iex> PermissionEx.test_permission(:show, [:any, :show, :edit]) 438 | true 439 | 440 | iex> PermissionEx.test_permission(:show, [:any, :edit, :show]) 441 | true 442 | 443 | iex> PermissionEx.test_permission(:show, [:any, :edit, :otherwise]) 444 | false 445 | 446 | iex> PermissionEx.test_permission(:show, ["any"]) 447 | false 448 | 449 | iex> PermissionEx.test_permission(:show, ["any", :show]) 450 | true 451 | 452 | iex> PermissionEx.test_permission(:show, ["any", :show, :edit]) 453 | true 454 | 455 | iex> PermissionEx.test_permission(:show, ["any", :edit, :show]) 456 | true 457 | 458 | iex> PermissionEx.test_permission(:show, ["any", :edit, :otherwise]) 459 | false 460 | 461 | iex> PermissionEx.test_permission(:show, ["any", "edit", "show"]) 462 | true 463 | 464 | iex> PermissionEx.test_permission(:show, ["any", "edit", "otherwise"]) 465 | false 466 | 467 | iex> PermissionEx.test_permission(:show, [:many]) 468 | false 469 | 470 | iex> PermissionEx.test_permission(:show, [:many, :show, :edit]) 471 | true 472 | 473 | iex> PermissionEx.test_permission([:show, :edit], [:many, :show, :edit]) 474 | true 475 | 476 | iex> PermissionEx.test_permission([:edit, :show], [:many, :show, :edit]) 477 | true 478 | 479 | iex> PermissionEx.test_permission(:show, "show") 480 | true 481 | 482 | iex> PermissionEx.test_permission(:show, "_") 483 | true 484 | 485 | iex> PermissionEx.test_permission(nil, "_") 486 | true 487 | 488 | iex> PermissionEx.test_permission([:show, 123], ["show", 123]) 489 | true 490 | 491 | iex> PermissionEx.test_permission(%{}, %{}) 492 | true 493 | 494 | iex> PermissionEx.test_permission(%{action: :show}, %{action: "show"}) 495 | true 496 | 497 | iex> PermissionEx.test_permission(%{action: "show"}, %{action: "show"}) 498 | true 499 | 500 | iex> PermissionEx.test_permission(%{action: :show}, %{"action" => :show}) 501 | true 502 | 503 | iex> PermissionEx.test_permission(%{"action" => :show}, %{action: :show}) 504 | true 505 | 506 | iex> PermissionEx.test_permission(%{action: :show}, %{action: [:any, :show, :index]}) 507 | true 508 | 509 | iex> PermissionEx.test_permission(%{action: :none}, %{action: [:any, :show, :index]}) 510 | false 511 | 512 | ``` 513 | """ 514 | @spec test_permission(any, permission) :: boolean 515 | def test_permission(required, permission) 516 | def test_permission(:_, _perm) ,do: true 517 | def test_permission("_", _perm) ,do: true 518 | def test_permission(_req, :_) ,do: true 519 | def test_permission(_req, "_") ,do: true # test_permission(req, :_) 520 | def test_permission(req, req) ,do: true 521 | def test_permission(_req, [:any]) ,do: false 522 | def test_permission(_req, []) ,do: false 523 | 524 | def test_permission(req, perm) when is_atom(req) and is_binary(perm) do 525 | to_string(req) === perm 526 | end 527 | 528 | def test_permission(req, ["any"|p]) ,do: test_permission(req, [:any|p]) 529 | def test_permission(req, ["many"|p]), do: test_permission(req, [:many|p]) 530 | 531 | def test_permission(required, [:any, permission | rest]) do 532 | case test_permission(required, permission) do 533 | true -> true 534 | false -> test_permission(required, [:any | rest]) 535 | end 536 | end 537 | 538 | def test_permission(required, [:many | perms]) do 539 | Enum.all?(List.wrap(required), fn req -> Enum.any?(perms, &test_permission(req, &1)) end) 540 | end 541 | 542 | def test_permission(req, perm) when is_list(req) and is_list(perm) and length(req) == length(perm) do 543 | Enum.zip(req, perm) 544 | |> Enum.all?(fn {req, perm} -> test_permission(req, perm) end) 545 | end 546 | 547 | def test_permission(req, perm) when is_map(req) and is_map(perm) do 548 | Enum.all?(req, fn 549 | {key, value} when is_binary(key) -> 550 | case perm[key] do 551 | nil -> 552 | case Enum.find(perm, fn {k, _v} -> to_string(k) == key end) do 553 | nil -> false 554 | {_otherk, other} -> test_permission(value, other) 555 | end 556 | other -> test_permission(value, other) 557 | end 558 | {key, value} -> # when is_atom(key) -> 559 | other = perm[key] || perm[to_string(key)] 560 | test_permission(value, other) 561 | end) 562 | end 563 | 564 | def test_permission(_required, _permission) do 565 | false 566 | end 567 | 568 | end 569 | --------------------------------------------------------------------------------