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
data/tests/acl.sql ADDED
@@ -0,0 +1,132 @@
1
+ \set ON_ERROR_STOP on
2
+
3
+ drop schema if exists acl cascade;
4
+ create schema acl;
5
+ set search_path to acl;
6
+
7
+ create view tables as
8
+ select distinct
9
+ schema_name,
10
+ table_name
11
+ from
12
+ meta.columns
13
+ where
14
+ column_name like 'acl\_%'
15
+ ;
16
+
17
+ \echo ACL_TABLES
18
+ select * from tables where schema_name = 'app_portal';
19
+
20
+ create view schemas as
21
+ select distinct
22
+ schema_name
23
+ from
24
+ tables
25
+ ;
26
+
27
+ -- Subset of meta.columns where both referencing and referenced tables are ACL tables
28
+ drop view if exists links cascade;
29
+ create view links as
30
+ select c.*
31
+ from tables tf
32
+ join meta.links c
33
+ on c.schema_name = tf.schema_name
34
+ and c.table_name = tf.table_name
35
+ join tables tt
36
+ on tt.schema_name = c.ref_schema_name
37
+ and tt.table_name = c.ref_table_name
38
+ ;
39
+
40
+ \echo ACL_LINKS
41
+ select * from links where schema_name = 'app_portal';
42
+
43
+ -- Subset of meta.chains where first and last table are ACL tables
44
+ drop view if exists chains cascade;
45
+ create view chains as
46
+ select mc.*
47
+ from meta.chains mc
48
+ join tables src on
49
+ mc.src_schema_name = src.schema_name
50
+ and mc.src_table_name = src.table_name
51
+ join tables dst on
52
+ mc.dst_schema_name = dst.schema_name
53
+ and mc.dst_table_name = dst.table_name
54
+ ;
55
+
56
+ \echo ACL_CHAINS
57
+ --select * from chains where src_schema_name = 'app_portal';
58
+
59
+ drop view if exists closures cascade;
60
+ create view closures as
61
+ select
62
+ at.schema_name,
63
+ at.table_name,
64
+ coalesce(
65
+ array_agg(ac.src_schema_name || '.' || ac.src_table_name) filter (where ac.src_schema_name is not null),
66
+ array[]::varchar[]
67
+ ) as closure_uids
68
+ from
69
+ tables at
70
+ left join chains ac
71
+ on ac.dst_schema_name = at.schema_name
72
+ and ac.dst_table_name = at.table_name
73
+ group by
74
+ at.schema_name,
75
+ at.table_name
76
+ ;
77
+
78
+ \echo ACL_CLOSURES
79
+ --select * from closures;
80
+
81
+ drop view if exists paths cascade;
82
+ create view paths as
83
+ with
84
+ recursive search_path as (
85
+ with edges as (
86
+ select distinct
87
+ schema_name || '.' || table_name as "from_table",
88
+ ref_schema_name || '.' || ref_table_name as "to_table",
89
+ column_name as "from_column",
90
+ ref_column_name as "to_column"
91
+ from
92
+ acl.links
93
+ )
94
+
95
+ -- Anchor member: start from the initial node
96
+ select
97
+ from_table as "start_table",
98
+ from_table,
99
+ to_table,
100
+ array[from_table] as path,
101
+ array[array[from_table || '.' || from_column, to_table || '.' || to_column]] as link,
102
+ false as cycle
103
+ from edges
104
+
105
+ union all
106
+
107
+ -- Recursive member: find the next links
108
+ select
109
+ sp.start_table,
110
+ e.from_table,
111
+ e.to_table,
112
+ sp.path || e.from_table as "path",
113
+ sp.link || array[array[e.from_table || '.' || e.from_column, e.to_table || '.' || e.to_column]] as "link",
114
+ e.from_table = any(sp.path) as "cycle"
115
+ from search_path sp
116
+ join edges e on e.from_table = sp.to_table
117
+ where not sp.cycle
118
+ )
119
+ select
120
+ start_table,
121
+ to_table as "stop_table",
122
+ path || to_table as "path",
123
+ link as "links"
124
+ from search_path
125
+ order by
126
+ start_table,
127
+ "stop_table"
128
+ ;
129
+
130
+ \echo ACL_PATHS
131
+ --select * from paths;
132
+
@@ -0,0 +1,94 @@
1
+ \set ON_ERROR_STOP on
2
+
3
+ set search_path to acl_portal, public;
4
+
5
+ drop function if exists delete_user_acl(_user_id integer) cascade;
6
+ drop function if exists update_user_acl(_user_id integer) cascade;
7
+ drop function if exists create_user_acl(_user_id integer) cascade;
8
+ drop function if exists select_user_acl(_user_id integer) cascade;
9
+ drop function if exists create_user_acls() cascade;
10
+
11
+ -- Compute new ACLs for the given user and insert them into user_acls. It is an
12
+ -- error if the user already has a record there
13
+ create function create_user_acl(_user_id integer) returns integer[] as $$
14
+ with
15
+ agg_domain_users as (
16
+ select
17
+ user_id as "role_id",
18
+ array_agg(domain_role_id) as "role_ids"
19
+ from acl_portal.domain_users
20
+ group by "role_id"
21
+ )
22
+ insert into user_acls (user_id, role_ids)
23
+ select
24
+ _user_id,
25
+ esr.system_role_ids || adu.role_ids as "role_ids"
26
+ from
27
+ agg.effective_system_roles esr
28
+ left join agg_domain_users adu on adu.role_id = esr.role_id
29
+ where esr.role_id = _user_id
30
+ returning role_ids;
31
+ $$ language sql
32
+ set search_path from current;
33
+
34
+ -- Remove a user's ACL record in user_acls. It is not an error if the record
35
+ -- doesn't exist. The function can be used to immediately block a user from
36
+ -- accessing the system without going through all the user's assignments
37
+ create function delete_user_acl(_user_id integer) returns boolean as $$
38
+ delete from user_acls where user_id = _user_id returning true;
39
+ $$ language sql
40
+ set search_path from current;
41
+
42
+ -- Update a user's ACL record. It is not an error if the record doesn't exist
43
+ create function update_user_acl(_user_id integer) returns boolean as $$
44
+ begin
45
+ perform delete_user_acl(_user_id);
46
+ perform create_user_acl(_user_id);
47
+ return true;
48
+ end;
49
+ $$ language plpgsql
50
+ set search_path from current;
51
+
52
+ -- Find a user's ACL list. If the record doesn't exist, it is created before
53
+ -- the ACL list is returned to the caller. This function is also called from
54
+ -- the various policy rules and matched against the current record's access ACL
55
+ -- list so it has to be fast
56
+ --
57
+ -- It is marked 'stable' which is a blatant lie because it _does_ change the
58
+ -- database but this is ok because it will only change on for access
59
+ --
60
+ create function select_user_acl(_user_id integer) returns integer[] as $$
61
+ declare
62
+ _role_ids integer[];
63
+ begin
64
+ select role_ids
65
+ into _role_ids
66
+ from user_acls
67
+ where user_id = _user_id;
68
+
69
+ if not found then
70
+ select create_user_acl(_user_id)
71
+ into _role_ids
72
+ ;
73
+ end if;
74
+
75
+ return _role_ids;
76
+ end;
77
+ $$ language plpgsql
78
+ stable
79
+ leakproof
80
+ security definer
81
+ set search_path from current;
82
+
83
+ -- Create or update all user's ACL lists. It is used after import of Sagsys
84
+ -- data
85
+ create function update_user_acls() returns boolean as $$
86
+ begin
87
+ delete from user_acls;
88
+ perform create_user_acl(id) from auth.users;
89
+ return true;
90
+ end;
91
+ $$ language plpgsql
92
+ set search_path from current;
93
+
94
+
@@ -0,0 +1,23 @@
1
+ \set ON_ERROR_STOP on
2
+
3
+ drop table if exists acl_portal.attach_acls cascade;
4
+ create table acl_portal.attach_acls (
5
+ id integer generated by default as identity primary key,
6
+ parent_table varchar not null,
7
+ parent_id integer not null,
8
+ child_table varchar not null,
9
+ child_field varchar not null,
10
+ acls integer[] not null,
11
+
12
+ unique (child_table, child_field, parent_table, parent_id, acls)
13
+ );
14
+
15
+ -- Acts as a materialized view. A user's record is updated whenever case_users
16
+ -- or event_users are changed
17
+ drop table if exists acl_portal.user_acls cascade;
18
+ create table acl_portal.user_acls (
19
+ id integer generated by default as identity primary key,
20
+ user_id integer not null references auth.users(id) unique,
21
+ role_ids integer[] not null
22
+ );
23
+
@@ -0,0 +1,73 @@
1
+ \set ON_ERROR_STOP on
2
+ /*
3
+ -- View of role kind id, domain id, role. Because case id and event id belongs
4
+ -- to the same namespace you can simply do
5
+ --
6
+ -- select * from domain_roles where id = ID' to get the roles of a domain object
7
+ --
8
+ drop view if exists acl_portal.domain_roles cascade;
9
+ create view acl_portal.domain_roles as
10
+ with
11
+ case_objects as (
12
+ select id, case_id as domain_id, role
13
+ from app_portal.case_roles cr
14
+ where role in ('CLA', 'CTA', 'KON', 'AKK')
15
+ ),
16
+ event_objects as (
17
+ select er.id, e.id as domain_id, er.role
18
+ from app_portal.event_roles er
19
+ join app_portal.events e on e.id = er.event_id
20
+ where er.role in ('ELA', 'ETA')
21
+ )
22
+ select * from case_objects
23
+ union
24
+ select * from event_objects
25
+ union
26
+ select co.id, eo.domain_id, co.role
27
+ from event_objects eo
28
+ join app_portal.events e on e.id = eo.domain_id
29
+ join case_objects co on co.domain_id = e.case_id
30
+ order by
31
+ domain_id,
32
+ role
33
+ ;
34
+
35
+ \echo DOMAIN_ROLES
36
+ select * from acl_portal.domain_roles;
37
+
38
+ -- TODO A LOT (automatically detect CLA, CTA)
39
+ drop view if exists acl_portal.domain_users cascade;
40
+ create view acl_portal.domain_users as
41
+ with
42
+ case_role_users as (
43
+ select
44
+ cr.id as "domain_role_id",
45
+ cu.user_id,
46
+ cr.role
47
+ from app_portal.case_users cu
48
+ join app_portal.case_roles cr on cr.id = cu.case_role_id
49
+ join app_portal.cases c on c.id = cr.case_id
50
+ where (not c.closed or cr.role in ('CLA', 'ELA'))
51
+ and cr.role in ('CLA', 'CTA', 'KON', 'AKK')
52
+ ),
53
+ event_role_users as (
54
+ select
55
+ er.id as "domain_role_id",
56
+ eu.user_id,
57
+ er.role
58
+ from app_portal.event_users eu
59
+ join app_portal.event_roles er on er.id = eu.event_role_id
60
+ join app_portal.events e on e.id = er.event_id
61
+ where not e.closed or er.role in ('CLA', 'ELA')
62
+ )
63
+ select * from case_role_users
64
+ union
65
+ select * from event_role_users
66
+ order by
67
+ "domain_role_id",
68
+ "user_id"
69
+ ;
70
+
71
+ \echo DOMAIN_USERS
72
+ select * from acl_portal.domain_users;
73
+ */
data/tests/agg.sql ADDED
@@ -0,0 +1,25 @@
1
+ \set ON_ERROR_STOP on
2
+
3
+ drop schema if exists agg cascade;
4
+ create schema agg;
5
+
6
+ drop table if exists agg.effective_system_roles;
7
+ create table agg.effective_system_roles (
8
+ role_id integer not null primary key,
9
+ system_role_ids integer[] not null
10
+ );
11
+
12
+ insert into agg.effective_system_roles(role_id, system_role_ids) values
13
+ (1, array[1]),
14
+ (2, array[2, 1]),
15
+ (3, array[3, 1]),
16
+ (4, array[4, 1]),
17
+ (5, array[5, 1000]),
18
+ (6, array[6, 2000]),
19
+ (7, array[7, 2001]),
20
+ (8, array[8, 2000]),
21
+ (9, array[9, 2001]),
22
+ (10, array[10, 2000]),
23
+ (11, array[11, 2001])
24
+ ;
25
+
data/tests/app.sql ADDED
@@ -0,0 +1,48 @@
1
+ \set ON_ERROR_STOP on
2
+
3
+ \i schemas.sql
4
+ \i meta.sql
5
+ \i acl.sql
6
+ \i auth.sql
7
+ \i agg.sql
8
+
9
+ \i initial-functions.sql
10
+
11
+ \i app_portal-tables.sql
12
+ \i app_portal-views.sql
13
+ \i acl_portal-tables.sql
14
+ \i acl_portal-views.sql
15
+ \i acl_portal-functions.sql
16
+ \i app_portal-triggers.sql
17
+ \i sys_portal.sql
18
+
19
+ \i final-functions.sql
20
+
21
+ set search_path to acl_portal, app_portal, sys_portal;
22
+
23
+ /*
24
+ \echo INCLUDING ACL-FUNCTIONS.SQL
25
+ \i acl-functions.sql
26
+
27
+ \echo INCLUDING ACL-META.SQL
28
+ \i acl-meta.sql
29
+
30
+ \echo INCLUDING ACL-ACL.sql
31
+ \i acl-acl.sql
32
+
33
+ \echo INCLUDING ACL-ACL_PORTAL.SQL
34
+ \i acl-acl_portal.sql
35
+
36
+ \echo INCLUDING ACL-SYS_PORTAL.SQL
37
+ \i acl-sys_portal.sql
38
+
39
+ \echo INCLUDING ACL-DOMAINS.SQL
40
+ \i acl-domains.sql
41
+
42
+ \echo INCLUDING ACL-FINAL_FUNCTIONS
43
+ \i acl-final_functions.sql
44
+
45
+ \echo ACLS
46
+ select * from agg_acl_users;
47
+ */
48
+
@@ -0,0 +1,138 @@
1
+ \set ON_ERROR_STOP on
2
+
3
+ set search_path to app_portal;
4
+
5
+ -- Abstract base table for cases and events
6
+ create table abstract_domain_objects (
7
+ id integer generated by default as identity primary key
8
+ );
9
+
10
+ create table cases (
11
+ id integer not null references abstract_domain_objects(id) primary key,
12
+ name varchar,
13
+ ident varchar generated always as (name) stored,
14
+ closed boolean default false,
15
+ acl_select integer[],
16
+ acl_update integer[],
17
+ acl_delete integer[]
18
+ );
19
+
20
+ create table role_kinds (
21
+ id integer generated by default as identity primary key,
22
+ kind varchar not null unique,
23
+ acl boolean not null default false,
24
+ ordinal integer
25
+ );
26
+
27
+ -- Abstract base table for case_roles and event_roles
28
+ create table abstract_roles (
29
+ id integer generated by default as identity primary key
30
+ );
31
+
32
+ create table case_role_templates (
33
+ id integer primary key references abstract_roles(id),
34
+ "role" varchar not null references role_kinds(kind)
35
+ );
36
+
37
+ create table case_roles (
38
+ id integer primary key references abstract_roles(id),
39
+ case_id integer not null references cases(id),
40
+ "role" varchar not null references role_kinds(kind),
41
+ acl_select integer[],
42
+ acl_update integer[],
43
+ acl_delete integer[],
44
+
45
+ unique (case_id, "role")
46
+ );
47
+
48
+ create table case_users (
49
+ id integer generated by default as identity primary key,
50
+ case_role_id integer not null references case_roles(id),
51
+ user_id integer not null references auth.roles(id),
52
+ acl_select integer[],
53
+ acl_update integer[],
54
+ acl_delete integer[]
55
+ );
56
+
57
+ create table events (
58
+ id integer not null references abstract_domain_objects(id) primary key,
59
+ case_id integer not null references cases(id),
60
+ name varchar,
61
+ label varchar generated always as (name) stored,
62
+ closed boolean default false,
63
+ deleted boolean default false,
64
+ acl_select integer[],
65
+ acl_update integer[],
66
+ acl_delete integer[]
67
+ );
68
+
69
+ create table event_role_templates (
70
+ id integer primary key references abstract_roles(id),
71
+ "role" varchar not null references role_kinds(kind)
72
+ );
73
+
74
+ create table event_roles (
75
+ id integer primary key references abstract_roles(id),
76
+ event_id integer not null references events(id),
77
+ "role" varchar not null references role_kinds(kind),
78
+ acl_select integer[],
79
+ acl_update integer[],
80
+ acl_delete integer[],
81
+
82
+ unique(event_id, "role")
83
+ );
84
+
85
+ create table event_users (
86
+ id integer generated by default as identity primary key,
87
+ event_role_id integer not null references event_roles(id),
88
+ user_id integer not null references auth.roles(id),
89
+ acl_select integer[],
90
+ acl_update integer[],
91
+ acl_delete integer[]
92
+ );
93
+
94
+ create table bookings (
95
+ id integer generated by default as identity primary key,
96
+ event_id integer not null references events(id),
97
+ acl_select integer[],
98
+ acl_update integer[],
99
+ acl_delete integer[]
100
+ );
101
+
102
+ create table user_bookings (
103
+ id integer generated by default as identity primary key,
104
+ user_id integer not null references auth.roles(id),
105
+ booking_id integer not null references bookings(id),
106
+ acl_select integer[],
107
+ acl_update integer[],
108
+ acl_delete integer[]
109
+ );
110
+
111
+ create table visits (
112
+ id integer generated by default as identity primary key,
113
+ event_id integer not null references events(id),
114
+ name varchar,
115
+ acl_select integer[],
116
+ acl_update integer[],
117
+ acl_delete integer[]
118
+ );
119
+
120
+ create table noncompliances (
121
+ id integer generated by default as identity primary key,
122
+ visit_id integer not null references visits(id),
123
+ name varchar,
124
+ acl_select integer[],
125
+ acl_update integer[],
126
+ acl_delete integer[]
127
+ );
128
+
129
+ create table noncompliance_uploads (
130
+ id integer generated by default as identity primary key,
131
+ noncompliance_id integer not null references noncompliances(id),
132
+ name varchar,
133
+ uploaded_by_id integer not null references auth.roles(id),
134
+ acl_select integer[],
135
+ acl_update integer[],
136
+ acl_delete integer[]
137
+ );
138
+
@@ -0,0 +1,23 @@
1
+
2
+ --
3
+ -- Triggers that maintains acl_portal.user_acls.
4
+ --
5
+
6
+ -- Call acl_portal.update_user_acl on any change to either case_users or
7
+ -- event_users. Note that this function is both cross-table and cross
8
+ -- insert/delete
9
+ create function domain_users_aiud() returns trigger as $$
10
+ begin
11
+ perform acl_portal.update_user_acl(coalesce(NEW.user_id, OLD.user_id));
12
+ return null; -- works because this is an after-trigger
13
+ end;
14
+ $$ language plpgsql;
15
+
16
+ create trigger case_users_aiud_trg after insert or update or delete on app_portal.case_users
17
+ for each row execute function domain_users_aiud()
18
+ ;
19
+
20
+ create trigger event_users_aiud_trg after insert or update or delete on app_portal.event_users
21
+ for each row execute function domain_users_aiud()
22
+ ;
23
+