mikras_utils 0.1.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 (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
+