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
@@ -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
|