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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +87 -0
  4. data/Rakefile +27 -0
  5. data/app/controllers/admin/operations/actions_controller.rb +28 -0
  6. data/app/controllers/admin/operations/users_controller.rb +28 -0
  7. data/app/controllers/admin/operations_controller.rb +12 -0
  8. data/app/controllers/operations/enforced_controller.rb +45 -0
  9. data/app/helpers/operations_helper.rb +15 -0
  10. data/app/views/admin/operations/actions/index.html.erb +20 -0
  11. data/app/views/admin/operations/actions/index/_edit.html.erb +0 -0
  12. data/app/views/admin/operations/actions/index/_view.html.erb +156 -0
  13. data/app/views/admin/operations/index.html.erb +88 -0
  14. data/app/views/admin/operations/users/index.html.erb +20 -0
  15. data/app/views/admin/operations/users/index/_edit.html.erb +4 -0
  16. data/app/views/admin/operations/users/index/_view.html.erb +158 -0
  17. data/config/routes.rb +13 -0
  18. data/lib/generators/operations_generator.rb +10 -0
  19. data/lib/generators/templates/operations.rb +33 -0
  20. data/lib/operations.rb +152 -0
  21. data/lib/operations/act_as_operationable.rb +26 -0
  22. data/lib/operations/config.rb +64 -0
  23. data/lib/operations/core_ext.rb +28 -0
  24. data/lib/operations/enforcer.rb +71 -0
  25. data/lib/operations/engine.rb +5 -0
  26. data/lib/operations/errors.rb +6 -0
  27. data/lib/operations/errors/base_exception.rb +12 -0
  28. data/lib/operations/errors/invalid_field_error.rb +9 -0
  29. data/lib/operations/errors/invalid_operation_error.rb +26 -0
  30. data/lib/operations/errors/not_authorized_error.rb +11 -0
  31. data/lib/operations/errors/not_implemented_error.rb +9 -0
  32. data/lib/operations/errors/not_logged_in_error.rb +10 -0
  33. data/lib/operations/operation.rb +167 -0
  34. data/lib/operations/railtie.rb +17 -0
  35. data/lib/operations/utils.rb +10 -0
  36. data/lib/operations/version.rb +3 -0
  37. data/lib/tasks/operations_tasks.rake +6 -0
  38. metadata +252 -0
@@ -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
@@ -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.
@@ -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).
@@ -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,12 @@
1
+ class Admin::OperationsController < ApplicationController
2
+
3
+ def index
4
+ @operations = Operations.operations_list
5
+ @permissions = []
6
+ respond_to do |format|
7
+ format.html
8
+ format.json { render json: @operations, except: [:uuid] }
9
+ end
10
+ end
11
+
12
+ 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>
@@ -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>