aki-operations 1.0.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 +87 -0
- data/Rakefile +27 -0
- data/app/controllers/admin/operations/actions_controller.rb +28 -0
- data/app/controllers/admin/operations/users_controller.rb +28 -0
- data/app/controllers/admin/operations_controller.rb +12 -0
- data/app/controllers/operations/enforced_controller.rb +45 -0
- data/app/helpers/operations_helper.rb +15 -0
- data/app/views/admin/operations/actions/index.html.erb +20 -0
- data/app/views/admin/operations/actions/index/_edit.html.erb +0 -0
- data/app/views/admin/operations/actions/index/_view.html.erb +156 -0
- data/app/views/admin/operations/index.html.erb +88 -0
- data/app/views/admin/operations/users/index.html.erb +20 -0
- data/app/views/admin/operations/users/index/_edit.html.erb +4 -0
- data/app/views/admin/operations/users/index/_view.html.erb +158 -0
- data/config/routes.rb +13 -0
- data/lib/generators/operations_generator.rb +10 -0
- data/lib/generators/templates/operations.rb +33 -0
- data/lib/operations.rb +152 -0
- data/lib/operations/act_as_operationable.rb +26 -0
- data/lib/operations/config.rb +64 -0
- data/lib/operations/core_ext.rb +28 -0
- data/lib/operations/enforcer.rb +71 -0
- data/lib/operations/engine.rb +5 -0
- data/lib/operations/errors.rb +6 -0
- data/lib/operations/errors/base_exception.rb +12 -0
- data/lib/operations/errors/invalid_field_error.rb +9 -0
- data/lib/operations/errors/invalid_operation_error.rb +26 -0
- data/lib/operations/errors/not_authorized_error.rb +11 -0
- data/lib/operations/errors/not_implemented_error.rb +9 -0
- data/lib/operations/errors/not_logged_in_error.rb +10 -0
- data/lib/operations/operation.rb +167 -0
- data/lib/operations/railtie.rb +17 -0
- data/lib/operations/utils.rb +10 -0
- data/lib/operations/version.rb +3 -0
- data/lib/tasks/operations_tasks.rake +6 -0
- metadata +252 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c2d08c48fb192fb73dc402a0f298bdf008f76c939484555bea9f3dd3618a9665
|
4
|
+
data.tar.gz: 6e2f64d2996ee38cd52906b28f09c8f072163a8c8d5eb9792e904c3150035877
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ab840178692d2edd91fa90fdd4bc18264e4cb5e23468440cec7501b0d12e9fcef7c0bc5b6f4c2a3a2f94f42e1143f5245b1814fd364517056fafa107436f78d8
|
7
|
+
data.tar.gz: 00a32fa12deff08c781b0599242f82b7323a94ca3db00b69dc5713e53853deb0f58f7fab9e0818402b18954fc80b4f454ca07c79732176b30e8728fd35665d23
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2018 アキントラ・アキニエレ
|
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,87 @@
|
|
1
|
+
# Rails Operations
|
2
|
+
Manage your users' operations (permissions to execute some actions) in your
|
3
|
+
Ruby on Rails application.
|
4
|
+
|
5
|
+
## Requirements
|
6
|
+
Prior to installing, please make sure these gems can be installed on your
|
7
|
+
system:
|
8
|
+
- rails (v5.2+)
|
9
|
+
|
10
|
+
If you wish to run this gem locally, the following gems are also to consider:
|
11
|
+
- bootstrap (v4.1+)
|
12
|
+
- jquery-rails (v4.3+)
|
13
|
+
- jquery-ui-rails (v6.0+)
|
14
|
+
- bootstrap4-kaminari-views
|
15
|
+
- sqlite3 (v1.3.6+)
|
16
|
+
- sass-rails (v5.0+)
|
17
|
+
- select2-rails
|
18
|
+
- web-console
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
Add this line to your application's Gemfile:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'operations'
|
25
|
+
```
|
26
|
+
|
27
|
+
And then execute:
|
28
|
+
```bash
|
29
|
+
$ bundle
|
30
|
+
```
|
31
|
+
|
32
|
+
Or install it yourself as:
|
33
|
+
```bash
|
34
|
+
$ gem install operations
|
35
|
+
```
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
Here are the most relevant API entries from this Gem:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
### From Operations module
|
42
|
+
Operations.operations_list # All valid Operations::Operation from Rails config
|
43
|
+
Operations.from_string(name) # Gets the Operations::Operation by string
|
44
|
+
Operations.allows?(user, name) # Checks if the user can execute the operation string
|
45
|
+
Operations.user_roles # All users roles defined in the Rails config
|
46
|
+
|
47
|
+
### From Operations::Operation class
|
48
|
+
operation = Operations::Operation.new do |operation|
|
49
|
+
# Your operation name
|
50
|
+
operation.name = 'my_operation'
|
51
|
+
|
52
|
+
# Allows :admin, :technician, :regular and :guest.
|
53
|
+
# These can be set in your config/application.rb with the
|
54
|
+
# variable config.user_roles. Example:
|
55
|
+
#
|
56
|
+
# module MyApp
|
57
|
+
# class Application < Rails::Application
|
58
|
+
# config.user_roles += {name: 'my_other_scope'}
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
operation.scope = :admin
|
62
|
+
end
|
63
|
+
|
64
|
+
# Or
|
65
|
+
operation = Operations::Operation.new{name: :my_operation, scope: :admin}
|
66
|
+
|
67
|
+
# Instance variable
|
68
|
+
allowed_users = operation.users # Returns a list of users based on the scope
|
69
|
+
is_valid = operation.is_valid? # For validation purposes
|
70
|
+
|
71
|
+
### Core extensions
|
72
|
+
# Convert a string to a list of Operations::Operations
|
73
|
+
"bf9[..]a248".to_operation # From a UUID (example truncated)
|
74
|
+
"{\"name\":\"my_operation\",\"scope\":\"admin\"}".to_operation # From a valid JSON string
|
75
|
+
```
|
76
|
+
|
77
|
+
## Contributing
|
78
|
+
Do you wish to contribute? It's simple! All you need to do is:
|
79
|
+
- Fork this project
|
80
|
+
- Work your magic
|
81
|
+
- **RUN TESTS**
|
82
|
+
- _Don't forget to synchronize with the master branch!_
|
83
|
+
- Push to your fork
|
84
|
+
- Make a pull request!
|
85
|
+
|
86
|
+
## License
|
87
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Operations'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'bundler/gem_tasks'
|
18
|
+
|
19
|
+
require 'rake/testtask'
|
20
|
+
|
21
|
+
Rake::TestTask.new(:test) do |t|
|
22
|
+
t.libs << 'test'
|
23
|
+
t.pattern = 'test/**/*_test.rb'
|
24
|
+
t.verbose = false
|
25
|
+
end
|
26
|
+
|
27
|
+
task default: :test
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Admin::Operations::ActionsController < ApplicationController
|
2
|
+
|
3
|
+
def index
|
4
|
+
@actions = Operations.operations_list
|
5
|
+
respond_to do |format|
|
6
|
+
format.html {
|
7
|
+
@editing = params[:edit].to_s.downcase.strip == 'true'
|
8
|
+
@editing_title = @editing ? 'Manage actions' : 'View actions'
|
9
|
+
}
|
10
|
+
format.json { render json: @actions }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def details
|
15
|
+
operation = Operations.from_uuid(params[:uuid])
|
16
|
+
if operation
|
17
|
+
users = operation.users
|
18
|
+
if users
|
19
|
+
render json: {success: true, operation: operation, users: users}
|
20
|
+
else
|
21
|
+
render json: {success: false, message: "Operation with UUID `#{params[:uuid]}' is not valid."}
|
22
|
+
end
|
23
|
+
else
|
24
|
+
render json: {success: false, message: "Operation with UUID `#{params[:uuid]}' not found."}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Admin::Operations::UsersController < ApplicationController
|
2
|
+
|
3
|
+
def index
|
4
|
+
@permissions = Operations.user_roles
|
5
|
+
respond_to do |format|
|
6
|
+
format.html {
|
7
|
+
@editing = params[:edit].to_s.downcase.strip == 'true'
|
8
|
+
@editing_title = @editing ? 'Manage user permissions' : 'View user permissions'
|
9
|
+
}
|
10
|
+
format.json { render json: @permissions }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def details
|
15
|
+
operation = Operations.user_roles.select{|o| o[:name].to_sym == params[:value].to_sym}.first
|
16
|
+
if operation
|
17
|
+
operations_list = Operations.operations_for(params[:value].to_sym)
|
18
|
+
if operations_list
|
19
|
+
render json: {success: true, operations: operations_list.as_json(methods: [:users]), operation: operation.as_json(methods: [:users])}
|
20
|
+
else
|
21
|
+
render json: {success: false, message: "Operation role `#{params[:value]}' is not valid."}
|
22
|
+
end
|
23
|
+
else
|
24
|
+
render json: {success: false, message: "Operation role `#{params[:value]}' not found."}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Operations
|
2
|
+
class EnforcedController < ActionController::Base
|
3
|
+
|
4
|
+
include ApplicationHelper
|
5
|
+
include OperationsHelper
|
6
|
+
|
7
|
+
before_action :check_operations
|
8
|
+
|
9
|
+
rescue_from Operations::Errors::NotLoggedInError, with: :go_to_login
|
10
|
+
rescue_from Operations::Errors::NotAuthorizedError, with: :render_403_error
|
11
|
+
|
12
|
+
def render_403_error(err)
|
13
|
+
Rails.logger.info "### Operations::Error : #{err} (#{err.class}) ###"
|
14
|
+
respond_to do |format|
|
15
|
+
format.html {redirect_back(fallback_location: root_path, alert: "#{err}")}
|
16
|
+
format.xml {render xml: {message: "#{err}"}, status: 403}
|
17
|
+
format.json {render json: {message: "#{err}"}, status: 403}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def go_to_login(err)
|
22
|
+
respond_to do |format|
|
23
|
+
format.html {
|
24
|
+
if Operations::Config.sign_in_path
|
25
|
+
redirect_to(Operations::Config.sign_in_path, alert: 'Please login first.')
|
26
|
+
else
|
27
|
+
redirect_back(fallback_location: root_path, alert: "#{err}")
|
28
|
+
end
|
29
|
+
}
|
30
|
+
format.xml {render xml: {message: "#{err}"}, status: 403}
|
31
|
+
format.json {render json: {message: "#{err}"}, status: 403}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def check_operations
|
37
|
+
controller = params[:controller]; action = params[:action]
|
38
|
+
if respond_to?(:current_user)
|
39
|
+
warn "Enforcing #{controller}:#{action}..."
|
40
|
+
Operations::Enforcer.enforce(controller, action, current_user)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module OperationsHelper
|
2
|
+
|
3
|
+
def enforce(operation, &block)
|
4
|
+
operation = Operations.from_string(operation)
|
5
|
+
return if operation.nil?
|
6
|
+
if block_given?
|
7
|
+
return capture(&block) if operation == :all
|
8
|
+
return '' if operation == :nobody
|
9
|
+
return capture(&block) if operation.accepts_scope?(current_user.named_scope)
|
10
|
+
''
|
11
|
+
end
|
12
|
+
''
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<div class="container">
|
2
|
+
<div class="padded">
|
3
|
+
<div class="justify-content-between d-flex w-100">
|
4
|
+
<h1><%= @editing_title %></h1>
|
5
|
+
<h6>You are a: <%= user_role_badge(current_user) %></h6>
|
6
|
+
</div>
|
7
|
+
<div class="text-center">
|
8
|
+
<div class="small text-muted">
|
9
|
+
System actions count: <%= @actions.size %>
|
10
|
+
</div>
|
11
|
+
</div>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<% if @editing %>
|
15
|
+
<%= render 'admin/operations/actions/index/edit' %>
|
16
|
+
<% else %>
|
17
|
+
<%= render 'admin/operations/actions/index/view' %>
|
18
|
+
<% end %>
|
19
|
+
|
20
|
+
</div>
|
File without changes
|
@@ -0,0 +1,156 @@
|
|
1
|
+
<p>
|
2
|
+
View here all system actions that are defined on the system. <!-- Should you need
|
3
|
+
adding, removing or editing these actions, please click
|
4
|
+
<%= link_to('here', admin_operations_actions_path(params: {edit: true})) %>. -->
|
5
|
+
</p>
|
6
|
+
|
7
|
+
<div class="row">
|
8
|
+
<div class="col-md-6">
|
9
|
+
<div class="very lightly padded text-center">
|
10
|
+
<b class="text-muted">System actions</b>
|
11
|
+
</div>
|
12
|
+
<div class="list-group" style="overflow-y: auto; height: 62vh;">
|
13
|
+
<% @actions.each do |action| %>
|
14
|
+
<div sys_action="<%= action.uuid %>" class="list-group-item list-group-item-action <%= action.is_valid? ? "clickable" : 'disabled' %>">
|
15
|
+
<% if action.description && action.name %>
|
16
|
+
<div class="d-flex w-100 justify-content-between">
|
17
|
+
<%= action.description %>
|
18
|
+
<small class="text-muted"><%= action.name %></small>
|
19
|
+
</div>
|
20
|
+
<% else %>
|
21
|
+
<%= action.description || action.name %>
|
22
|
+
<% end %>
|
23
|
+
</div>
|
24
|
+
<% end %>
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
<div class="col-md-6">
|
28
|
+
<div class="very lightly padded">
|
29
|
+
<div class="text-center">
|
30
|
+
<b class="text-muted">More details</b>
|
31
|
+
</div>
|
32
|
+
<div style="margin-top: 10px;">
|
33
|
+
<div class="cards">
|
34
|
+
<div class="text-center">
|
35
|
+
<div before-getting="description" class="card-body">
|
36
|
+
You can click on one of the actions on the right. Doing that
|
37
|
+
will display more details about the role right here.
|
38
|
+
</div>
|
39
|
+
<div before-getting="loading" class="card-body">
|
40
|
+
Please wait while we're fetching the latest information for
|
41
|
+
this system action...
|
42
|
+
</div>
|
43
|
+
</div>
|
44
|
+
<div after-getting="description" class="card-bodys">
|
45
|
+
<p class="very lightly padded">
|
46
|
+
<div class="d-flex w-100 justify-content-between">
|
47
|
+
<span><b>Name: </b> <span after-getting="field" data-field="name"></span></span>
|
48
|
+
<div id="operation-scope">
|
49
|
+
<b>Scope: </b> <span after-getting="field" data-field="operation_scope"></span>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
</p>
|
53
|
+
<div class="small">
|
54
|
+
<p class="very lightly padded">
|
55
|
+
Required category to execute this action: <b><span after-getting="field" data-field="scope"></span></b>
|
56
|
+
</p>
|
57
|
+
<div id="users-list-cont">
|
58
|
+
<p class="very lightly padded">
|
59
|
+
The following <b><span after-getting="field" data-field="users_count"></span></b> are allowed to execute this action:
|
60
|
+
</p>
|
61
|
+
<div id="users-list" class="list-group"></div>
|
62
|
+
</div>
|
63
|
+
</div>
|
64
|
+
</div>
|
65
|
+
</div>
|
66
|
+
</div>
|
67
|
+
</div>
|
68
|
+
</div>
|
69
|
+
</div>
|
70
|
+
|
71
|
+
<script type="text/javascript">
|
72
|
+
$(document).ready(function() {
|
73
|
+
$('[before-getting]').hide();
|
74
|
+
$('[after-getting]').hide();
|
75
|
+
$('[before-getting="description"]').show();
|
76
|
+
$('#users-list-cont').hide();
|
77
|
+
|
78
|
+
$('[sys_action]').on('click', function(e) {
|
79
|
+
$('[sys_action]').removeClass('active');
|
80
|
+
$('[after-getting]').show();
|
81
|
+
$('[before-getting]').hide();
|
82
|
+
$('[before-getting="loading"]').show();
|
83
|
+
|
84
|
+
let target = $(this);
|
85
|
+
$('[sys_action]').addClass('disabled');
|
86
|
+
let value = target.attr('sys_action');
|
87
|
+
if (value) {
|
88
|
+
$.ajax({
|
89
|
+
url: '/admin/operations/actions/' + value + '/details',
|
90
|
+
success: function(response) {
|
91
|
+
$('[before-getting]').hide();
|
92
|
+
$('#before-getting').hide();
|
93
|
+
$('[sys_action]').removeClass('disabled');
|
94
|
+
if (response.success) {
|
95
|
+
target.addClass('active');
|
96
|
+
let operation = response.operation;
|
97
|
+
let users = response.users;
|
98
|
+
|
99
|
+
$('[after-getting="field"][data-field="name"]')
|
100
|
+
.text(operation.name);
|
101
|
+
$('[after-getting="field"][data-field="scope"]')
|
102
|
+
.text(operation.description);
|
103
|
+
if (operation.scope) {
|
104
|
+
$('#operation-scope').show();
|
105
|
+
} else {
|
106
|
+
$('#operation-scope').hide();
|
107
|
+
}
|
108
|
+
$('[after-getting="field"][data-field="operation_scope"]')
|
109
|
+
.text(operation.scope);
|
110
|
+
|
111
|
+
const users_count = count => {
|
112
|
+
return users.length == 1 ?
|
113
|
+
'one operation' :
|
114
|
+
users.length + " users";
|
115
|
+
}
|
116
|
+
$('#users-list').text('');
|
117
|
+
if (users.length > 0) {
|
118
|
+
$('#users-list-cont').show();
|
119
|
+
$('[after-getting="field"][data-field="users_count"]')
|
120
|
+
.text(users_count(users.length));
|
121
|
+
let user_html = [];
|
122
|
+
const operations_description = operations => {
|
123
|
+
if (!operations || operations.length < 1) {
|
124
|
+
return "No custom operations have been set."
|
125
|
+
}
|
126
|
+
return "Custom operations count: " + operations.length;
|
127
|
+
}
|
128
|
+
|
129
|
+
[].forEach.call(users, function(user) {
|
130
|
+
user_html += [
|
131
|
+
'<div class="list-group">',
|
132
|
+
'<div class="list-group-item">',
|
133
|
+
'<div class="d-flex w-100 justify-content-between">',
|
134
|
+
'<span class="bold">' + user.email + '</span>',
|
135
|
+
'<small class="text-muted">' + user.scope + '</small>',
|
136
|
+
'</div>',
|
137
|
+
'<p class="mb-1">',
|
138
|
+
operations_description(user.operations),
|
139
|
+
'</p>',
|
140
|
+
'</div>',
|
141
|
+
'</div>'
|
142
|
+
].join('');
|
143
|
+
});
|
144
|
+
$('#users-list').html(user_html);
|
145
|
+
} else {
|
146
|
+
users_list_text = "No users can have been associated."
|
147
|
+
$('#users-list-cont').hide();
|
148
|
+
$('[after-getting="field"][data-field="users_count"]').text('');
|
149
|
+
}
|
150
|
+
}
|
151
|
+
}
|
152
|
+
});
|
153
|
+
}
|
154
|
+
});
|
155
|
+
});
|
156
|
+
</script>
|