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,203 @@
1
+ \set ON_ERROR_STOP on
2
+
3
+ set search_path to app_portal;
4
+
5
+ -- Union of case_roles and event_roles
6
+ drop view if exists acl_portal.domain_roles cascade;
7
+ create view acl_portal.domain_roles as
8
+ select
9
+ id,
10
+ case_id as "domain_id",
11
+ role
12
+ from
13
+ app_portal.case_roles
14
+
15
+ union all
16
+
17
+ select
18
+ id,
19
+ event_id as "domain_id",
20
+ role
21
+ from
22
+ app_portal.event_roles
23
+ ;
24
+
25
+ -- Union of case_users and event_users
26
+ drop view if exists acl_portal.domain_users cascade;
27
+ create view acl_portal.domain_users as
28
+ select
29
+ case_role_id as "domain_role_id",
30
+ user_id
31
+ from
32
+ app_portal.case_users
33
+
34
+ union all
35
+
36
+ select
37
+ event_role_id as "domain_role_id",
38
+ user_id
39
+ from
40
+ app_portal.event_users
41
+ ;
42
+
43
+ -- Combines cases, case_roles, and case_users and also computes the virtual CLA
44
+ -- and CTA roles from the event ELA and ETA roles and assigns the CLA role to
45
+ -- RES users. Only ACL roles are included
46
+ drop view if exists case_role_users cascade;
47
+ create view case_role_users as
48
+ with
49
+ -- Base roles
50
+ roles as (
51
+ select
52
+ cr.case_id,
53
+ cu.user_id,
54
+ cr.id as "role_id",
55
+ cr.role
56
+ from app_portal.role_kinds rk
57
+ join app_portal.case_roles cr on cr.role = rk.kind
58
+ join app_portal.case_users cu on cu.case_role_id = cr.id
59
+ where rk.acl
60
+ ),
61
+ -- RES user is also CLA
62
+ res_roles as (
63
+ select
64
+ r.case_id,
65
+ r.user_id,
66
+ cr.id as "role_id",
67
+ cr.role
68
+ from roles r
69
+ join case_roles cr on cr.case_id = r.case_id and cr.role = 'CLA'
70
+ where r.role = 'RES'
71
+ ),
72
+ -- Event roles ELA and ETA are translated to CLA and CTA case roles
73
+ eta_ela_roles as (
74
+ select
75
+ e.case_id,
76
+ eu.user_id,
77
+ cr.id,
78
+ cr.role
79
+ from
80
+ events e
81
+ join event_roles er on er.event_id = e.id
82
+ join event_users eu on eu.event_role_id = er.id
83
+ join case_roles cr on
84
+ cr.case_id = e.case_id
85
+ and (
86
+ (cr.role = 'CTA' and er.role = 'ETA')
87
+ or (cr.role = 'CLA' and er.role = 'ELA')
88
+ )
89
+ where not e.closed
90
+ )
91
+ select * from roles
92
+ union
93
+ select * from res_roles
94
+ union
95
+ select * from eta_ela_roles
96
+ order by case_id, user_id -- FIXME: Yt
97
+ ;
98
+
99
+ -- Combines case_role_users with event_roles and event_users. It is the set of
100
+ -- roles (case or event) that is associated with an event. Users that are no
101
+ -- longer associated with the case are excluded
102
+ drop view if exists event_role_users cascade;
103
+ create view event_role_users as
104
+ -- Roles inherited from case
105
+ select
106
+ e.id as "event_id",
107
+ cru.user_id,
108
+ cru.role_id,
109
+ cru.role
110
+ from
111
+ events e
112
+ join case_role_users cru on cru.case_id = e.case_id
113
+ where
114
+ not e.closed
115
+
116
+ union all
117
+
118
+ -- Roles from event users. KON and AKK are duplicates
119
+ select
120
+ e.id as "event_id",
121
+ eu.user_id,
122
+ er.id as "role_id",
123
+ er.role
124
+ from
125
+ events e
126
+ join event_roles er on er.event_id = e.id
127
+ join event_users eu on eu.event_role_id = er.id
128
+ where
129
+ not e.closed
130
+
131
+ order by event_id -- FIXME Yt
132
+ ;
133
+
134
+ drop view if exists domain_role_users cascade;
135
+ create view domain_role_users as
136
+ select
137
+ case_id as "domain_id",
138
+ user_id,
139
+ role_id,
140
+ role
141
+ from
142
+ case_role_users
143
+
144
+ union all
145
+
146
+ select
147
+ event_id as "domain_id",
148
+ user_id,
149
+ role_id,
150
+ role
151
+ from
152
+ event_role_users
153
+ ;
154
+
155
+ drop view if exists agg.domain_role_users cascade;
156
+ create view agg.domain_role_users as
157
+ select
158
+ domain_id,
159
+ user_id,
160
+ array_agg(role_id) as "role_ids",
161
+ array_agg(role) as "roles"
162
+ from
163
+ app_portal.domain_role_users
164
+ group by
165
+ domain_id,
166
+ user_id
167
+ ;
168
+
169
+ drop view if exists name.domain_role_users cascade;
170
+ create view name.domain_role_users as
171
+ select
172
+ dru.domain_id,
173
+ dru.user_id,
174
+ dru.role_id,
175
+ coalesce(c.ident, e.label) as "domain",
176
+ u.rolename as "username",
177
+ dru.role
178
+ from
179
+ domain_role_users dru
180
+ join auth.roles u on u.id = dru.user_id
181
+ left join cases c on c.id = dru.domain_id
182
+ left join events e on e.id = dru.domain_id
183
+ ;
184
+
185
+ drop view if exists agg_name.domain_role_users cascade;
186
+ create view agg_name.domain_role_users as
187
+ select
188
+ domain_id,
189
+ user_id,
190
+ domain,
191
+ username,
192
+ array_agg(role_id) as "role_ids",
193
+ array_agg(role) as "roles"
194
+ from
195
+ name.domain_role_users
196
+ group by
197
+ domain_id,
198
+ user_id,
199
+ domain,
200
+ username
201
+ order by domain_id, username
202
+ ;
203
+
data/tests/auth.sql ADDED
@@ -0,0 +1,12 @@
1
+ \set ON_ERROR_STOP on
2
+
3
+ create table auth.roles (
4
+ id integer generated by default as identity primary key,
5
+ rolename varchar not null
6
+ );
7
+
8
+ create table auth.users (
9
+ id integer generated by default as identity primary key,
10
+ rolename varchar not null
11
+ );
12
+
@@ -0,0 +1,5 @@
1
+ -- FIXME
2
+ /*
3
+ delete from auth.users;
4
+ insert into auth.users select * from auth.roles;
5
+ */
data/tests/build ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/bash
2
+
3
+ . bash.include
4
+
5
+ psql -f app.sql
6
+ fox --delete=touched -r reflections.yml clr acl.fox | psql clr
7
+ psql -f fox.sql
@@ -0,0 +1,28 @@
1
+
2
+ -- Returns an array of role IDs for the current user (from both auth and
3
+ -- app_portal). This array is matched against the relevant ACL field in the RLS
4
+ -- policies so performance is important (TODO)
5
+ drop function if exists public.current_role_ids() cascade;
6
+ create function public.current_role_ids() returns integer[] as $$
7
+ select acl_portal.select_user_acl(public.current_user_id());
8
+ $$ language sql
9
+ stable
10
+ leakproof
11
+ security definer
12
+ ;
13
+
14
+ -- Returns an array of app_portal role names (kind) for the current user in the
15
+ -- given domain
16
+ drop function if exists public.current_domain_roles(integer) cascade;
17
+ create function public.current_domain_roles(_domain_id integer) returns text[] as $$
18
+ select coalesce(array_agg(dr.role), array[]::text[])
19
+ from acl_portal.domain_users du
20
+ join acl_portal.domain_roles dr on dr.id = du.domain_role_id
21
+ where du.user_id = public.current_user_id()
22
+ and dr.domain_id = _domain_id;
23
+ $$ language sql
24
+ stable
25
+ leakproof
26
+ security definer
27
+ ;
28
+
data/tests/fox.sql ADDED
@@ -0,0 +1,158 @@
1
+ \set ON_ERROR_STOP on
2
+
3
+ /*
4
+ delete from acl_portal.attach_acls;
5
+
6
+ insert into acl_portal.attach_acls (parent_table, parent_id, child_table, child_field, acls) values
7
+ ('cases', 1, 'events', 'case_id', array[1]), -- case-hdj can create events on case 1
8
+ ('events', 3, 'visits', 'event_id', array[16]), -- event-hdj can create visits on event 3 of case 1
9
+ ('visits', 1, 'noncompliances', 'visit_id', array[16, 17]),
10
+ ('visits', 2, 'noncompliances', 'visit_id', array[16, 2]),
11
+ ('events', 3, 'bookings', 'event_id', array[16])
12
+ ;
13
+
14
+ insert into acl_portal.attach_acls (parent_table, parent_id, child_table, child_field, acls)
15
+ select
16
+ 'case_roles' as "parent_table",
17
+ id as "parent_id",
18
+ 'case_users' as "child_table",
19
+ 'case_role_id' as "child_id",
20
+ array[2] -- hdj
21
+ from
22
+ app_portal.case_roles
23
+ where case_id = 1
24
+ and role = 'CLA'
25
+ ;
26
+ */
27
+
28
+ delete from auth.users;
29
+ insert into auth.users select * from auth.roles;
30
+
31
+ select acl_portal.update_acls();
32
+ select acl_portal.update_user_acls();
33
+
34
+ /*
35
+ drop schema if exists acl cascade;
36
+ create schema acl;
37
+ set search_path to acl;
38
+
39
+ create view tables as
40
+ select distinct
41
+ schema_name,
42
+ table_name
43
+ from
44
+ meta.columns
45
+ where
46
+ column_name like 'acl\_%'
47
+ ;
48
+
49
+ \echo ACL_TABLES
50
+ select * from tables where schema_name = 'app_portal';
51
+
52
+ -- Subset of meta.columns where both referencing and referenced tables are ACL tables
53
+ drop view if exists links cascade;
54
+ create view links as
55
+ select c.*
56
+ from tables tf
57
+ join meta.links c
58
+ on c.schema_name = tf.schema_name
59
+ and c.table_name = tf.table_name
60
+ join tables tt
61
+ on tt.schema_name = c.ref_schema_name
62
+ and tt.table_name = c.ref_table_name
63
+ ;
64
+
65
+ \echo ACL_LINKS
66
+ select * from links where schema_name = 'app_portal';
67
+
68
+ -- Subset of meta.chains where first and last table are ACL tables
69
+ drop view if exists chains cascade;
70
+ create view chains as
71
+ select mc.*
72
+ from meta.chains mc
73
+ join tables src on
74
+ mc.src_schema_name = src.schema_name
75
+ and mc.src_table_name = src.table_name
76
+ join tables dst on
77
+ mc.dst_schema_name = dst.schema_name
78
+ and mc.dst_table_name = dst.table_name
79
+ ;
80
+
81
+ \echo ACL_CHAINS
82
+ --select * from chains where src_schema_name = 'app_portal';
83
+
84
+ drop view if exists closures cascade;
85
+ create view closures as
86
+ select
87
+ at.schema_name,
88
+ at.table_name,
89
+ coalesce(
90
+ array_agg(ac.src_schema_name || '.' || ac.src_table_name) filter (where ac.src_schema_name is not null),
91
+ array[]::varchar[]
92
+ ) as closure_uids
93
+ from
94
+ tables at
95
+ left join chains ac
96
+ on ac.dst_schema_name = at.schema_name
97
+ and ac.dst_table_name = at.table_name
98
+ group by
99
+ at.schema_name,
100
+ at.table_name
101
+ ;
102
+
103
+ \echo ACL_CLOSURES
104
+ --select * from closures;
105
+
106
+ drop view if exists paths cascade;
107
+ create view paths as
108
+ with
109
+ recursive search_path as (
110
+ with edges as (
111
+ select distinct
112
+ schema_name || '.' || table_name as "from_table",
113
+ ref_schema_name || '.' || ref_table_name as "to_table",
114
+ column_name as "from_column",
115
+ ref_column_name as "to_column"
116
+ from
117
+ acl.links
118
+ )
119
+
120
+ -- Anchor member: start from the initial node
121
+ select
122
+ from_table as "start_table",
123
+ from_table,
124
+ to_table,
125
+ array[from_table] as path,
126
+ array[array[from_table || '.' || from_column, to_table || '.' || to_column]] as link,
127
+ false as cycle
128
+ from edges
129
+
130
+ union all
131
+
132
+ -- Recursive member: find the next links
133
+ select
134
+ sp.start_table,
135
+ e.from_table,
136
+ e.to_table,
137
+ sp.path || e.from_table as "path",
138
+ sp.link || array[array[e.from_table || '.' || e.from_column, e.to_table || '.' || e.to_column]] as "link",
139
+ e.from_table = any(sp.path) as "cycle"
140
+ from search_path sp
141
+ join edges e on e.from_table = sp.to_table
142
+ where not sp.cycle
143
+ )
144
+ select
145
+ start_table,
146
+ to_table as "stop_table",
147
+ path || to_table as "path",
148
+ link as "links"
149
+ from search_path
150
+ order by
151
+ start_table,
152
+ "stop_table"
153
+ ;
154
+
155
+ \echo ACL_PATHS
156
+ --select * from paths;
157
+
158
+ */
@@ -0,0 +1,6 @@
1
+
2
+ drop function if exists public.current_user_id() cascade;
3
+ create function public.current_user_id() returns integer as $$
4
+ select 2
5
+ $$ language sql;
6
+
data/tests/meta.sql ADDED
@@ -0,0 +1,197 @@
1
+
2
+ \set ON_ERROR_STOP on
3
+
4
+ drop schema if exists meta cascade;
5
+ create schema meta;
6
+ set search_path to meta;
7
+
8
+ create view schemas as
9
+ select
10
+ nspname as "schema_name"
11
+ from
12
+ pg_namespace
13
+ where nspname <> 'information_schema'
14
+ and nspname not like 'pg\_%'
15
+ and nspname not like 'fdw\_%'
16
+ and nspname not in ('pgcrypto') -- array because more libraries may come
17
+ ;
18
+
19
+ create view tables as
20
+ select
21
+ relnamespace::regnamespace::varchar as "schema_name",
22
+ relname as "table_name"
23
+ from
24
+ pg_class
25
+ where
26
+ relkind = 'r'
27
+ and relnamespace::regnamespace::varchar <> 'information_schema'
28
+ and relnamespace::regnamespace::varchar not like 'pg\_%'
29
+ -- FIXME relation "fdw_webtoolpublished.publisheddata" does not exist
30
+ and relnamespace::regnamespace::varchar not like 'fdw\_%'
31
+ ;
32
+
33
+ \echo META_TABLES
34
+ select *
35
+ from tables
36
+ where
37
+ schema_name = 'app_portal'
38
+ ;
39
+
40
+ create view columns as
41
+ with
42
+ referencing_fields as (
43
+ select
44
+ tf.relnamespace::regnamespace::varchar as "schema_name",
45
+ tf.relname as "table_name", -- avoids qualified table name
46
+ af.attname as "column_name",
47
+ tt.relnamespace::regnamespace::varchar as "ref_schema_name",
48
+ tt.relname as "ref_table_name", -- avoids qualified table name
49
+ at.attname as "ref_column_name"
50
+ from
51
+ pg_constraint as c
52
+ join pg_attribute as af on af.attnum = any(c.conkey) and af.attrelid = c.conrelid
53
+ join pg_attribute as at on at.attnum = any(c.confkey) and at.attrelid = c.confrelid
54
+ join pg_class tf on tf.oid = c.conrelid
55
+ join pg_class tt on tt.oid = c.confrelid
56
+ where
57
+ c.contype = 'f'
58
+ order by
59
+ "schema_name",
60
+ "table_name",
61
+ "column_name"
62
+ )
63
+ select
64
+ t.schema_name,
65
+ t.table_name,
66
+ af.attname as "column_name",
67
+ at.ref_schema_name,
68
+ at.ref_table_name,
69
+ at.ref_column_name,
70
+ af.attnum as "ordinal"
71
+ from
72
+ tables t
73
+ join pg_attribute af
74
+ on af.attrelid = (schema_name || '.' || table_name)::regclass
75
+ and af.attnum > 0
76
+ left join referencing_fields at
77
+ on at.schema_name = t.schema_name
78
+ and at.table_name = t.table_name
79
+ and at.column_name = af.attname
80
+ order by
81
+ "schema_name",
82
+ "table_name",
83
+ "ordinal"
84
+ ;
85
+
86
+ \echo META_COLUMNS
87
+ /*
88
+ select
89
+ schema_name, table_name, column_name,
90
+ ref_schema_name, ref_table_name, ref_column_name
91
+ from columns
92
+ where
93
+ schema_name = 'app_portal'
94
+ ;
95
+ */
96
+
97
+ create view links as
98
+ select *
99
+ from columns
100
+ where ref_table_name is not null
101
+ ;
102
+
103
+ \echo META_LINKS
104
+ --select * from links;
105
+
106
+ create view chains as
107
+ with
108
+ recursive search_path as (
109
+ with
110
+ edges as (
111
+ select distinct
112
+ schema_name || '.' || table_name as "from_table",
113
+ ref_schema_name || '.' || ref_table_name as "to_table",
114
+ column_name as "from_column",
115
+ ref_column_name as "to_column"
116
+ from
117
+ links
118
+ )
119
+
120
+ -- Anchor member: src from the initial node
121
+ select
122
+ from_table as "src_table",
123
+ from_table,
124
+ to_table,
125
+ array[from_table] as path,
126
+ array[array[from_table || '.' || from_column, to_table || '.' || to_column]] as link,
127
+ false as cycle
128
+ from edges
129
+
130
+ union all
131
+
132
+ -- Recursive member: find the next links
133
+ select
134
+ sp.src_table,
135
+ e.from_table,
136
+ e.to_table,
137
+ sp.path || e.from_table as "path",
138
+ sp.link || array[array[e.from_table || '.' || e.from_column, e.to_table || '.' || e.to_column]] as "link",
139
+ e.from_table = any(sp.path) as "cycle"
140
+ from search_path sp
141
+ join edges e on e.from_table = sp.to_table
142
+ where not sp.cycle
143
+ )
144
+ select
145
+ regexp_replace(src_table, '\..*', '') as src_schema_name,
146
+ regexp_replace(src_table, '^.*\.', '') as src_table_name,
147
+ regexp_replace(to_table, '\..*', '') as dst_schema_name,
148
+ regexp_replace(to_table, '^.*\.', '') as dst_table_name,
149
+ path || to_table as "path",
150
+ link as "links"
151
+ from search_path
152
+ order by
153
+ "src_schema_name",
154
+ "src_table_name",
155
+ "dst_schema_name",
156
+ "dst_table_name"
157
+ ;
158
+
159
+ \echo META_CHAINS
160
+ --select * from chains;
161
+
162
+ create view closures as
163
+ select
164
+ mt.schema_name,
165
+ mt.table_name,
166
+ coalesce(
167
+ array_agg(mc.src_schema_name || '.' || mc.src_table_name) filter (where mc.src_schema_name is not null),
168
+ array[]::varchar[]
169
+ ) as closure_uids
170
+ from
171
+ tables mt
172
+ left join chains mc
173
+ on mc.dst_schema_name = mt.schema_name
174
+ and mc.dst_table_name = mt.table_name
175
+ group by
176
+ mt.schema_name,
177
+ mt.table_name
178
+ ;
179
+
180
+ \echo META_CLOSURES
181
+ --select * from closures;
182
+
183
+ create view functions as
184
+ select
185
+ s.schema_name,
186
+ proname as "function_name",
187
+ string_to_array(pg_catalog.pg_get_function_identity_arguments(oid), ',') as arguments,
188
+ pg_catalog.format_type(prorettype, null) as return_type,
189
+ proconfig as "config"
190
+ from
191
+ meta.schemas s
192
+ join pg_proc p on p.pronamespace::regnamespace::varchar = s.schema_name
193
+ order by
194
+ s.schema_name,
195
+ "function_name"
196
+ ;
197
+
@@ -0,0 +1,5 @@
1
+ ---
2
+ - match: app_portal.event_users.case_role_id
3
+ that: event_user
4
+ - match: app_portal.event_users.user_id
5
+ that: event_role
data/tests/schemas.sql ADDED
@@ -0,0 +1,25 @@
1
+ \set ON_ERROR_STOP on
2
+
3
+ drop schema if exists app_portal cascade;
4
+ drop schema if exists acl_portal cascade;
5
+ drop schema if exists sys_portal cascade;
6
+
7
+ drop schema if exists meta cascade;
8
+ drop schema if exists acl cascade;
9
+
10
+ drop schema if exists agg cascade;
11
+ drop schema if exists name cascade;
12
+ drop schema if exists agg_name cascade;
13
+
14
+ drop schema if exists auth cascade;
15
+
16
+ create schema auth;
17
+ create schema app_portal;
18
+ create schema acl_portal;
19
+ create schema sys_portal;
20
+ create schema meta;
21
+ create schema acl;
22
+ create schema agg;
23
+ create schema name;
24
+ create schema agg_name;
25
+
data/tests/setup.sql ADDED
@@ -0,0 +1,8 @@
1
+
2
+ grant all on schema app_portal to alt;
3
+ grant all on all tables in schema app_portal to alt;
4
+ alter table cases enable row level security;
5
+ alter table events enable row level security;
6
+ alter table visits enable row level security;
7
+ alter table noncompliances enable row level security;
8
+