-
Notifications
You must be signed in to change notification settings - Fork 0
/
permit.ex
184 lines (144 loc) · 7.26 KB
/
permit.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
defmodule Permit do
@moduledoc ~S"""
Permit is an extensible, DSL-less library allowing the coder to define authorization rules in plain Elixir.
It can run on its own, but is also integrated with widely used Elixir libraries and frameworks.
## Libraries and repositories
The Permit library can run on its own as a standalone library, but it can also be used alongside Ecto and Phoenix integrations.
* [`Permit`](https://github.com/curiosum-dev/permit) - provides a syntax to define permissions to perform actions (defined as atoms) on objects (structs) by a specific user (subject) using functions or keyword lists, matching against an object's attributes.
* [`Permit.Ecto`](https://github.com/curiosum-dev/permit_ecto) - provides a resolver using Ecto to build and execute singular or collection Ecto queries based on defined permissions, also extending the syntax with a possibility to define more sophisticated permissions convertible to Ecto queries.
* [`Permit.Phoenix`](https://github.com/curiosum-dev/permit_phoenix) - uses `Permit` and `Permit.Ecto` to retrieve records via loader functions or queries generated by `Permit.Ecto` (if installed), based on data accessible in current context defined by a Plug `conn` or a LiveView `socket`.
## Paradigm and Extensibility
At the core of authorization resolution, there's always the question of:
* **What action** is being performed (for Phoenix, it's most likely a controller action)
* **What subject** performs the action (usually, the current user)
* **What object** the action is performed on
Once answers to these three questions are found, authorization or lack thereof is determined based on the set of permission definitions, defined as [expressions in disjunctive normal form (DNF)](https://en.wikipedia.org/wiki/Disjunctive_normal_form) expressions - that is, a set of sufficient conditions, with each condition defined as a conjunction of predicates, for example:
```text
Subject | Action | Object
--------------------------------------------
A **user** can | **update** | an **article**
...if user's ID = article author's ID AND the article is not published,
...if user's ID = article author's ID AND the article type is a live ticker,
...if user's role is editor-in-chief AND the article is not published,
...if user's role is editor-in-chief ID AND the article type is a live ticker,
...or if the use has a super-admin role.
```
Which, in Permit syntax, is translated to the following. Note the usage of pattern matching on the current user's (subject's) attributes, which allows to create function clauses for each user role. Permit does not enforce a specific structure of the `can/1` function, but as pattern matching usage is convenient in this case, it is naturally encouraged.
```elixir
def can(%User{role: :editor_in_chief} = _current_user) do
permit()
|> update(Article, state: {:not, :published})
|> update(Article, type: :live_ticker)
end
def can(%User{id: user_id} = _current_user) do
permit()
|> update(Article, author_id: user_id, state: {:not, :published})
|> update(Article, author_id: user_id, type: :live_ticker)
end
def can(%User{id: user_id, role: :super_admin} = _current_user) do
permit()
|> update(Article)
end
```
The library is written with extensibility in mind. Analogously to Phoenix interoperatbility, the developer may define their own integration with different frameworks.
For more details on interoperability, see `Permit.ResolverBase`.
## Configuration and usage
For more details on Ecto and Phoenix usage, visit [`permit_ecto`](https://hexdocs.pm/permit_ecto) and [`permit_phoenix`](https://hexdocs.pm/permit_phoenix) documentations, respectively.
### Configure & define your permissions
```elixir
defmodule MyApp.Authorization do
use Permit, permissions_module: MyApp.Permissions
end
defmodule MyApp.Permissions do
use Permit.Permissions, actions_module: Permit.Actions.CrudActions
def can(%{role: :admin} = user) do
permit()
|> all(MyApp.Blog.Article)
end
def can(%{id: user_id} = user) do
permit()
|> all(MyApp.Blog.Article, author_id: user_id)
|> read(MyApp.Blog.Article)
end
def can(user), do: permit()
end
```
Note that in the permission definitions module the `read` function is generated based on configuration provided as the `:actions_module` option - in this case, `CrudActions` generates `create`, `read`, `update` and `delete`. For more on this, see `Permit.Actions` and `Permit.Permissions`.
### Check a user's authorization to perform an action on a resource
```elixir
iex(1)> import MyApp.Authorization
iex(2)> can(%MyApp.User{id: 1}) |> read?(%MyApp.Article{author_id: 1})
true
iex(3)> can(%MyApp.User{id: 1}) |> read?(%MyApp.Article{author_id: 2})
true
iex(4)> can(%MyApp.User{id: 1}) |> update?(%MyApp.Article{author_id: 2})
false
iex(4)> can(%MyApp.User{role: :admin}) |> delete?(%MyApp.Article{author_id: 2})
true
```
Functions such as `MyApp.Authorization.read?/2`, `MyApp.Authorization.update?/2`, etc. are also generated based on the `:actions_module` option. See more in `Permit.Actions`.
"""
alias Permit.Permissions
alias Permit.SubjectMapping
alias Permit.Types
@callback resolver_module :: Types.resolver_module()
defmacro __using__(opts) do
alias Permit.Types
permissions_module = Keyword.fetch!(opts, :permissions_module)
predicates =
Macro.expand(permissions_module, __CALLER__).actions_module()
|> Permit.Actions.list_groups()
|> Enum.map(&add_predicate_name/1)
|> Enum.map(fn {predicate, name} ->
quote do
@spec unquote(predicate)(Permit.Context.t(), Types.object_or_resource_module()) ::
boolean()
def unquote(predicate)(authorization, resource) do
Permit.verify_record(authorization, resource, unquote(name))
end
end
end)
quote do
@behaviour Permit
def permissions_module do
unquote(permissions_module)
end
require unquote(permissions_module)
def actions_module,
do: unquote(permissions_module).actions_module()
@spec can(SubjectMapping.t()) :: Permit.Context.t()
def can(nil),
do: raise("Unable to create permit authorization for nil role/user")
def can(who) do
Permit.can(who, unquote(permissions_module))
end
@impl Permit
def resolver_module, do: Permit.Resolver
defoverridable resolver_module: 0
unquote(predicates)
end
end
@doc false
def can(who, permissions_module) do
who
|> SubjectMapping.subjects()
|> Stream.map(&permissions_module.can/1)
|> Enum.reduce(&Permissions.concatenate(&1, &2))
|> then(&%Permit.Context{subject: (is_struct(who) && who) || nil, permissions: &1})
end
@doc false
@spec verify_record(Permit.Context.t(), Types.object_or_resource_module(), Types.action_group()) ::
boolean()
def verify_record(
%{
permissions: permissions,
subject: subject
} = _authorization,
record,
action
) do
Permissions.granted?(permissions, action, record, subject)
end
defp add_predicate_name(atom),
do: {(Atom.to_string(atom) <> "?") |> String.to_atom(), atom}
end