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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5db4dc4f784bd24e9a739b4de231f2a52c6927e1b5cc4da48256bdfe318b27f3
4
+ data.tar.gz: 15eb9d1dc3052833d5b8481c0043d3cb32610960ae8b781c63f5ea9f6a827d5a
5
+ SHA512:
6
+ metadata.gz: db57c96600c9fcd448dfd72a62e3008f59da9d91b14d1aed60c56032b68bec8e78643da77f1c2f42779466ab0a7cc4c69f5aa176b1f2fe8881eadaf990336fdb
7
+ data.tar.gz: 3c9acdf983bdadeb7891bfa5211107a7a77e7c66001b1c78bdb470712ee7f727dbb98b8061a1cbf9af0516a249aa2767ad72b6f415492bd5bd7e7b8c4f881322
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.1.2
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in mikras_utils.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # MikrasUtils
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/mikras_utils`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Development
24
+
25
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
+
27
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+
29
+ ## Contributing
30
+
31
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/mikras_utils.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/acl-build ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/bash
2
+
3
+ . bash.include
4
+
5
+ cd tests && ./acl-build
data/build ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/bash
2
+
3
+ . bash.include
4
+
5
+ cd tests
6
+ psql -f app.sql
7
+ cd -
8
+ bundle exec exe/mkacl tests/acl.spec | psql
9
+ cd tests
10
+ fox --delete=touched -r reflections.yml clr acl.fox | psql clr
11
+ psql -f fox.sql
data/exe/mikras_utils ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mikras_utils.rb'
4
+
5
+ puts MikrasUtils::VERSION
data/exe/mkacl ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ SPEC = %(
4
+ @ Make RLS policies
5
+
6
+ -- [DATABASE [USERNAME]] SPEC-FILE
7
+
8
+ Read SPEC and generate following functions, policies, and triggers
9
+
10
+ SPEC FILE
11
+ The format is a YAML file where each root key represents a table. A table
12
+ consists of insert, select, update, and delete actions that are arrays of
13
+ ACL entries. An ACL entry list of associated roles and possibly a check SQL
14
+ condition and a fields list
15
+
16
+ Field is a list of field names that can be updated. If the first field is
17
+ prefixed with a '-', the list is exclusive
18
+
19
+ The special 'schemas' root key contains the fields 'app' and 'acl'. The
20
+ 'app' value is the name of the schema used in the spec file. The 'acl'
21
+ field is the schema where triggers are created (this avoid namespace
22
+ pollution)
23
+
24
+ ROLES
25
+ The following roles are recognized: LA, TA, KON, AKK. ADM is excluded
26
+ because it is now a RBAC fole. PUP and NON is excluded because they are not
27
+ implemented in Mikras
28
+
29
+ OPTIONS
30
+ -i,interactive
31
+ Generate code for interactive use by added ON_ERROR_STOP and other variables
32
+
33
+ -g,generate=LIST
34
+ Generate only the given modules. LIST is a comma-separated list of
35
+ modules. The following modules are currently aavailable: id_functions,
36
+ insert_triggers, rules, acl_functions, and role_functions
37
+ )
38
+
39
+ require 'yaml'
40
+ require 'shellopts'
41
+
42
+ require_relative '../lib/mikras_utils/mkacl.rb'
43
+
44
+ opts, args = ShellOpts::process(SPEC, ARGV)
45
+ file = args.extract(-1)
46
+ database, username = args.extract(0..2)
47
+ username ||= database || ENV['PRICK_USERNAME']
48
+ database ||= ENV['PRICK_DATABASE']
49
+ conn = PgConn.new database, username
50
+
51
+ spec = MkAcl::Parser.parse(file)
52
+ MkAcl::Analyzer.analyze(spec, conn)
53
+ #spec.dump
54
+ #puts
55
+ #exit
56
+
57
+ modules = opts.generate? ? opts.generate.split(',').map(&:to_sym) : MkAcl::Generator::MODULES
58
+ MkAcl::Generator.generate(spec, conn, modules, interactive: opts.interactive?)
59
+
@@ -0,0 +1,46 @@
1
+
2
+ module MkAcl
3
+ class Analyzer
4
+ attr_reader :spec
5
+ attr_reader :conn
6
+
7
+ def initialize(spec, conn)
8
+ @spec = spec
9
+ @conn = conn
10
+ end
11
+
12
+ def analyze
13
+ links = conn.tuples %(
14
+ select
15
+ table_name,
16
+ ref_table_name,
17
+ schema_name || '.' || table_name,
18
+ ref_schema_name || '.' || ref_table_name
19
+ from meta.links
20
+ )
21
+
22
+ # Link up tables
23
+ for child_table_name, parent_table_name, child_table_uid, parent_table_uid in links
24
+ spec[child_table_name].parents << spec[parent_table_name] if spec[parent_table_name]
25
+ end
26
+
27
+ # Assign domains
28
+ spec.tables.each { |table| resolve_domain(table) }
29
+
30
+ # Check that no table has more than one parent. FIXME
31
+ spec.tables.each { |table|
32
+ table.parents.size <= 1 or raise ArgumentError, "Table '#{table.name}' has multiple parents"
33
+ }
34
+
35
+ spec
36
+ end
37
+
38
+ def self.analyze(spec, conn) self.new(spec, conn).analyze end
39
+
40
+ private
41
+ def resolve_domain(table)
42
+ table.domain ||= resolve_domain(table.parents.first)
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,55 @@
1
+
2
+ require_relative './generators/id_functions.rb'
3
+ require_relative './generators/acl_functions.rb'
4
+ require_relative './generators/insert_triggers.rb'
5
+ require_relative './generators/role_functions.rb'
6
+ require_relative './generators/rules.rb'
7
+
8
+ module MkAcl
9
+ class Generator
10
+ MODULES = [:id_functions, :insert_triggers, :rules, :acl_functions, :role_functions]
11
+
12
+ using String::Text
13
+
14
+ attr_reader :spec
15
+ attr_reader :conn
16
+ forward_to :spec, :app_schema, :acl_schema
17
+
18
+ def initialize(spec, conn)
19
+ @spec = spec
20
+ @conn = conn
21
+ end
22
+
23
+ def generate(modules = MODULES, interactive: true)
24
+ if interactive
25
+ puts "\\set ON_ERROR_STOP on"
26
+ puts
27
+ else
28
+ puts "-- Auto-generated by mkacl(1) - please don't touch"
29
+ puts "--"
30
+ puts
31
+ end
32
+
33
+ matches = modules.map { |k| [k, true] }.to_h
34
+
35
+ IdFunctions.generate(self) if matches.key?(:id_functions)
36
+ for table in spec.tables
37
+ InsertTriggers.generate(self, table) if matches.key?(:insert_triggers)
38
+ end
39
+ Rules.generate(self) if matches.key?(:rules)
40
+ AclFunctions.generate(self) if matches.key?(:acl_functions)
41
+ RoleFunctions.generate(self) if matches.key?(:role_functions)
42
+ end
43
+
44
+ def self.generate(spec, conn, modules = MODULES, **opts)
45
+ self.new(spec, conn).generate(modules, **opts)
46
+ end
47
+
48
+ def inspect() "Generator(#{@database.inspect}, #{@username.inspect}, #{@spec})" end
49
+
50
+ private
51
+ def generate_select_rls_rule(table)
52
+ end
53
+ end
54
+ end
55
+
@@ -0,0 +1,208 @@
1
+
2
+ module MkAcl
3
+ class Generator
4
+ class AclFunctions
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
+ for table in spec.tables
16
+ generate_acl_table_update_function(table)
17
+ end
18
+ generate_acl_update_function
19
+ end
20
+
21
+ def self.generate(generator) self.new(generator).generate end
22
+
23
+ private
24
+ def generate_acl_update_function
25
+ signature = "#{acl_schema}.update_acls()"
26
+
27
+ stmts = spec.tables.map { |table|
28
+ "perform #{acl_schema}.#{table}_update_acls(id) from #{app_schema}.#{table};"
29
+ }
30
+
31
+ puts %(
32
+ -- Set the ACL fields and add attach ACLs for all ACL tables
33
+ --
34
+ drop function if exists #{signature} cascade;
35
+ create function #{signature} returns boolean as $$
36
+ begin
37
+ ).align
38
+ indent(2) { puts stmts }
39
+ puts %(
40
+ return true;
41
+ end;
42
+ $$ language plpgsql;
43
+ ).align
44
+ puts
45
+ end
46
+
47
+ def generate_acl_table_update_function(table)
48
+ function_name = "#{table}_update_acls"
49
+ function_uid = "#{acl_schema}.#{function_name}"
50
+ signature = "#{function_uid}(_id integer)"
51
+
52
+ puts %(
53
+ -- Set the ACL fields of the given record and create an attach_acls
54
+ -- record for insert ACLs. Existing ACLs are ignored/deleted
55
+ --
56
+ -- Note: The different table-level variations of this function can be
57
+ -- collapsed into a single function but it requires dynamic execution of
58
+ -- a delete statement with array-of-array arguments :-O
59
+ --
60
+ -- Note: This function is auto-generated by #{$PROGRAM_NAME} using #{spec.file}
61
+ --
62
+ drop function if exists #{signature} cascade;
63
+ create function #{signature} returns integer as $$
64
+ ).align
65
+ indent {
66
+ puts %(
67
+ declare
68
+ _domain_id integer;
69
+ _acls integer[]; -- per-rule ACLs
70
+ _acl_select integer[][]; -- per-action ACLs
71
+ _acl_update integer[][];
72
+ _acl_delete integer[][];
73
+ begin
74
+ ).align
75
+ indent {
76
+ puts "-- Find security domain id"
77
+ puts "_domain_id := #{acl_schema}.#{table.domain}_id_of_#{table.record_name}(_id);"
78
+ puts
79
+ generate_acl_function_set_acl_fields(table)
80
+ generate_acl_function_create_attach_acls(table)
81
+ puts "return _id;"
82
+ }
83
+ puts "end;"
84
+ }
85
+ puts "$$ language plpgsql;"
86
+ puts
87
+ end
88
+
89
+ def generate_acl_function_set_acl_fields(table)
90
+ acls = {} # Map from action name to array of arrays of role IDs
91
+
92
+ for action in %w(select update delete).map { |key| table.actions[key] }
93
+ puts "-- #{action} ACLs"
94
+ acl_field = "acl_#{action}"
95
+ acl_var = "_acl_#{action}"
96
+ puts "#{acl_var} := array[]::integer[][];"
97
+ puts
98
+
99
+ for rule in action.rules
100
+ auth_roles = rule.roles.select { _1 == _1.downcase }
101
+ case_roles = rule.roles.select { _1 == _1.upcase }
102
+
103
+ if !auth_roles.empty?
104
+ role_seq = conn.quote_value_seq(auth_roles)
105
+ puts %(
106
+ -- Find "#{action}" system role ACLs
107
+ _acls := array[]::integer[];
108
+ select _acls || array_agg(id)
109
+ into _acls
110
+ from auth.roles
111
+ where rolename = any(array[#{role_seq}]);
112
+ ).align
113
+ puts
114
+ end
115
+
116
+ if !case_roles.empty?
117
+ role_seq = conn.quote_value_seq(case_roles)
118
+ puts %(
119
+ -- Find "#{action}" case role ACLs
120
+ select _acls || array_agg(id)
121
+ into _acls
122
+ from acl_portal.domain_roles
123
+ where domain_id = _domain_id
124
+ and role = any(array[#{role_seq}]);
125
+ ).align
126
+ puts
127
+ end
128
+
129
+ puts "#{acl_var} := #{acl_var} || _acls;"
130
+ puts
131
+ end
132
+ end
133
+ puts %(
134
+ -- Update ACL fields
135
+ update #{table.uid}
136
+ set acl_select = _acl_select,
137
+ acl_update = _acl_update,
138
+ acl_delete = _acl_delete
139
+ where id = _id;
140
+ ).align
141
+ puts
142
+ end
143
+
144
+ def generate_acl_function_create_attach_acls(table)
145
+ record = table.record_name
146
+
147
+ domain = table.domain
148
+ domain_table = "#{domain}s"
149
+ domain_roles_table ="#{domain}_roles"
150
+
151
+ # Insert a attach record per parent table. This snippet handles
152
+ # multiple parents of a record but the check algorithm probably does
153
+ # not TODO
154
+ auth_role_list = conn.quote_value_seq table.insert.rules.map(&:auth_roles).flatten
155
+ case_role_list = conn.quote_value_seq table.insert.rules.map(&:case_roles).flatten
156
+ for parent in table.parents
157
+ id_fields = conn.values %(select column_name from acl.links where table_name = '#{table}')
158
+ for id_field in id_fields
159
+ puts %(
160
+ -- Delete attach ACLs
161
+ delete
162
+ from acl_portal.attach_acls
163
+ where parent_table = '#{parent.name}'
164
+ and parent_id = _id;
165
+
166
+ -- Create attach ACLs
167
+ insert into acl_portal.attach_acls (parent_table, parent_id, child_table, child_field, acls)
168
+ with
169
+ case_role_acls as (
170
+ select
171
+ id
172
+ from
173
+ acl_portal.domain_roles
174
+ where domain_id = _id
175
+ and role = any(array[#{case_role_list}]::text[])
176
+ ),
177
+ auth_role_acls as (
178
+ select
179
+ id
180
+ from
181
+ auth.roles
182
+ where rolename = any(array[#{auth_role_list}]::text[])
183
+ ),
184
+ role_acls as (
185
+ select * from case_role_acls
186
+ union
187
+ select * from auth_role_acls
188
+ )
189
+ select
190
+ '#{parent.name}' as "parent_table",
191
+ _id as "parent_id",
192
+ l.table_name as "child_table",
193
+ l.column_name as "child_field",
194
+ array_agg(ra.id) as "acls"
195
+ from
196
+ acl.links l,
197
+ role_acls ra
198
+ where l.table_name = '#{table}'
199
+ group by
200
+ "parent_table", "parent_id",
201
+ "child_table", "child_field";
202
+ ).align
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end