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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile +10 -0
- data/README.md +31 -0
- data/Rakefile +8 -0
- data/acl-build +5 -0
- data/build +11 -0
- data/exe/mikras_utils +5 -0
- data/exe/mkacl +59 -0
- data/lib/mikras_utils/mkacl/analyzer.rb +46 -0
- data/lib/mikras_utils/mkacl/generator.rb +55 -0
- data/lib/mikras_utils/mkacl/generators/acl_functions.rb +208 -0
- data/lib/mikras_utils/mkacl/generators/id_functions.rb +262 -0
- data/lib/mikras_utils/mkacl/generators/insert_triggers.rb +267 -0
- data/lib/mikras_utils/mkacl/generators/role_functions.rb +72 -0
- data/lib/mikras_utils/mkacl/generators/rules.rb +80 -0
- data/lib/mikras_utils/mkacl/parser.rb +73 -0
- data/lib/mikras_utils/mkacl/spec.rb +154 -0
- data/lib/mikras_utils/mkacl.rb +18 -0
- data/lib/mikras_utils/version.rb +5 -0
- data/lib/mikras_utils.rb +6 -0
- data/sig/mikras_utils.rbs +4 -0
- data/tests/acl.fox +312 -0
- data/tests/acl.spec +135 -0
- data/tests/acl.sql +132 -0
- data/tests/acl_portal-functions.sql +94 -0
- data/tests/acl_portal-tables.sql +23 -0
- data/tests/acl_portal-views.sql +73 -0
- data/tests/agg.sql +25 -0
- data/tests/app.sql +48 -0
- data/tests/app_portal-tables.sql +138 -0
- data/tests/app_portal-triggers.sql +23 -0
- data/tests/app_portal-views.sql +203 -0
- data/tests/auth.sql +12 -0
- data/tests/auth.users.sql +5 -0
- data/tests/build +7 -0
- data/tests/final-functions.sql +28 -0
- data/tests/fox.sql +158 -0
- data/tests/initial-functions.sql +6 -0
- data/tests/meta.sql +197 -0
- data/tests/reflections.yml +5 -0
- data/tests/schemas.sql +25 -0
- data/tests/setup.sql +8 -0
- data/tests/sys_portal.sql +172 -0
- 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
|
+
|