mikras_utils 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+