mikras_utils 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +10 -0
  5. data/README.md +31 -0
  6. data/Rakefile +8 -0
  7. data/acl-build +5 -0
  8. data/build +11 -0
  9. data/exe/mikras_utils +5 -0
  10. data/exe/mkacl +59 -0
  11. data/lib/mikras_utils/mkacl/analyzer.rb +46 -0
  12. data/lib/mikras_utils/mkacl/generator.rb +55 -0
  13. data/lib/mikras_utils/mkacl/generators/acl_functions.rb +208 -0
  14. data/lib/mikras_utils/mkacl/generators/id_functions.rb +262 -0
  15. data/lib/mikras_utils/mkacl/generators/insert_triggers.rb +267 -0
  16. data/lib/mikras_utils/mkacl/generators/role_functions.rb +72 -0
  17. data/lib/mikras_utils/mkacl/generators/rules.rb +80 -0
  18. data/lib/mikras_utils/mkacl/parser.rb +73 -0
  19. data/lib/mikras_utils/mkacl/spec.rb +154 -0
  20. data/lib/mikras_utils/mkacl.rb +18 -0
  21. data/lib/mikras_utils/version.rb +5 -0
  22. data/lib/mikras_utils.rb +6 -0
  23. data/sig/mikras_utils.rbs +4 -0
  24. data/tests/acl.fox +312 -0
  25. data/tests/acl.spec +135 -0
  26. data/tests/acl.sql +132 -0
  27. data/tests/acl_portal-functions.sql +94 -0
  28. data/tests/acl_portal-tables.sql +23 -0
  29. data/tests/acl_portal-views.sql +73 -0
  30. data/tests/agg.sql +25 -0
  31. data/tests/app.sql +48 -0
  32. data/tests/app_portal-tables.sql +138 -0
  33. data/tests/app_portal-triggers.sql +23 -0
  34. data/tests/app_portal-views.sql +203 -0
  35. data/tests/auth.sql +12 -0
  36. data/tests/auth.users.sql +5 -0
  37. data/tests/build +7 -0
  38. data/tests/final-functions.sql +28 -0
  39. data/tests/fox.sql +158 -0
  40. data/tests/initial-functions.sql +6 -0
  41. data/tests/meta.sql +197 -0
  42. data/tests/reflections.yml +5 -0
  43. data/tests/schemas.sql +25 -0
  44. data/tests/setup.sql +8 -0
  45. data/tests/sys_portal.sql +172 -0
  46. metadata +145 -0
