argus-trail 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +176 -0
- data/Rakefile +6 -0
- data/app/controllers/argus/trail/application_controller.rb +45 -0
- data/app/controllers/argus/trail/audit_entries_controller.rb +19 -0
- data/app/controllers/argus/trail/permissions_controller.rb +65 -0
- data/app/controllers/argus/trail/roles_controller.rb +73 -0
- data/app/helpers/argus/trail/application_helper.rb +34 -0
- data/app/models/argus/trail/application_record.rb +7 -0
- data/app/models/argus/trail/audit_entry.rb +38 -0
- data/app/models/argus/trail/permission.rb +11 -0
- data/app/models/argus/trail/role.rb +53 -0
- data/app/models/argus/trail/role_assignment.rb +13 -0
- data/app/models/argus/trail/role_permission.rb +10 -0
- data/app/views/argus/trail/audit_entries/index.html.erb +94 -0
- data/app/views/argus/trail/pagination/_simple_pager.html.erb +9 -0
- data/app/views/argus/trail/permissions/_form.html.erb +29 -0
- data/app/views/argus/trail/permissions/edit.html.erb +10 -0
- data/app/views/argus/trail/permissions/index.html.erb +47 -0
- data/app/views/argus/trail/permissions/new.html.erb +10 -0
- data/app/views/argus/trail/permissions/show.html.erb +27 -0
- data/app/views/argus/trail/roles/_form.html.erb +50 -0
- data/app/views/argus/trail/roles/edit.html.erb +10 -0
- data/app/views/argus/trail/roles/index.html.erb +68 -0
- data/app/views/argus/trail/roles/new.html.erb +10 -0
- data/app/views/argus/trail/roles/show.html.erb +52 -0
- data/app/views/layouts/argus/trail/application.html.erb +38 -0
- data/config/routes.rb +7 -0
- data/lib/argus/trail/actor.rb +67 -0
- data/lib/argus/trail/configuration.rb +46 -0
- data/lib/argus/trail/current.rb +7 -0
- data/lib/argus/trail/engine.rb +13 -0
- data/lib/argus/trail/pagination.rb +41 -0
- data/lib/argus/trail/version.rb +5 -0
- data/lib/argus/trail.rb +30 -0
- data/lib/generators/argus/trail/config/config_generator.rb +21 -0
- data/lib/generators/argus/trail/install/install_generator.rb +46 -0
- data/lib/generators/argus/trail/install/templates/POST_INSTALL +43 -0
- data/lib/generators/argus/trail/install/templates/create_argus_trail_tables.rb.erb +47 -0
- data/lib/generators/argus/trail/install/templates/initializer.rb +40 -0
- data/lib/generators/argus/trail/views/views_generator.rb +18 -0
- metadata +105 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8012d44e5a16eb856aec00b306bc64e780d96c13f34e729e25b0ed0087aca325
|
|
4
|
+
data.tar.gz: 2990ef7c7ce767ce3f3a61874b4ddb185c44c7e50cce740e7628dfd0edf531ae
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0ac4bd48b2f65ac648d8ba978a5dc8e119f9e189403ae2ef0a97b350f400ff7ae35c4ee14b3e2271446b7083a6876daaaf6a9d2fd02a4ec50225a09e49da6a90
|
|
7
|
+
data.tar.gz: 711cee8042f3d5200792b1297f770cd0a016b67e017fe65f19f8d85c0b1f10688263300fdfe0298837c34dc170c476b39a73728b51ff50420c70de3d75725840
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright TODO: Write your name
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Argus::Trail
|
|
2
|
+
|
|
3
|
+
A mountable Rails engine that gives any app configurable roles and
|
|
4
|
+
permissions — actors can hold any number of roles — plus a unified,
|
|
5
|
+
immutable audit log of every role assignment/revocation and every
|
|
6
|
+
permission granted/revoked on a role, with ready-made, paginated HTML admin
|
|
7
|
+
screens.
|
|
8
|
+
|
|
9
|
+
See [`docs/INTEGRATION_GUIDE.md`](docs/INTEGRATION_GUIDE.md) for a detailed,
|
|
10
|
+
step-by-step walkthrough (including troubleshooting for a couple of easy
|
|
11
|
+
integration traps) — this README is the quick-start version.
|
|
12
|
+
|
|
13
|
+
Stays out of your way on the things it doesn't need to own:
|
|
14
|
+
- **No hardcoded role/permission names** — you define whatever `Role`/`Permission` rows you want.
|
|
15
|
+
- **No fixed actor class** — works with whatever you call your `User`/`Account` model.
|
|
16
|
+
- **No schema change to your actor table** — roles live in the engine's own polymorphic join table, so actors can hold 0..N roles.
|
|
17
|
+
- **No required auth library** — works standalone; auto-integrates with [Pundit](https://github.com/varvet/pundit) if it's in your Gemfile.
|
|
18
|
+
- **No required pagination library** — auto-integrates with [Kaminari](https://github.com/kaminari/kaminari) if it's in your Gemfile, otherwise falls back to a small built-in pager.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# Gemfile
|
|
24
|
+
gem "argus-trail"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle install
|
|
29
|
+
bin/rails generate argus:trail:install
|
|
30
|
+
bin/rails db:migrate
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The install generator writes `config/initializers/argus_trail.rb`, creates
|
|
34
|
+
the engine's own tables (`argus_trail_roles`, `argus_trail_permissions`,
|
|
35
|
+
`argus_trail_role_permissions`, `argus_trail_role_assignments`,
|
|
36
|
+
`argus_trail_audit_entries`), and mounts the engine at `/admin/access` in
|
|
37
|
+
`config/routes.rb`. There's no migration on your own actor/user table —
|
|
38
|
+
`argus_trail_role_assignments` is a polymorphic join table the engine owns
|
|
39
|
+
end to end.
|
|
40
|
+
|
|
41
|
+
## Wiring up your app
|
|
42
|
+
|
|
43
|
+
**1. Opt your user model in:**
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
class User < ApplicationRecord
|
|
47
|
+
include Argus::Trail::Actor
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This adds `has_many :roles` (through the engine's join table),
|
|
52
|
+
`has_permission?(name)` (true if *any* assigned role has that permission),
|
|
53
|
+
and `sync_roles!` — diffs the requested role ids against the ones the actor
|
|
54
|
+
currently holds and writes one `AuditEntry` per assignment
|
|
55
|
+
(`role_assigned`) or revocation (`role_revoked`):
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
user.sync_roles!([ admin_role.id, support_role.id ], changed_by: current_user)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
An actor can hold any number of roles at once; assigning a single role is
|
|
62
|
+
just the `new_role_ids.size == 1` case of the same call.
|
|
63
|
+
|
|
64
|
+
**2. Tell the engine who's making changes**, once, in your `ApplicationController`:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
before_action { Argus::Trail.current_actor = current_user }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**3. Authorize the admin screens.** If you have Pundit, define policies —
|
|
71
|
+
Argus::Trail uses Pundit's normal lookup, so these are just regular
|
|
72
|
+
policies:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class Argus::Trail::RolePolicy < ApplicationPolicy
|
|
76
|
+
def index? = user.admin?
|
|
77
|
+
# ...
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Do the same for `Argus::Trail::PermissionPolicy` and
|
|
82
|
+
`Argus::Trail::AuditEntryPolicy`. Without Pundit — e.g. if your app uses
|
|
83
|
+
CanCanCan, Action Policy, or nothing at all — set `config.authorize_with` to
|
|
84
|
+
a proc instead; it always takes priority over Pundit, so this works even if
|
|
85
|
+
Pundit happens to also be in your Gemfile:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# config/initializers/argus_trail.rb
|
|
89
|
+
Argus::Trail.configure do |config|
|
|
90
|
+
config.authorize_with = ->(controller, record_or_class) { controller.current_user&.admin? }
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
See [`docs/INTEGRATION_GUIDE.md`](docs/INTEGRATION_GUIDE.md#8-step-6--authorization)
|
|
95
|
+
for a wiring example per authorization gem (Pundit, CanCanCan, Action
|
|
96
|
+
Policy, plain proc). With neither Pundit nor `authorize_with` configured,
|
|
97
|
+
the engine fails closed and raises an actionable error rather than silently
|
|
98
|
+
allowing access.
|
|
99
|
+
|
|
100
|
+
## Recording role changes on an actor
|
|
101
|
+
|
|
102
|
+
Use `Actor#sync_roles!` (instead of assigning `role_ids=`/`roles=` directly)
|
|
103
|
+
so assignments and revocations land in the audit log:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
user.sync_roles!(params[:role_ids], changed_by: current_user)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Recording permission changes on a role
|
|
110
|
+
|
|
111
|
+
Use `Role#sync_permissions!` (instead of assigning `permission_ids=`
|
|
112
|
+
directly) so grants and revokes land in the audit log:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
role.sync_permissions!(params[:permission_ids], changed_by: current_user)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Customizing the views
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
bin/rails generate argus:trail:views
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Copies every view into `app/views/argus/trail` (and the layout into
|
|
125
|
+
`app/views/layouts/argus/trail`) for full override. The shipped layout is
|
|
126
|
+
self-contained (Tailwind via CDN) so it renders correctly with zero host
|
|
127
|
+
asset-pipeline setup; set `config.layout` to render inside one of your own
|
|
128
|
+
layouts instead.
|
|
129
|
+
|
|
130
|
+
Already running ActiveAdmin and would rather manage roles/permissions there
|
|
131
|
+
instead? `Role`/`Permission`/`AuditEntry` are plain ActiveRecord models, so
|
|
132
|
+
`ActiveAdmin.register` works directly — see
|
|
133
|
+
[`docs/INTEGRATION_GUIDE.md`](docs/INTEGRATION_GUIDE.md#19-optional-registering-with-activeadmin)
|
|
134
|
+
for copy-pasteable registrations and the one gotcha to know about (ActiveAdmin's
|
|
135
|
+
default checkboxes bypass the audit trail unless you call
|
|
136
|
+
`sync_permissions!`/`sync_roles!` explicitly).
|
|
137
|
+
|
|
138
|
+
## Configuration reference
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
Argus::Trail.configure do |config|
|
|
142
|
+
config.actor_class_name = "User" # your user/account model
|
|
143
|
+
config.role_class_name = "Argus::Trail::Role" # rename if it collides with another gem/model
|
|
144
|
+
config.permission_class_name = "Argus::Trail::Permission"
|
|
145
|
+
config.role_permission_class_name = "Argus::Trail::RolePermission"
|
|
146
|
+
config.changed_by_resolver = -> { Argus::Trail.current_actor }
|
|
147
|
+
config.authorize_with = nil # see "Authorize the admin screens" above
|
|
148
|
+
config.current_actor_method = :current_user
|
|
149
|
+
config.per_page = 30
|
|
150
|
+
config.layout = nil # e.g. "application"
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`role_class_name`/`permission_class_name`/`role_permission_class_name` only
|
|
155
|
+
rename the classes — they still map to the engine's own
|
|
156
|
+
`argus_trail_roles`/`argus_trail_permissions`/`argus_trail_role_permissions`
|
|
157
|
+
tables. This is for renaming (e.g. your app already has its own `Role`
|
|
158
|
+
model for something unrelated), **not** for pointing the engine at an
|
|
159
|
+
existing Role/Permission table with a different schema — see
|
|
160
|
+
[`docs/INTEGRATION_GUIDE.md`](docs/INTEGRATION_GUIDE.md#14-configuration-reference)
|
|
161
|
+
for the full mapping and caveats.
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
bundle install
|
|
167
|
+
bin/rails db:migrate
|
|
168
|
+
bin/rails test
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`test/dummy` is a minimal Rails app (with a `User` model already including
|
|
172
|
+
`Argus::Trail::Actor`, and Pundit policies) used to exercise the engine.
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Argus
|
|
2
|
+
module Trail
|
|
3
|
+
# Inherits from the *host* app's ApplicationController (not
|
|
4
|
+
# ActionController::Base) — the standard mountable-engine trick that gets
|
|
5
|
+
# current_user/session/CSRF/flash/Devise helpers/layout for free.
|
|
6
|
+
class ApplicationController < ::ApplicationController
|
|
7
|
+
include Pundit::Authorization if defined?(Pundit)
|
|
8
|
+
|
|
9
|
+
layout -> { Argus::Trail.config.layout || "argus/trail/application" }
|
|
10
|
+
|
|
11
|
+
helper_method :current_actor
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def current_actor
|
|
16
|
+
send(Argus::Trail.config.current_actor_method)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Call with a record (show/edit/update/destroy) or a class (index/new/create).
|
|
20
|
+
# Resolution order: an explicitly configured proc always wins; otherwise, if
|
|
21
|
+
# Pundit is bundled, delegate to it (host must define policies, e.g.
|
|
22
|
+
# Argus::Trail::RolePolicy — same lookup Pundit already does for any
|
|
23
|
+
# namespaced record); otherwise fail closed with an actionable error.
|
|
24
|
+
def authorize_access!(record_or_class)
|
|
25
|
+
handler = Argus::Trail.config.authorize_with
|
|
26
|
+
return handler.call(self, record_or_class) if handler
|
|
27
|
+
return authorize(record_or_class) if defined?(Pundit)
|
|
28
|
+
|
|
29
|
+
raise Argus::Trail::Configuration::MissingAuthorization
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Scope a relation down to what the current actor may see. Only Pundit's
|
|
33
|
+
# policy_scope narrows results; a custom authorize_with is a yes/no gate,
|
|
34
|
+
# not a scope, so it leaves the relation as-is.
|
|
35
|
+
def authorized_scope(scope_class)
|
|
36
|
+
return authorized_scope_via_pundit(scope_class) if Argus::Trail.config.authorize_with.nil? && defined?(Pundit)
|
|
37
|
+
scope_class.all
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def authorized_scope_via_pundit(scope_class)
|
|
41
|
+
policy_scope(scope_class)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Argus
|
|
2
|
+
module Trail
|
|
3
|
+
class AuditEntriesController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
authorize_access!(AuditEntry)
|
|
6
|
+
|
|
7
|
+
@entries = authorized_scope(AuditEntry)
|
|
8
|
+
.includes(:subject, :changed_by, :role, :permission)
|
|
9
|
+
.recent
|
|
10
|
+
|
|
11
|
+
@entries = @entries.where(subject_type: params[:subject_type]) if params[:subject_type].present?
|
|
12
|
+
@entries = @entries.where(subject_id: params[:subject_id]) if params[:subject_id].present?
|
|
13
|
+
@entries = @entries.where(role_id: params[:role_id]) if params[:role_id].present?
|
|
14
|
+
|
|
15
|
+
@pages = Argus::Trail::Pagination.paginate(@entries, page: params[:page], per: Argus::Trail.config.per_page)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Argus
|
|
2
|
+
module Trail
|
|
3
|
+
class PermissionsController < ApplicationController
|
|
4
|
+
before_action :set_permission, only: [ :show, :edit, :update, :destroy ]
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
authorize_access!(permission_class)
|
|
8
|
+
@permissions = authorized_scope(permission_class).includes(:roles).order(:name)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def show
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def new
|
|
15
|
+
@permission = permission_class.new
|
|
16
|
+
authorize_access!(@permission)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create
|
|
20
|
+
@permission = permission_class.new(permission_params)
|
|
21
|
+
authorize_access!(@permission)
|
|
22
|
+
|
|
23
|
+
if @permission.save
|
|
24
|
+
redirect_to permission_path(@permission), notice: "Permission '#{@permission.name}' created."
|
|
25
|
+
else
|
|
26
|
+
render :new, status: :unprocessable_entity
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def edit
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def update
|
|
34
|
+
if @permission.update(permission_params)
|
|
35
|
+
redirect_to permission_path(@permission), notice: "Permission '#{@permission.name}' updated."
|
|
36
|
+
else
|
|
37
|
+
render :edit, status: :unprocessable_entity
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def destroy
|
|
42
|
+
if @permission.roles.any?
|
|
43
|
+
redirect_to permissions_path,
|
|
44
|
+
alert: "Cannot delete permission '#{@permission.name}' — it is assigned to #{@permission.roles.count} role(s)."
|
|
45
|
+
else
|
|
46
|
+
@permission.destroy
|
|
47
|
+
redirect_to permissions_path, notice: "Permission '#{@permission.name}' deleted."
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def set_permission
|
|
54
|
+
@permission = permission_class.find(params[:id])
|
|
55
|
+
authorize_access!(@permission)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def permission_params
|
|
59
|
+
params.require(:permission).permit(:name, :description)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def permission_class = Argus::Trail.config.permission_class
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Argus
|
|
2
|
+
module Trail
|
|
3
|
+
class RolesController < ApplicationController
|
|
4
|
+
before_action :set_role, only: [ :show, :edit, :update, :destroy ]
|
|
5
|
+
before_action :set_permissions, only: [ :new, :edit, :create, :update ]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
authorize_access!(role_class)
|
|
9
|
+
@roles = authorized_scope(role_class).includes(:permissions).order(:name)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def show
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def new
|
|
16
|
+
@role = role_class.new
|
|
17
|
+
authorize_access!(@role)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create
|
|
21
|
+
@role = role_class.new(role_params)
|
|
22
|
+
authorize_access!(@role)
|
|
23
|
+
|
|
24
|
+
if @role.save
|
|
25
|
+
@role.sync_permissions!(params[:permission_ids], changed_by: current_actor)
|
|
26
|
+
redirect_to role_path(@role), notice: "Role '#{@role.name}' created."
|
|
27
|
+
else
|
|
28
|
+
render :new, status: :unprocessable_entity
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def edit
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def update
|
|
36
|
+
if @role.update(role_params)
|
|
37
|
+
@role.sync_permissions!(params[:permission_ids], changed_by: current_actor)
|
|
38
|
+
redirect_to role_path(@role), notice: "Role '#{@role.name}' updated."
|
|
39
|
+
else
|
|
40
|
+
render :edit, status: :unprocessable_entity
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def destroy
|
|
45
|
+
if @role.actors.any?
|
|
46
|
+
redirect_to roles_path,
|
|
47
|
+
alert: "Cannot delete role '#{@role.name}' — it is assigned to #{@role.actors.count} record(s)."
|
|
48
|
+
else
|
|
49
|
+
@role.destroy
|
|
50
|
+
redirect_to roles_path, notice: "Role '#{@role.name}' deleted."
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def set_role
|
|
57
|
+
@role = role_class.find(params[:id])
|
|
58
|
+
authorize_access!(@role)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def set_permissions
|
|
62
|
+
@permissions = permission_class.order(:name)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def role_params
|
|
66
|
+
params.require(:role).permit(:name, :description)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def role_class = Argus::Trail.config.role_class
|
|
70
|
+
def permission_class = Argus::Trail.config.permission_class
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Argus
|
|
2
|
+
module Trail
|
|
3
|
+
module ApplicationHelper
|
|
4
|
+
BADGE_PALETTE = [
|
|
5
|
+
"bg-red-100 text-red-800",
|
|
6
|
+
"bg-yellow-100 text-yellow-800",
|
|
7
|
+
"bg-green-100 text-green-800",
|
|
8
|
+
"bg-blue-100 text-blue-800",
|
|
9
|
+
"bg-purple-100 text-purple-800",
|
|
10
|
+
"bg-pink-100 text-pink-800",
|
|
11
|
+
"bg-indigo-100 text-indigo-800"
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
# Deterministic name -> palette mapping so any role name gets a stable
|
|
15
|
+
# badge color, without a hardcoded admin/manager/bde case statement.
|
|
16
|
+
def role_badge_class(name)
|
|
17
|
+
return "bg-gray-100 text-gray-700" if name.blank?
|
|
18
|
+
BADGE_PALETTE[name.to_s.sum % BADGE_PALETTE.size]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def event_type_label(event_type)
|
|
22
|
+
event_type.to_s.humanize
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def argus_trail_paginate(pages)
|
|
26
|
+
if pages.is_a?(Argus::Trail::Pagination::SimplePage)
|
|
27
|
+
render partial: "argus/trail/pagination/simple_pager", locals: { pages: pages }
|
|
28
|
+
else
|
|
29
|
+
paginate(pages)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Argus
|
|
2
|
+
module Trail
|
|
3
|
+
# Immutable log of every role assignment/revocation on an actor, and every
|
|
4
|
+
# permission granted/revoked on a role. `subject` is the actor for
|
|
5
|
+
# role_assigned/role_revoked events, or the Role for
|
|
6
|
+
# permission_granted/permission_revoked events.
|
|
7
|
+
class AuditEntry < ApplicationRecord
|
|
8
|
+
attribute :metadata, :json
|
|
9
|
+
|
|
10
|
+
belongs_to :subject, polymorphic: true
|
|
11
|
+
belongs_to :changed_by, polymorphic: true, optional: true
|
|
12
|
+
belongs_to :role, class_name: Argus::Trail.config.role_class_name, optional: true
|
|
13
|
+
belongs_to :permission, class_name: Argus::Trail.config.permission_class_name, optional: true
|
|
14
|
+
|
|
15
|
+
validates :event_type, presence: true
|
|
16
|
+
|
|
17
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
18
|
+
|
|
19
|
+
def role_name = role&.name || metadata&.dig("role_name")
|
|
20
|
+
def permission_name = permission&.name || metadata&.dig("permission_name")
|
|
21
|
+
|
|
22
|
+
def changed_by_label
|
|
23
|
+
return "System" if changed_by.nil?
|
|
24
|
+
changed_by.try(:argus_trail_display_name) ||
|
|
25
|
+
changed_by.try(:name) ||
|
|
26
|
+
changed_by.try(:email) ||
|
|
27
|
+
"#{changed_by.class.name} ##{changed_by.id}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def subject_label
|
|
31
|
+
subject.try(:argus_trail_display_name) ||
|
|
32
|
+
subject.try(:name) ||
|
|
33
|
+
subject.try(:email) ||
|
|
34
|
+
"#{subject.class.name} ##{subject.id}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Argus
|
|
2
|
+
module Trail
|
|
3
|
+
class Permission < ApplicationRecord
|
|
4
|
+
has_many :role_permissions, class_name: Argus::Trail.config.role_permission_class_name, dependent: :destroy
|
|
5
|
+
has_many :roles, through: :role_permissions
|
|
6
|
+
|
|
7
|
+
validates :name, presence: true, uniqueness: true
|
|
8
|
+
validates :description, presence: true
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Argus
|
|
2
|
+
module Trail
|
|
3
|
+
class Role < ApplicationRecord
|
|
4
|
+
has_many :role_permissions, class_name: Argus::Trail.config.role_permission_class_name, dependent: :destroy
|
|
5
|
+
has_many :permissions, through: :role_permissions
|
|
6
|
+
|
|
7
|
+
# The join table itself is polymorphic (no migration needed on the
|
|
8
|
+
# host's actor table), but a has_many :through needs one concrete type
|
|
9
|
+
# to instantiate results as, hence source_type here.
|
|
10
|
+
has_many :role_assignments, class_name: "Argus::Trail::RoleAssignment", dependent: :destroy
|
|
11
|
+
has_many :actors, through: :role_assignments, source: :actor, source_type: Argus::Trail.config.actor_class_name
|
|
12
|
+
|
|
13
|
+
validates :name, presence: true, uniqueness: true
|
|
14
|
+
validates :description, presence: true
|
|
15
|
+
|
|
16
|
+
# Diffs the requested permission ids against the role's current ones and
|
|
17
|
+
# writes one AuditEntry per addition/removal, so the audit log captures
|
|
18
|
+
# exactly which permissions were granted or revoked (not just "something changed").
|
|
19
|
+
def sync_permissions!(new_permission_ids, changed_by: Argus::Trail.config.changed_by_resolver.call)
|
|
20
|
+
new_ids = Array(new_permission_ids).map(&:to_i).reject(&:zero?)
|
|
21
|
+
current_ids = permission_ids
|
|
22
|
+
|
|
23
|
+
added_ids = new_ids - current_ids
|
|
24
|
+
removed_ids = current_ids - new_ids
|
|
25
|
+
|
|
26
|
+
transaction do
|
|
27
|
+
self.permission_ids = new_ids
|
|
28
|
+
|
|
29
|
+
Argus::Trail.config.permission_class.where(id: added_ids).find_each do |permission|
|
|
30
|
+
record_permission_change("permission_granted", permission, changed_by)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Argus::Trail.config.permission_class.where(id: removed_ids).find_each do |permission|
|
|
34
|
+
record_permission_change("permission_revoked", permission, changed_by)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def record_permission_change(event_type, permission, changed_by)
|
|
42
|
+
AuditEntry.create!(
|
|
43
|
+
event_type: event_type,
|
|
44
|
+
subject: self,
|
|
45
|
+
role_id: id,
|
|
46
|
+
permission_id: permission.id,
|
|
47
|
+
changed_by: changed_by,
|
|
48
|
+
metadata: { "role_name" => name, "permission_name" => permission.name }
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Argus
|
|
2
|
+
module Trail
|
|
3
|
+
# Join model between an actor (any class that includes Argus::Trail::Actor)
|
|
4
|
+
# and a Role — polymorphic so any number of actor classes/tables can hold
|
|
5
|
+
# roles without the engine needing a foreign key on each one.
|
|
6
|
+
class RoleAssignment < ApplicationRecord
|
|
7
|
+
belongs_to :actor, polymorphic: true
|
|
8
|
+
belongs_to :role, class_name: Argus::Trail.config.role_class_name
|
|
9
|
+
|
|
10
|
+
validates :role_id, uniqueness: { scope: [ :actor_type, :actor_id ] }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Argus
|
|
2
|
+
module Trail
|
|
3
|
+
class RolePermission < ApplicationRecord
|
|
4
|
+
belongs_to :role, class_name: Argus::Trail.config.role_class_name
|
|
5
|
+
belongs_to :permission, class_name: Argus::Trail.config.permission_class_name
|
|
6
|
+
|
|
7
|
+
validates :permission_id, uniqueness: { scope: :role_id }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|