aki-operations 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|