@@ -0,0 +1,73 @@
1
+
2
+ module MkAcl
3
+ class Parser
4
+ attr_reader :file
5
+
6
+ def initialize(file) @file = file end
7
+ def parse() parse_spec end
8
+ def self.parse(file) Parser.new(file).parse end
9
+
10
+ private
11
+ def error(*msg) raise ParseError, *msg end
12
+ def norm_array(value) value.is_a?(Array) ? value : value&.split end
13
+
14
+ def parse_spec
15
+ hash = YAML.load(IO.read(file), symbolize_names: true)
16
+
17
+ schema = hash.delete(:schema) or raise ArgumentError, "Can't find 'schema' declaration"
18
+ app_schema = schema[:app] or raise ArgumentError, "Can't find 'schema.app' attribute"
19
+ acl_schema = schema[:acl] or raise ArgumentError, "Can't find 'schema.acl' attribute"
20
+ spec = Spec.new(file, app_schema, acl_schema)
21
+ parse_tables(spec, hash)
22
+ spec
23
+ end
24
+
25
+ def parse_tables(spec, tables)
26
+ for table_name, actions in tables
27
+ table = Table.new(spec, table_name, actions.delete(:domain))
28
+ parse_actions(table, actions)
29
+ end
30
+ end
31
+
32
+ def parse_actions(table, actions)
33
+ for action_name, rules in actions
34
+ constrain?(action_name, :insert, :select, :update, :delete) or error "Illegal action '#{action_name}'"
35
+ constrain?(rules, String, Array, Hash) or error "Illegal value for #{action} action '#{rules}'"
36
+ action = Action.new(table, action_name)
37
+
38
+ # Normalize rules
39
+ case rules
40
+ when Hash
41
+ rules = [rules]
42
+ when String
43
+ rules = [{ role: rules }]
44
+ end
45
+
46
+ parse_rules(action, rules)
47
+ end
48
+ end
49
+
50
+ def parse_rules(action, rules)
51
+ index = 0
52
+ for entry in rules
53
+ rule = Rule.new(action, index += 1)
54
+
55
+ for key, value in entry
56
+ case key
57
+ when :role; rule.roles = norm_array(value)
58
+ when :using; rule.using = value
59
+ when :check; rule.check = value
60
+ when :include; rule.include = norm_array(value)
61
+ when :exclude; rule.exclude = norm_array(value)
62
+ else
63
+ raise ArgumentError, "Illegal field '#{key}' in #{action.table}.#{action}"
64
+ end
65
+ end
66
+
67
+ !action.rules.empty? or
68
+ error "At least one rule is required in #{action.table}.#{action}"
69
+ end
70
+ end
71
+ end
72
+ end
73
+
@@ -0,0 +1,154 @@
1
+
2
+ module MkAcl
3
+ class Spec
4
+ attr_reader :file # Only for informational purposes
5
+ attr_reader :app_schema
6
+ attr_reader :acl_schema
7
+ attr_reader :tables
8
+
9
+ forward_to :@table_hash, :[], :key?, :keys, :values
10
+
11
+ def initialize(file, app_schema, acl_schema)
12
+ @file = file
13
+ @app_schema = app_schema
14
+ @acl_schema = acl_schema
15
+ @tables = []
16
+ @table_hash = {}
17
+ end
18
+
19
+ def dump
20
+ puts "Schema"
21
+ indent {
22
+ puts "app_schema: #{app_schema}"
23
+ puts "acl_schema: #{acl_schema}"
24
+ puts
25
+ tables.map(&:dump)
26
+ }
27
+ end
28
+
29
+ private
30
+ def attach_table(table)
31
+ @tables << table
32
+ @table_hash[table.name] = table
33
+ end
34
+ end
35
+
36
+ class Table
37
+ attr_reader :spec
38
+ attr_accessor :parents # Parent TableSpec objects. Initialized by the analyzer
39
+ attr_reader :uid # SCHEMA.TABLE name
40
+ attr_reader :name
41
+ attr_reader :record_name # Associated record name. Used in function names
42
+ attr_accessor :domain # Security domain - either 'case' or 'event'. Initialized by the analyzer
43
+
44
+ # Action objects
45
+ def insert = @actions["insert"]
46
+ def select = @actions["select"]
47
+ def update = @actions["update"]
48
+ def delete = @actions["delete"]
49
+
50
+ # Hash from action name to action object
51
+ attr_reader :actions
52
+
53
+ def initialize(spec, name, domain)
54
+ @spec = spec
55
+ @parents = []
56
+ @name = name.to_s
57
+ @uid = "#{@spec.app_schema}.#{@name}"
58
+ @record_name = Prick::Inflector.singularize(@name)
59
+ @domain = domain
60
+ @actions = {}
61
+ @spec.send :attach_table, self
62
+ for action_name in %w(insert select update delete)
63
+ attach_action(Action.new(self, action_name))
64
+ end
65
+ end
66
+
67
+ def to_s() = name
68
+ def inspect() "<<#{name}>>" end
69
+
70
+ def dump
71
+ puts "#{name}:"
72
+ indent {
73
+ puts "domain: #{domain}" if domain
74
+ puts "parents: #{parents.inspect}"
75
+ for action_name in %w(insert select update delete)
76
+ actions[action_name]&.dump
77
+ end
78
+ }
79
+ end
80
+
81
+ private
82
+ def attach_action(action)
83
+ @actions[action.name] = action
84
+ end
85
+ end
86
+
87
+ class Action
88
+ attr_reader :table
89
+ attr_reader :name
90
+ attr_reader :rules
91
+
92
+ def initialize(table, name)
93
+ @table = table
94
+ @name = name.to_s
95
+ @rules = []
96
+ @table.send :attach_action, self
97
+ end
98
+
99
+ def fields() @include + @exclude.map { "-#{_1}" } end
100
+
101
+ def to_s() name end
102
+
103
+ def dump
104
+ if rules.empty?
105
+ puts "#{name}: []"
106
+ else
107
+ puts name
108
+ indent {
109
+ for rule in rules
110
+ print "- "
111
+ indent(bol: false) { rule.dump }
112
+ end
113
+ }
114
+ end
115
+ end
116
+
117
+ private
118
+ def attach_rule(rule)
119
+ @rules << rule
120
+ end
121
+ end
122
+
123
+ class Rule
124
+ attr_reader :action
125
+ forward_to :action, :table, :name
126
+ attr_reader :index
127
+ attr_accessor :roles
128
+ attr_accessor :using # Goes into the postgres policy
129
+ attr_accessor :check # Goes into the postgres trigger
130
+ attr_accessor :include
131
+ attr_accessor :exclude
132
+
133
+ def auth_roles() @auth_roles ||= roles.select { _1 == _1.downcase } end
134
+ def case_roles() @case_roles ||= roles.select { _1 == _1.upcase } end
135
+
136
+ def initialize(action, index)
137
+ @action = action
138
+ @index = index
139
+ @roles, @check, @include, @exclude = [], nil, [], []
140
+ action.send :attach_rule, self
141
+ end
142
+
143
+ def fields() @include + @exclude.map { "-#{_1}" } end
144
+
145
+ def dump
146
+ puts "index: #{index}"
147
+ puts "roles: #{roles.join(' ')}"
148
+ puts "check: #{check}" if check
149
+ puts "include: #{include.join(' ')}" if !include.empty?
150
+ puts "exclude: #{exclude.join(' ')}" if !exclude.empty?
151
+ end
152
+ end
153
+ end
154
+
@@ -0,0 +1,18 @@
1
+
2
+ require 'pg_conn'
3
+ require 'indented_io'
4
+ require 'string-text'
5
+ require 'forward_to'; include ForwardTo
6
+ require 'constrain'; include Constrain
7
+ require 'prick-inflector'
8
+
9
+ module MkAcl
10
+ class ParseError < RuntimeError; end
11
+
12
+ ROLES = %w(LA TA KON AKK)
13
+ end
14
+
15
+ require_relative 'mkacl/spec.rb'
16
+ require_relative 'mkacl/parser.rb'
17
+ require_relative 'mkacl/analyzer.rb'
18
+ require_relative 'mkacl/generator.rb'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MikrasUtils
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mikras_utils/version"
4
+
5
+ module MikrasUtils
6
+ end
@@ -0,0 +1,4 @@
1
+ module MikrasUtils
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
data/tests/acl.fox ADDED
@@ -0,0 +1,312 @@
1
+
2
+
3
+ auth.roles
4
+ - &internal
5
+ rolename: internal
6
+ - &hdj
7
+ rolename: hdj
8
+ - &bkn
9
+ rolename: bkn
10
+ - &hr
11
+ rolename: hr
12
+ - &ta
13
+ rolename: ta
14
+ - &kon1
15
+ rolename: kon1
16
+ - &akk1
17
+ rolename: akk1
18
+ - &kon2
19
+ rolename: kon2
20
+ - &akk2
21
+ rolename: akk2
22
+ - &kon3
23
+ rolename: kon3
24
+ - &akk3
25
+ rolename: akk3
26
+
27
+ @schema app_portal
28
+
29
+ cases
30
+ - &case1
31
+ name: case1
32
+ - &case2
33
+ name: case2
34
+ - &case3
35
+ name: case3
36
+
37
+ role_kinds
38
+ - kind: RES
39
+ acl: true
40
+ ordinal: 1
41
+ - kind: LA
42
+ ordinal: 2
43
+ - kind: TA
44
+ ordinal: 3
45
+ - kind: KON
46
+ acl: true
47
+ ordinal: 4
48
+ - kind: AKK
49
+ acl: true
50
+ ordinal: 5
51
+ - kind: ELA
52
+ acl: true
53
+ ordinal: 6
54
+ - kind: CLA
55
+ acl: true
56
+ ordinal: 7
57
+ - kind: ETA
58
+ acl: true
59
+ ordinal: 8
60
+ - kind: CTA
61
+ acl: true
62
+ ordinal: 9
63
+
64
+ case_role_templates
65
+ - role: RES
66
+ - role: LA
67
+ - role: TA
68
+ - role: CLA
69
+ - role: CTA
70
+ - role: KON
71
+ - role: AKK
72
+
73
+ case_roles
74
+ - &case1_res
75
+ case: *case1
76
+ role: RES
77
+ - &case1_la
78
+ case: *case1
79
+ role: LA
80
+ - &case1_ta
81
+ case: *case1
82
+ role: TA
83
+ - &case1_cla
84
+ case: *case1
85
+ role: CLA
86
+ - &case1_cta
87
+ case: *case1
88
+ role: CTA
89
+ - &case1_kon
90
+ case: *case1
91
+ role: KON
92
+ - &case1_akk
93
+ case: *case1
94
+ role: AKK
95
+
96
+ - &case2_res
97
+ case: *case2
98
+ role: RES
99
+ - &case2_la
100
+ case: *case2
101
+ role: LA
102
+ - &case2_ta
103
+ case: *case2
104
+ role: TA
105
+ - &case2_Cla
106
+ case: *case2
107
+ role: CLA
108
+ - &case2_cta
109
+ case: *case2
110
+ role: CTA
111
+ - &case2_kon
112
+ case: *case2
113
+ role: KON
114
+ - &case2_akk
115
+ case: *case2
116
+ role: AKK
117
+
118
+ - &case3_res
119
+ case: *case3
120
+ role: RES
121
+ - &case3_la
122
+ case: *case3
123
+ role: LA
124
+ - &case3_ta
125
+ case: *case3
126
+ role: TA
127
+ - &case3_cla
128
+ case: *case3
129
+ role: CLA
130
+ - &case3_cta
131
+ case: *case3
132
+ role: CTA
133
+ - &case3_kon
134
+ case: *case3
135
+ role: KON
136
+ - &case3_akk
137
+ case: *case3
138
+ role: AKK
139
+
140
+ case_users
141
+ - case_role: *case1_res
142
+ user: *hdj
143
+ - case_role: *case1_la
144
+ user: *hdj
145
+ - case_role: *case1_ta
146
+ user: *bkn
147
+ - case_role: *case1_kon
148
+ user: *kon1
149
+ - case_role: *case1_akk
150
+ user: *akk1
151
+
152
+ - case_role: *case2_res
153
+ user: *hdj
154
+ - case_role: *case2_la
155
+ user: *hdj
156
+ - case_role: *case2_la
157
+ user: *hr
158
+ - case_role: *case2_ta
159
+ user: *ta
160
+ - case_role: *case2_kon
161
+ user: *kon2
162
+ - case_role: *case2_akk
163
+ user: *akk2
164
+
165
+ - case_role: *case3_res
166
+ user: *hdj
167
+ - case_role: *case3_la
168
+ user: *hdj
169
+ - case_role: *case3_ta
170
+ user: *bkn
171
+ - case_role: *case3_ta
172
+ user: *ta
173
+ - case_role: *case3_kon
174
+ user: *kon3
175
+
176
+ events
177
+ - &event11
178
+ case: *case1
179
+ name: e11
180
+ closed: true
181
+ - &event12
182
+ case: *case1
183
+ name: e12
184
+ closed: false
185
+ - &event13
186
+ case: *case1
187
+ name: e13
188
+ closed: false
189
+ - &event21
190
+ case: *case2
191
+ name: e21
192
+ closed: false
193
+
194
+ event_role_templates
195
+ - role: ELA
196
+ - role: ETA
197
+
198
+ event_roles
199
+ - &event_role11_ela
200
+ event: *event11
201
+ role: ELA
202
+ - &event_role11_eta
203
+ event: *event11
204
+ role: ETA
205
+ - &event_role11_kon
206
+ event: *event11
207
+ role: KON
208
+ - &event_role11_akk
209
+ event: *event11
210
+ role: AKK
211
+
212
+ - &event_role12_ela
213
+ event: *event12
214
+ role: ELA
215
+ - &event_role12_eta
216
+ event: *event12
217
+ role: ETA
218
+ - &event_role12_kon
219
+ event: *event12
220
+ role: KON
221
+ - &event_role12_akk
222
+ event: *event12
223
+ role: AKK
224
+
225
+ - &event_role13_ela
226
+ event: *event13
227
+ role: ELA
228
+ - &event_role13_eta
229
+ event: *event13
230
+ role: ETA
231
+ - &event_role13_kon
232
+ event: *event13
233
+ role: KON
234
+ - &event_role13_akk
235
+ event: *event13
236
+ role: AKK
237
+
238
+ - &event_role21_ela
239
+ event: *event21
240
+ role: ELA
241
+ - &event_role21_eta
242
+ event: *event21
243
+ role: ETA
244
+ - &event_role21_kon
245
+ event: *event21
246
+ role: KON
247
+ - &event_role21_akk
248
+ event: *event21
249
+ role: AKK
250
+
251
+ event_users
252
+ - event_role: *event_role11_ela
253
+ user: *hdj
254
+ - event_role: *event_role12_eta
255
+ user: *ta # No longer assciated with the case
256
+ - event_role: *event_role11_kon
257
+ user: *kon1
258
+ - event_role: *event_role11_akk
259
+ user: *akk1
260
+
261
+ # No TA on this event
262
+ - event_role: *event_role12_ela
263
+ user: *hdj
264
+ - event_role: *event_role12_kon
265
+ user: *kon1
266
+ - event_role: *event_role12_akk
267
+ user: *akk1
268
+
269
+ # Two TAs on this event
270
+ - event_role: *event_role13_ela
271
+ user: *hdj
272
+ - event_role: *event_role13_eta
273
+ user: *ta
274
+ - event_role: *event_role13_eta
275
+ user: *bkn
276
+ - event_role: *event_role13_kon
277
+ user: *kon1
278
+ - event_role: *event_role13_akk
279
+ user: *akk1
280
+
281
+ # Event-specific LA
282
+ - event_role: *event_role21_ela
283
+ user: *hr
284
+ - event_role: *event_role21_eta
285
+ user: *ta
286
+ - event_role: *event_role21_kon
287
+ user: *kon3
288
+ - event_role: *event_role21_akk
289
+ user: *akk3
290
+
291
+ visits
292
+ - &visit131
293
+ event: *event13
294
+ name: "visit 1 of event 3 of case 1"
295
+ - &visit132
296
+ event: *event13
297
+ name: "visit 2 of event 3 of case 1"
298
+ - &visit211
299
+ event: *event21
300
+ name: "visit 1 of event 1 of case 2"
301
+
302
+ noncompliances
303
+ - &nc1311
304
+ visit: *visit131
305
+ name: "NC 1 of visit 1 of event 3 of case 1"
306
+ - &nc1312
307
+ visit: *visit131
308
+ name: "NC 2 of visit 1 of event 3 of case 1"
309
+ - &nc2111
310
+ visit: *visit131
311
+ name: "NC 1 of visit 1 of event 1 of case 2"
312
+
data/tests/acl.spec ADDED
@@ -0,0 +1,135 @@
1
+
2
+ schema:
3
+ app: app_portal
4
+ acl: acl_portal
5
+
6
+ #
7
+ # CASES AND ROLES
8
+ #
9
+ # insert, update, and delete are not used for many tables because data are
10
+ # loaded from sagsys databases without policy checks and without triggers
11
+ #
12
+
13
+ cases:
14
+ domain: case
15
+ insert: admin
16
+ select: internal TA KON AKK
17
+ update:
18
+ - role: admin
19
+ - role: RES CLA
20
+ exclude: serial ident name
21
+ delete: admin
22
+
23
+ case_roles: # TODO: RBAC
24
+ select: internal KON AKK
25
+
26
+ case_users:
27
+ insert: CLA admin
28
+ select: internal
29
+ delete: CLA admin
30
+
31
+ #
32
+ # EVENTS AND VISITS
33
+ #
34
+
35
+ events:
36
+ domain: event
37
+ insert: CLA
38
+ select: internal ETA KON AKK
39
+ update:
40
+ role: CLA ELA
41
+ include: kind
42
+ delete: CLA ELA
43
+
44
+ event_roles:
45
+ select: internal ETA KON AKK
46
+
47
+ event_users:
48
+ # insert: CLA ELA
49
+ # insert:
50
+ # role: CLA ELA
51
+ # call: acl_portal.insert_event_user(NEW.event_id, NEW.role, NEW.user_id) # In instead-of trigger
52
+ # delete
53
+ # role: CLA ELA
54
+ # call: acl_portal.delete_event_user(OLD.event_id, OLD.user_id) # In instead-of trigger
55
+
56
+ select: internal ETA KON AKK
57
+
58
+ visits:
59
+ insert: CLA ELA
60
+ select: internal ETA KON AKK
61
+ update:
62
+ role: CLA ELA
63
+ include: notes closed progress progress_description visit_at mat_received_at reported_at deadline_at
64
+ delete: CLA ELA
65
+
66
+ noncompliances:
67
+ insert:
68
+ - role: CLA ELA
69
+ - role: ETA
70
+ check: true # FIXME Example
71
+ select:
72
+ - role: internal CTA
73
+ - role: KON AKK
74
+ using: true # FIXME Example
75
+ update: CLA ELA ETA KON AKK # FLS managed by trigger
76
+ delete: CLA ELA ETA KON AKK # FLS managed by trigger
77
+
78
+ noncompliance_uploads:
79
+ insert: CLA ELA ETA KON AKK
80
+ select: internal ETA KON AKK
81
+ update:
82
+ - role: CLA ELA
83
+ include: description visible
84
+ - role: ETA KON AKK
85
+ include: description
86
+ delete:
87
+ - role: CLA ELA
88
+ - role: ETA KON AKK
89
+ using: uploaded_by_id = current_user_id()
90
+
91
+ bookings:
92
+ insert: CLA ELA
93
+ select: internal ETA KON AKK
94
+ update: CLA ELA
95
+ delete: CLA ELA
96
+
97
+ user_bookings:
98
+ insert: CLA ELA
99
+ select: internal KON AKK
100
+ update:
101
+ - role: CLA ELA
102
+ - role: ETA KON AKK
103
+ using: user_id = current_user_id()
104
+ include: mask note locked
105
+ delete: CLA ELA
106
+
107
+ #
108
+ # METHOD LINES
109
+ #
110
+
111
+ #method_lines:
112
+ # insert: CLA AKK KON
113
+ # select: CLA CTA KON AKK internal
114
+ # update: CLA KON AKK
115
+ # delete: CLA KON AKK
116
+ #
117
+ #buckets:
118
+ # insert: CLA
119
+ # select: CLA CTA KON AKK internal
120
+ # update: CLA
121
+ # delete: CLA
122
+ #
123
+ #bucket_kinds:
124
+ # select: CLA CTA KON AKK internal
125
+ #
126
+ #bucket_method_lines:
127
+ # insert: CLA
128
+ # select: CLA CTA KON AKK internal
129
+ # update: CLA
130
+ # delete: CLA
131
+
132
+
133
+
134
+
135
+