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
@@ -0,0 +1,26 @@
|
|
1
|
+
module Operations
|
2
|
+
module ActAsOperationable extend ActiveSupport::Concern
|
3
|
+
class_methods do
|
4
|
+
def acts_as_operationable(options={})
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
def get_operations
|
9
|
+
unless respond_to?(:operations)
|
10
|
+
raise Operations::Errors::NotImplementedError.new(self.class, :operations)
|
11
|
+
end
|
12
|
+
unless self.operations.nil? || self.operations.class == String
|
13
|
+
raise Operations::Errors::InvalidFieldError.new(:operations, self.class, String)
|
14
|
+
end
|
15
|
+
self.operations.to_s.to_operations
|
16
|
+
end
|
17
|
+
|
18
|
+
def named_scope
|
19
|
+
Operations.user_role_name_from(role || 2)
|
20
|
+
end
|
21
|
+
|
22
|
+
def as_json(options={})
|
23
|
+
super.as_json(options).merge({scope: named_scope, operations: get_operations})
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Operations
|
2
|
+
module Config
|
3
|
+
# Returns a list of user roles straight from the config file
|
4
|
+
mattr_accessor :user_roles
|
5
|
+
@@user_roles = [
|
6
|
+
{name: :admin, description: 'Administrator', value: 0, db_value: 0},
|
7
|
+
{name: :guest, description: 'Anyone', value: 3, db_value: 1},
|
8
|
+
{name: :regular, description: 'Regular user', value: 2, db_value: 2},
|
9
|
+
{name: :technician, description: 'Technician', value: 1, db_value: 3},
|
10
|
+
{name: :all, description: 'Anyone on the system', value: -1},
|
11
|
+
{name: :nobody, description: 'No one on the system', value: -2}
|
12
|
+
]
|
13
|
+
|
14
|
+
mattr_accessor :operation_name_regex
|
15
|
+
@@operation_name_regex = %r{\w}
|
16
|
+
|
17
|
+
mattr_accessor :operation_uuid_algorithm
|
18
|
+
@@operation_uuid_algorithm = OpenSSL::Digest::SHA256
|
19
|
+
|
20
|
+
# Returns a list of Operation instances for convenience
|
21
|
+
mattr_accessor :operations_list
|
22
|
+
@@operations_list = []
|
23
|
+
|
24
|
+
# Specifies how the operations should be enforced within the app
|
25
|
+
mattr_accessor :enforcements
|
26
|
+
@@enforcements = []
|
27
|
+
|
28
|
+
# Defines the default path to go to should the user not be logged in
|
29
|
+
mattr_accessor :sign_in_path
|
30
|
+
@@sign_in_path = nil
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def operation_scope_regex
|
34
|
+
%r{\A(#{user_roles.map{|o| o[:name]}.join('|')})\z}
|
35
|
+
end
|
36
|
+
|
37
|
+
def operation_scope_regex=(*args)
|
38
|
+
raise 'Operations::Config does not allow to set the variable operation_name_regex'
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_sign_in_path
|
42
|
+
if sign_in_path.nil?
|
43
|
+
warn "Warning! You have not set the Operations::Config.sign_in_path variable!"
|
44
|
+
return nil
|
45
|
+
end
|
46
|
+
Rails.application.routes.recognize_path sign_in_path
|
47
|
+
rescue ActionController::RoutingError
|
48
|
+
{controller: nil, action: nil}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# A simple way to setup Operations.
|
53
|
+
def self.setup
|
54
|
+
yield self
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.get_operations_list
|
58
|
+
operations_list
|
59
|
+
.map{|operation|
|
60
|
+
Operations::Operation.new(**operation)}
|
61
|
+
.select{|operation| operation.is_valid?}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class String
|
2
|
+
def to_operations
|
3
|
+
real_size = self.blank? ? 0 : self.strip.size
|
4
|
+
return [] if real_size == 0
|
5
|
+
|
6
|
+
by_uuid = Operations.from_uuid(self)
|
7
|
+
return by_uuid unless by_uuid.nil?
|
8
|
+
|
9
|
+
operations_list = []
|
10
|
+
|
11
|
+
begin
|
12
|
+
json_operations = JSON.parse(self)
|
13
|
+
rescue JSON::ParserError
|
14
|
+
raise Operations::Errors::InvalidOperationError.new('Could not parsel this String into a Operations::Operation object')
|
15
|
+
end
|
16
|
+
if json_operations.class == Array
|
17
|
+
json_operations.each do |json_value|
|
18
|
+
operations_list.append(json_value.to_s.to_operations)
|
19
|
+
end
|
20
|
+
elsif json_operations.class == Hash # OK
|
21
|
+
json_operations.deep_symbolize_keys!
|
22
|
+
operations_list = Operations::Operation.new(**json_operations)
|
23
|
+
end
|
24
|
+
|
25
|
+
operations_list
|
26
|
+
end
|
27
|
+
alias :to_operation :to_operations
|
28
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Operations
|
2
|
+
module Enforcer
|
3
|
+
class << self
|
4
|
+
def default_operation
|
5
|
+
Operations::Operation.new do |operation|
|
6
|
+
operation.name = :default_enforced_operation
|
7
|
+
operation.scope = :admin
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def application_actions
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
def check_for_pattern(rule, value)
|
16
|
+
rule = rule.to_s; value = value.to_s
|
17
|
+
value_ok = rule.nil? \
|
18
|
+
|| rule == '*' \
|
19
|
+
|| rule.to_s == value.to_s
|
20
|
+
unless value_ok
|
21
|
+
rule = rule.to_s
|
22
|
+
if rule.include?('*')
|
23
|
+
regex = Operations::Utils.parse_to_regex(rule)
|
24
|
+
value_ok = regex === value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
value_ok
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_operation(controller, action)
|
31
|
+
result = Operations::Config.enforcements.select do |rule|
|
32
|
+
check_for_pattern(rule[:controller], controller) && check_for_pattern(rule[:action], action)
|
33
|
+
end
|
34
|
+
rule = result[0]
|
35
|
+
return default_operation if rule.nil?
|
36
|
+
Operations.from_string(rule[:operation])
|
37
|
+
end
|
38
|
+
|
39
|
+
def enforce(controller, action, user)
|
40
|
+
operation = get_operation(controller, action)
|
41
|
+
|
42
|
+
if operation == :nobody
|
43
|
+
raise Operations::Errors::NotAuthorizedError.new(operation, 'no one is allowed to execute this action!')
|
44
|
+
end
|
45
|
+
|
46
|
+
return if operation == :all
|
47
|
+
|
48
|
+
if user.nil?
|
49
|
+
# Check if we are not already on the sign in page
|
50
|
+
sign_in_path = Operations::Config.get_sign_in_path
|
51
|
+
if sign_in_path && sign_in_path[:controller] && sign_in_path[:action]
|
52
|
+
return if sign_in_path[:controller].to_s == controller.to_s \
|
53
|
+
&& sign_in_path[:action].to_s == action.to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
# Case 1: There is no user and the operation was found
|
57
|
+
raise Operations::Errors::NotLoggedInError.new(operation)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Case 2: The operation was found
|
61
|
+
if operation
|
62
|
+
unless operation.accepts_scope? user.named_scope
|
63
|
+
raise Operations::Errors::NotAuthorizedError.new(operation, 'insufficient privileges')
|
64
|
+
else
|
65
|
+
warn "Access granted for User##{user.id} (#{controller}:#{action})"; return
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Operations
|
2
|
+
module Errors
|
3
|
+
class BaseException < StandardError
|
4
|
+
end
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require "operations/errors/not_implemented_error"
|
9
|
+
require "operations/errors/invalid_operation_error"
|
10
|
+
require "operations/errors/invalid_field_error"
|
11
|
+
require "operations/errors/not_authorized_error"
|
12
|
+
require "operations/errors/not_logged_in_error"
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Operations
|
2
|
+
module Errors
|
3
|
+
class InvalidOperationError < BaseException
|
4
|
+
def initialize(operation, msg=nil)
|
5
|
+
if operation.class == Operations::Operation
|
6
|
+
cause = "Cause:"
|
7
|
+
has_cause = false
|
8
|
+
unless operation.has_valid_name?
|
9
|
+
cause += " invalid name"
|
10
|
+
has_cause = true
|
11
|
+
end
|
12
|
+
unless operation.has_valid_scope?
|
13
|
+
cause += " invalid scope `#{operation.invalid_scope}'"
|
14
|
+
has_cause = true
|
15
|
+
end
|
16
|
+
cause "Unknown cause." unless has_cause
|
17
|
+
message = "The operation #{operation} is not valid. #{cause}"
|
18
|
+
else
|
19
|
+
message = "Invalid operation object. Cause: `#{operation}'."
|
20
|
+
end
|
21
|
+
message += " Additional info: #{msg}" if msg
|
22
|
+
super(message)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Operations
|
2
|
+
module Errors
|
3
|
+
class NotAuthorizedError < BaseException
|
4
|
+
def initialize(operation=nil, msg=nil)
|
5
|
+
cause = msg || "unknown"
|
6
|
+
operation_name = operation ? operation.name : 'insufficient privileges'
|
7
|
+
super("Operation access denied: #{operation_name}. Cause: #{cause}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Operations
|
2
|
+
module Errors
|
3
|
+
class NotLoggedInError < BaseException
|
4
|
+
def initialize(operation=nil)
|
5
|
+
operation_name = operation.nil? ? 'unknown' : (operation.description || operation.name)
|
6
|
+
super("You must log in before running this action. Attempted operation `#{operation_name}'.")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
module Operations
|
2
|
+
class Operation
|
3
|
+
OperationValue = Struct.new :name, :scope, :description
|
4
|
+
|
5
|
+
def initialize(**options, &block)
|
6
|
+
if block_given?
|
7
|
+
yield values
|
8
|
+
else
|
9
|
+
options.each do |key, value|
|
10
|
+
if values.respond_to?(key)
|
11
|
+
values.send(key.to_s + '=', value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
ensure_operation_is_valid!
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def values
|
20
|
+
@values ||= OperationValue.new(nil, :nobody)
|
21
|
+
end
|
22
|
+
|
23
|
+
def name
|
24
|
+
has_valid_name? ? values.name : 'N/A (Invalid operation name)'
|
25
|
+
end
|
26
|
+
|
27
|
+
def description
|
28
|
+
values.description
|
29
|
+
end
|
30
|
+
|
31
|
+
def users
|
32
|
+
return User.where(id: []) if scope == :nobody
|
33
|
+
return User.all if scope == :all
|
34
|
+
@users ||= Operations.users_acting_as(scope)
|
35
|
+
end
|
36
|
+
|
37
|
+
# def user
|
38
|
+
# return nil if values.user_id.nil?
|
39
|
+
# @user ||= User.find_by(id: values.user_id)
|
40
|
+
# end
|
41
|
+
|
42
|
+
# def save
|
43
|
+
# return false if user.nil?
|
44
|
+
# ensure_operation_is_valid!("Cannot save user #{user}")
|
45
|
+
# result = user.update_attribute :operations, self.to_json
|
46
|
+
# end
|
47
|
+
|
48
|
+
def is_valid?
|
49
|
+
has_valid_name? && has_valid_scope? # && !user.nil?
|
50
|
+
end
|
51
|
+
|
52
|
+
def has_valid_name?
|
53
|
+
Operations::Config.operation_name_regex === values.name
|
54
|
+
end
|
55
|
+
|
56
|
+
def has_valid_scope?
|
57
|
+
Operations::Config.operation_scope_regex === scope
|
58
|
+
end
|
59
|
+
|
60
|
+
def invalid_scope
|
61
|
+
return scope unless has_valid_scope?
|
62
|
+
end
|
63
|
+
|
64
|
+
def is_scope?(scope_name)
|
65
|
+
scope.to_s == scope_name.to_s
|
66
|
+
end
|
67
|
+
|
68
|
+
def accepts_scope?(scope_name)
|
69
|
+
scope_name = scope_name.to_sym
|
70
|
+
return true if scope == :all
|
71
|
+
return false if scope == :nobody
|
72
|
+
return true if is_scope?(scope_name)
|
73
|
+
Operations.allowed_named_roles_for(scope).include?(scope_name)
|
74
|
+
end
|
75
|
+
|
76
|
+
def allowed_roles
|
77
|
+
Operations.user_roles
|
78
|
+
end
|
79
|
+
|
80
|
+
def denied_roles
|
81
|
+
end
|
82
|
+
|
83
|
+
def as_json(options={})
|
84
|
+
additional_hash = {uuid: uuid, users_count: users.count}
|
85
|
+
if (methods = options[:methods])
|
86
|
+
if methods.class != Array
|
87
|
+
methods = [methods]
|
88
|
+
end
|
89
|
+
methods.each do |method|
|
90
|
+
begin
|
91
|
+
additional_hash[method] = send(method)
|
92
|
+
rescue NoMethodError
|
93
|
+
# ignore
|
94
|
+
next
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
values
|
99
|
+
.as_json(options)
|
100
|
+
.merge(additional_hash)
|
101
|
+
.as_json
|
102
|
+
end
|
103
|
+
|
104
|
+
def uuid
|
105
|
+
alg = Operations::Config.operation_uuid_algorithm
|
106
|
+
return Operations::Errors::BaseException("The UUID Algorithm has be a class") unless alg.class == Class
|
107
|
+
alg.new(name.to_s).to_s
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_json
|
111
|
+
as_json.to_json
|
112
|
+
end
|
113
|
+
|
114
|
+
def inspect
|
115
|
+
"\#<#{self.class.name} `#{self.name}' allowed for `#{scope}'>"
|
116
|
+
end
|
117
|
+
|
118
|
+
def ==(sc_op)
|
119
|
+
if sc_op.class == self.class
|
120
|
+
sc_op.name == name && sc_op.uuid == uuid
|
121
|
+
else
|
122
|
+
is_scope?(sc_op)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def >(sc_op)
|
127
|
+
if sc_op.class == self.class
|
128
|
+
return false if self == sc_op
|
129
|
+
return !sc_op.accepts_scope?(scope)
|
130
|
+
elsif sc_op.class == Symbol
|
131
|
+
accepts_scope?(sc_op)
|
132
|
+
else
|
133
|
+
false
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def <(sc_op)
|
138
|
+
if sc_op.class == self.class
|
139
|
+
return false if self == sc_op
|
140
|
+
return sc_op.accepts_scope?(scope)
|
141
|
+
elsif sc_op.class == Symbol
|
142
|
+
!accepts_scope?(sc_op)
|
143
|
+
else
|
144
|
+
false
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def >=(sc_op)
|
149
|
+
self == sc_op || self > sc_op
|
150
|
+
end
|
151
|
+
|
152
|
+
def <=(sc_op)
|
153
|
+
self == sc_op || self < sc_op
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def ensure_operation_is_valid!(msg=nil)
|
159
|
+
raise Operations::Errors::InvalidOperationError.new(self, nil) unless is_valid?
|
160
|
+
end
|
161
|
+
|
162
|
+
# Hide the scope for security reasons
|
163
|
+
def scope
|
164
|
+
self.values.scope || :nobody
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|