aki-operations 1.0.0

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