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,262 @@
1
+
2
+ module MkAcl
3
+ class Generator
4
+ class IdFunctions
5
+ using String::Text
6
+
7
+ attr_reader :generator
8
+ forward_to :generator, :conn, :spec, :app_schema, :acl_schema
9
+
10
+ def initialize(generator)
11
+ @generator = generator
12
+ end
13
+
14
+ def generate
15
+ generate_per_table_id_functions
16
+ generate_general_id_functions
17
+ generate_general_id_of_record_functions
18
+ end
19
+
20
+ def self.generate(generator) self.new(generator).generate end
21
+
22
+ private
23
+ # Generate a case_id_of_RECORD and event_id_of_RECORD, and a
24
+ # domain_id_of_RECORD function for each table that returns the associated
25
+ # case_id/event_id for a given record. Some examples:
26
+ #
27
+ # case_id_of_event(id integer)
28
+ # event_id_of_noncompliance(id integer)
29
+ # domain_id_of_noncompliance(id integer)
30
+ #
31
+ # Returns null if not found. Note that the record has to exists because
32
+ # we access it to read the foreign keys
33
+ #
34
+ # The functions are used to find the roles IDs (case or event) when
35
+ # inserting a record. The role ids are written into the various acl_*
36
+ # fields by a trigger
37
+ #
38
+ # The alternative would be to cascade case and event keys in all records.
39
+ # Cascading keys are usually dereferred because it only gives a minor
40
+ # speed-up in most queries but in this case it optimizes away a recursive
41
+ # join up the hierarchy from the current table to the events or cases
42
+ # table. It may be a better solution
43
+ #
44
+ # IDEA: Make the implementations also take a record type instead of an
45
+ # id. This is useful in triggers because we already have the first record
46
+ # in the acl chain
47
+ def generate_per_table_id_functions
48
+ for domain in %w(case event)
49
+ # Create identity functions (makes some stuff easier)
50
+ table = "#{domain}s"
51
+ id_field = "#{domain}_id"
52
+ signature = "#{acl_schema}.#{id_field}_of_#{domain}(_id integer)"
53
+ puts %(
54
+ drop function if exists #{signature} cascade;
55
+ create function #{signature} returns integer as $$
56
+ select _id;
57
+ $$ language sql;
58
+ ).align
59
+ puts
60
+
61
+ # Create remaining funktions
62
+ functions = conn.tuples %(
63
+ select
64
+ start_table as "table_uid",
65
+ path as "tables",
66
+ links
67
+ from
68
+ acl.paths
69
+ where
70
+ stop_table = '#{app_schema}.#{table}'
71
+ )
72
+
73
+ for table_uid, tables, links in functions
74
+ table_name = table_uid.split(".").last
75
+ record_name = Prick::Inflector.singularize(table_name)
76
+ id_arg = "_#{id_field}"
77
+ signature = "#{acl_schema}.#{id_field}_of_#{record_name}(#{id_arg} integer)"
78
+ puts "drop function if exists #{signature} cascade;"
79
+ puts "create function #{signature} returns integer as $$"
80
+ indent {
81
+ tables.pop
82
+ end_id_field = links.pop.first
83
+
84
+ puts "select #{end_id_field}"
85
+ puts "from"
86
+ indent {
87
+ puts tables.join(",\n")
88
+ }
89
+ puts "where #{tables.first}.id = #{id_arg}"
90
+ indent {
91
+ puts links.map { |l| "and #{l.first} = #{l.last}" }.join("\n") if !links.empty?
92
+ }
93
+ puts ";"
94
+ }
95
+ puts "$$ language sql;"
96
+ puts
97
+
98
+ end
99
+ end
100
+
101
+ for table_uid in functions.map(&:first)
102
+ table_name = table_uid.split(".").last
103
+ record_name = Prick::Inflector.singularize(table_name)
104
+ signature = "#{acl_schema}.domain_id_of_#{record_name}(_id integer)"
105
+ case_function = "#{acl_schema}.case_id_of_#{record_name}"
106
+ event_function = "#{acl_schema}.event_id_of_#{record_name}"
107
+ puts %(
108
+ drop function if exists #{signature} cascade;
109
+ create function #{signature} returns integer as $$
110
+ select coalesce(#{event_function}(_id), #{case_function}(_id));
111
+ $$ language sql;
112
+ ).align
113
+ puts
114
+ end
115
+ end
116
+
117
+ # Generate general case/event id-of functions:
118
+ #
119
+ # case_id_of(table varchar, id integer)
120
+ # event_id_of(table varchar, id integer)
121
+ # domain_id_of(table varchar, id integer)
122
+ #
123
+ # These methods are practically as efficient as using the more
124
+ # specialized versions above
125
+ #
126
+ def generate_general_id_functions
127
+ for domain in %w(case event)
128
+ domain_table = "#{domain}s"
129
+ field = "#{domain}_id"
130
+ signature = "#{acl_schema}.#{field}_of(_table varchar, _id integer)"
131
+
132
+ puts %(
133
+ drop function if exists #{signature} cascade;
134
+ create function #{signature} returns integer as $$
135
+ select
136
+ ).align
137
+ indent(2) {
138
+ puts "case _table"
139
+ tables = conn.values %(
140
+ select start_table
141
+ from acl.paths
142
+ where stop_table = '#{app_schema}.#{domain}'
143
+ ).align
144
+ indent {
145
+ for table_uid in tables
146
+ table_name = table_uid.split(".").last
147
+ record_name = Prick::Inflector.singularize(table_name)
148
+ function_name = "#{field}_of_#{record_name}"
149
+ puts "when '#{table_name}' then #{acl_schema}.#{function_name}(_id)"
150
+ end
151
+ puts "when 'cases' then _id"
152
+ puts "when 'events' then _id"
153
+ puts "else null"
154
+ }
155
+ puts "end case;"
156
+ }
157
+ puts "$$ language sql;"
158
+ puts
159
+ end
160
+
161
+ signature = "#{acl_schema}.domain_id_of(_table varchar, _id integer)"
162
+ puts %(
163
+ -- FIXME: Ugly implementation
164
+ drop function if exists #{signature} cascade;
165
+ create function #{signature} returns integer as $$
166
+ select coalesce(acl_portal.event_id_of(_table, _id), acl_portal.case_id_of(_table, _id));
167
+ $$ language sql;
168
+ ).align
169
+ puts
170
+ end
171
+
172
+ # General domain-id-of-record functions
173
+ #
174
+ # case_id_of(record)
175
+ # event_id_of(record)
176
+ # domain_id_of(record)
177
+ #
178
+ # Returns the case or event id associated with the given record. The
179
+ # record should be an ACL table record
180
+ #
181
+ # Very useful in before insert triggers because it takes a record (eg.
182
+ # NEW) instead of an ID
183
+ #
184
+ def generate_general_id_of_record_functions
185
+ links = (conn.structs %(
186
+ select
187
+ table_name,
188
+ column_name,
189
+ ref_table_name
190
+ from
191
+ acl.links
192
+ where
193
+ schema_name = '#{app_schema}'
194
+ )).group_by(&:table_name)
195
+
196
+ for domain in %w(case event)
197
+ domain_id_field = "#{domain}_id"
198
+ signature = "#{acl_schema}.#{domain_id_field}_of(_r record)"
199
+ puts %(
200
+ drop function if exists #{signature} cascade;
201
+ create function #{signature} returns integer as $$
202
+ ).align
203
+ indent {
204
+ puts %(
205
+ declare
206
+ type text;
207
+ begin
208
+ select pg_typeof(_r)::text into type;
209
+ case type
210
+ ).align
211
+ indent {
212
+ indent {
213
+ for table in spec.tables
214
+ next if domain == "event" && table.domain == "case"
215
+ link = links[table.name]&.first
216
+ if table.name == "cases"
217
+ puts "when '#{table.uid}' then return _r.id;"
218
+ elsif domain == "event" && table.name == "events"
219
+ puts "when '#{table.uid}' then return _r.id;"
220
+ elsif link.ref_table_name == "cases"
221
+ puts "when '#{table.uid}' then return _r.#{link.column_name};"
222
+ elsif link.ref_table_name == "events" && table.domain == "event"
223
+ puts "when '#{table.uid}' then return _r.#{link.column_name};"
224
+ else
225
+ ref_record = spec[link.ref_table_name].record_name
226
+ id_of_function = "#{acl_schema}.#{domain_id_field}_of_#{ref_record}"
227
+ puts "when '#{table.uid}' then return #{id_of_function}(_r.#{link.column_name});"
228
+ end
229
+ end
230
+ puts "else return null;"
231
+ }
232
+ puts "end case;"
233
+ }
234
+ puts "end;"
235
+ }
236
+ puts %(
237
+ $$ language plpgsql
238
+ set search_path to '';
239
+ ).align
240
+ puts
241
+ end
242
+
243
+ signature = "#{acl_schema}.domain_id_of(_r record)"
244
+ puts %(
245
+ -- Note: Ugly implementation but maybe close to optimal
246
+ drop function if exists #{signature} cascade;
247
+ create function #{signature} returns integer as $$
248
+ declare
249
+ _id integer;
250
+ begin
251
+ select coalesce(acl_portal.event_id_of(_r), acl_portal.case_id_of(_r))
252
+ into _id;
253
+
254
+ return _id;
255
+ end;
256
+ $$ language plpgsql;
257
+ ).align
258
+ puts
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,267 @@
1
+ module MkAcl
2
+ class Generator
3
+ class InsertTriggers
4
+ using String::Text
5
+
6
+ attr_reader :generator
7
+ forward_to :generator, :conn, :spec, :app_schema, :acl_schema
8
+
9
+ def initialize(generator)
10
+ @generator = generator
11
+ end
12
+
13
+ def generate(table)
14
+ generate_bi_trigger(table)
15
+ generate_ai_trigger(table)
16
+ end
17
+
18
+ def self.generate(generator, table) self.new(generator).generate(table) end
19
+
20
+ private
21
+ # Generate a TABLE_rls_bi() trigger function for each table. The trigger
22
+ # checks the current role against the attach_acls table for each foreign
23
+ # key
24
+ #
25
+ def generate_bi_trigger(table)
26
+ function_name = "#{table}_rls_bi"
27
+ trigger_name = "#{function_name}_trg"
28
+ function_uid = "#{acl_schema}.#{function_name}"
29
+ signature = "#{function_uid}()"
30
+
31
+ puts "drop function if exists #{signature} cascade;"
32
+ puts "create function #{signature} returns trigger as $$"
33
+ indent {
34
+ puts %(
35
+ declare
36
+ domain_user_roles varchar[];
37
+ begin
38
+ ).align
39
+ indent {
40
+ generate_bi_trigger_foreign_key_rls(table)
41
+ generate_bi_trigger_check_conditions(table)
42
+ puts "return NEW;"
43
+ }
44
+ puts "end;"
45
+ }
46
+ puts "$$ language plpgsql;"
47
+ puts
48
+
49
+ puts %(
50
+ create trigger #{trigger_name} before insert on #{table.uid}
51
+ for each row execute function #{signature};
52
+ ).align
53
+ puts
54
+ end
55
+
56
+ def generate_bi_trigger_foreign_key_rls(table)
57
+ puts "-- Foreign key RLS check(s)"
58
+ fields = conn.values %(
59
+ select
60
+ column_name as field
61
+ from
62
+ acl.links
63
+ where
64
+ table_name = '#{table}'
65
+ )
66
+
67
+ for field in fields
68
+ puts %(
69
+ perform
70
+ from acl_portal.attach_acls
71
+ where child_table = '#{table}'
72
+ and child_field = '#{field}'
73
+ and parent_id = NEW.#{field}
74
+ and acls && public.current_role_ids();
75
+
76
+ if not found then
77
+ raise 'Illegal value in #{table}.#{field} - security violation';
78
+ end if;
79
+ ).align
80
+ end
81
+ puts
82
+ end
83
+
84
+ def generate_bi_trigger_check_conditions(table)
85
+ using_actions = table.insert.rules.select(&:using)
86
+ return if using_actions.empty?
87
+ domain = table.domain
88
+
89
+ puts %(
90
+ -- Per-role(s) using(s)
91
+ select
92
+ acl_portal.#{domain}_user_roles_of(acl_portal.#{domain}_id_of(NEW), public.current_role_id())
93
+ into domain_user_roles
94
+ ;
95
+ ).align
96
+ puts
97
+
98
+ for action in using_actions
99
+ role_list = "'" + action.roles.join(', ') + "'"
100
+ puts %(
101
+ if array[#{role_list}] && domain_user_roles then
102
+ if not (#{action.using}) then
103
+ raise 'Check condition failed - security violation';
104
+ end if;
105
+ end
106
+ ).align
107
+ end
108
+ end
109
+
110
+ # Generate a TABLE_rls_ai() trigger for each table. The trigger creates
111
+ # an entry in attach_acls for each table refering the current table
112
+ #
113
+ # TODO: Delete trigger to cleanup attach_acls
114
+ #
115
+ def generate_ai_trigger(table)
116
+ function_name = "#{table}_rls_ai"
117
+ trigger_name = "#{function_name}_trg"
118
+ function_uid = "#{acl_schema}.#{function_name}"
119
+ signature = "#{function_uid}()"
120
+
121
+ puts "drop function if exists #{signature} cascade;"
122
+ puts "create function #{signature} returns trigger as $$"
123
+ indent {
124
+ puts %(
125
+ declare
126
+ _domain_id integer;
127
+ begin
128
+ ).align
129
+ indent {
130
+ puts "-- Find security domain id"
131
+ puts "_domain_id := #{acl_schema}.#{table.domain}_id_of(NEW);"
132
+ puts
133
+ generate_ai_trigger_insert_domain_roles(table) if %w(cases events).include?(table.name)
134
+ generate_ai_trigger_create_acls(table)
135
+ # generate_ai_trigger_set_acl_fields(table)
136
+ # generate_ai_trigger_attach_acls(table)
137
+ puts "return NEW;"
138
+ }
139
+ puts "end;"
140
+ }
141
+ puts "$$ language plpgsql;"
142
+ puts
143
+
144
+ puts %(
145
+ create trigger #{trigger_name} after insert on #{table.uid}
146
+ for each row execute function #{signature};
147
+ ).align
148
+ puts
149
+ end
150
+
151
+ def generate_ai_trigger_insert_domain_roles(table)
152
+ domain = table.domain
153
+ domain_roles_table = "#{app_schema}.#{domain}_roles"
154
+ domain_role_templates_table = "#{app_schema}.#{domain}_role_templates"
155
+ domain_id_field = "#{domain}_id"
156
+ puts %(
157
+ -- Generate domain roles
158
+ insert into #{domain_roles_table} (#{domain_id_field}, role)
159
+ select NEW.id as #{domain_id_field}, role
160
+ from #{domain_role_templates_table};
161
+ ).align
162
+ puts
163
+ end
164
+
165
+ def generate_ai_trigger_create_acls(table)
166
+ puts %(
167
+ -- Generate ACLs
168
+ perform #{table}_update_acls(NEW.id);
169
+ ).align
170
+ puts
171
+ end
172
+
173
+ # def generate_ai_trigger_set_acl_fields(table)
174
+ # domain = table.domain
175
+ # id_field = "#{domain}_id"
176
+ # domain_roles_table = "#{app_schema}.#{domain}_roles"
177
+ # domain_id_function = "#{acl_schema}.#{id_field}_of"
178
+ #
179
+ # for action_name in %w(select update delete)
180
+ # puts "-- Find #{action_name} ACLs"
181
+ # acl_field = "NEW.acl_#{action_name}"
182
+ # puts "#{acl_field} := array[]::integer[][];"
183
+ # puts
184
+ #
185
+ # for rule in table.actions[action_name].rules
186
+ # auth_roles = rule.roles.select { _1 == _1.downcase }
187
+ # case_roles = rule.roles.select { _1 == _1.upcase }
188
+ #
189
+ # if !auth_roles.empty?
190
+ # role_seq = conn.quote_value_seq(auth_roles)
191
+ # puts %(
192
+ # select #{acl_field} || array_agg(id)
193
+ # from auth.roles
194
+ # where rolename = any(array[#{role_seq}]);
195
+ # ).align
196
+ # puts
197
+ # end
198
+ #
199
+ # if !case_roles.empty?
200
+ # role_seq = conn.quote_value_seq(case_roles)
201
+ # puts %(
202
+ # select #{acl_field} || array_agg(id)
203
+ # into #{acl_field}
204
+ # from #{domain_roles_table}
205
+ # where #{id_field} = _domain_id
206
+ # and role = any(array[#{role_seq}]);
207
+ # ).align
208
+ # puts
209
+ # end
210
+ # end
211
+ # end
212
+ # puts %(
213
+ # -- Update ACL fields
214
+ # update #{table.uid}
215
+ # set acl_select = NEW.acl_select,
216
+ # acl_update = NEW.acl_update,
217
+ # acl_delete = NEW.acl_delete
218
+ # where id = NEW.id;
219
+ # ).align
220
+ # puts
221
+ # end
222
+ #
223
+ # def generate_ai_trigger_attach_acls(table)
224
+ # record = table.record_name
225
+ #
226
+ # domain = table.domain
227
+ # domain_table = "#{domain}s"
228
+ # domain_roles_table ="#{domain}_roles"
229
+ #
230
+ # # Insert a attach record per parent table. This snippet handles
231
+ # # multiple parents of a record but the check algorithm probably does
232
+ # # not TODO
233
+ # roles = table.insert.rules.map(&:roles).flatten
234
+ # role_list = conn.quote_value_seq(roles)
235
+ # for parent in table.parents
236
+ # id_fields = conn.values %(select column_name from acl.links where table_name = '#{table}')
237
+ # for id_field in id_fields
238
+ # id_of_function = "acl_portal.#{table.domain}_id_of"
239
+ #
240
+ # puts %(
241
+ # -- Generate attach ACLs
242
+ # insert into acl_portal.attach_acls (parent_table, parent_id, child_table, child_field, acls)
243
+ # select
244
+ # '#{parent.name}' as "parent_table",
245
+ # NEW.id as "parent_id",
246
+ # l.table_name as "child_table",
247
+ # l.column_name as "child_field",
248
+ # array_agg(dr.id) as "acls"
249
+ # from
250
+ # acl.links l,
251
+ # #{domain_table} d
252
+ # join #{domain_roles_table} dr on dr.event_id = d.id
253
+ # where l.table_name = '#{table.name}'
254
+ # and d.id = _domain_id
255
+ # and dr.role = any(array[#{role_list}])
256
+ # group by
257
+ # "parent_table", "parent_id",
258
+ # "child_table", "child_field";
259
+ # ).align
260
+ # puts
261
+ # end
262
+ # end
263
+ # end
264
+ end
265
+ end
266
+ end
267
+
@@ -0,0 +1,72 @@
1
+ module MkAcl
2
+ class Generator
3
+ class RoleFunctions
4
+ using String::Text
5
+
6
+ attr_reader :generator
7
+ forward_to :generator, :conn, :spec, :app_schema, :acl_schema
8
+
9
+ def initialize(generator)
10
+ @generator = generator
11
+ end
12
+
13
+ def generate
14
+ generate_role_functions
15
+ end
16
+
17
+ def self.generate(generator) self.new(generator).generate end
18
+
19
+ private
20
+ def generate_role_functions
21
+ # TODO: Test. Then combine with per-table methods
22
+ signature = "public.current_is_role(_case_id integer, _roles text[])"
23
+ puts %(
24
+ drop function if exists #{signature} cascade;
25
+ create function #{signature} returns boolean as $$
26
+ select exists(
27
+ select
28
+ from #{app_schema}.case_roles cr
29
+ join #{app_schema}.case_role_users cru on cru.case_role_id = cr.id
30
+ where cr.case_id = _case_id
31
+ and cru.user_id = public.current_user_id()
32
+ and cr.kind = any(_roles)
33
+ );
34
+ $$ language sql
35
+ security definer;
36
+ ).align
37
+ puts
38
+
39
+ signature = "public.current_is_role(_case_id integer, role text)"
40
+ puts %(
41
+ drop function if exists #{signature} cascade;
42
+ create function #{signature} returns boolean as $$
43
+ select public.current_is_role(_case_id, array[role]);
44
+ $$ language sql
45
+ security definer;
46
+ ).align
47
+ puts
48
+
49
+ for role in MkAcl::ROLES
50
+ name = role.downcase
51
+ signature = "public.current_is_#{name}(_case_id integer)"
52
+ puts %(
53
+ drop function if exists #{signature} cascade;
54
+ create function #{signature} returns boolean as $$
55
+ select exists(
56
+ select
57
+ from #{app_schema}.case_roles cr
58
+ join #{app_schema}.case_role_users cru on cru.case_role_id = cr.id
59
+ where cr.case_id = _case_id
60
+ and cru.user_id = public.current_user_id()
61
+ and cr.kind = '#{role}'
62
+ );
63
+ $$ language sql
64
+ security definer;
65
+ ).align
66
+ puts
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+
@@ -0,0 +1,80 @@
1
+
2
+ module MkAcl
3
+ class Generator
4
+ # Generate a select, update, and delete rule for each table. Create rules
5
+ # are implemented as triggers and delete should probably also be. The
6
+ # problem is that a record that fails the rule check is silently ignored
7
+ # which is probably not what you want
8
+ #
9
+ # The roles matches a acl_* array against a role action entry in the spec
10
+ # file. The acl_* arrays are themselves array of role ids. Each subarray is
11
+ # indexed using the order in the acl.spec file
12
+ #
13
+ # This is not the optimal solution because it makes both data and code more
14
+ # complex. An alternative would be to unify the roles id of all rules in an
15
+ # action and then let a trigger take case of 'check' and 'field' checks
16
+ #
17
+ class Rules
18
+ using String::Text
19
+
20
+ attr_reader :generator
21
+ forward_to :generator, :conn, :spec, :app_schema, :acl_schema
22
+
23
+ def initialize(generator)
24
+ @generator = generator
25
+ end
26
+
27
+ def generate
28
+ generate_rules
29
+ generate_rule_functions
30
+ end
31
+
32
+ private
33
+ def generate_rule_functions
34
+ for action in %w(enable disable)
35
+ signature = "#{acl_schema}.#{action}_rls()"
36
+ stmts = spec.tables.map { |table|
37
+ "alter table #{app_schema}.#{table} #{action} row level security;"
38
+ }
39
+ puts %(
40
+ drop function if exists #{signature} cascade;
41
+ create function #{signature} returns boolean as $$
42
+ begin
43
+ ).align
44
+ indent(2) { puts stmts }
45
+ puts %(
46
+ return true;
47
+ end;
48
+ $$ language plpgsql;
49
+ ).align
50
+ puts
51
+ end
52
+ end
53
+
54
+ def generate_rules
55
+ for table in spec.tables
56
+ for action in %w(select update delete).map { table.actions[_1] }
57
+ rule_expr = action.rules.map { |rule|
58
+ exprs = []
59
+ exprs << "acl_#{action.name}[#{rule.index}:#{rule.index}][1:] && public.current_role_ids()" \
60
+ if !rule.roles.empty?
61
+ exprs << "(#{rule.using})" if rule.using
62
+ "(#{exprs.join(' and ')})"
63
+ }.join(" or ")
64
+ rule_expr = "false" if rule_expr.empty?
65
+
66
+ puts %(
67
+ drop policy if exists rls_#{action.name} on #{table.uid} cascade;
68
+ create policy rls_#{action.name} on #{table.uid} for #{action.name}
69
+ using (#{rule_expr});
70
+ ).align
71
+ puts
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.generate(generator) self.new(generator).generate end
77
+ end
78
+ end
79
+ end
80
+