mikras_utils 0.3.3 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf10f02dfcf24bfe2f518edfda534abbb78047b6876610f6a09adfac5217f73c
4
- data.tar.gz: 7801d2245d2cd810b4defcf94b2bace9f8a6587e3fbebccc49240a689b47763e
3
+ metadata.gz: 74256281ee10bf5186723bfa260cd62dda3057216660aa945c7b591fa1581177
4
+ data.tar.gz: 882636fab035922c146cc33c33a5af9deaf0da818bec6897e17a84ad8f1f2289
5
5
  SHA512:
6
- metadata.gz: 565ee40963dc77d9759bd78a55dacddab5296f23c022a07ccce42fd8c890db65eddcb6ce015c909943a34483009233f53d2c75dd0fe05faa1ccca2c1831b5375
7
- data.tar.gz: 8d8afe7d2bb415edd5c3a275bca441a0d4bf73299b97e5f2d8578c9efa0b37b62840679ba73eeb60b525dbf0254e1a8a3a893ba11c4ba0add75f89706219bf50
6
+ metadata.gz: 5ca4be02024c23f2109cc7f3919331c180f6403cb04906d5652446d80241e7b502ed9cfcebfcc5ddf66b098c4b6def8e3f1cd3863942f9e988cb3e5bd1abc501
7
+ data.tar.gz: 6d82b691b1b68214910dac0acc847a68b928b6e0f45c8a157e272ded5fdc84b2f3fe5e3f23ddbadbee4bd82f1621164eea1d736deb7923787fb9f999926ddf0e
data/exe/mkacl CHANGED
@@ -3,9 +3,12 @@
3
3
  SPEC = %(
4
4
  @ Make RLS functions and policies
5
5
 
6
- -- [DATABASE [USERNAME]] SPEC-FILE
6
+ -- [DATABASE] SPEC-FILE
7
7
 
8
- Read SPEC and generate following functions, policies, and triggers
8
+ Read SPEC and generate following functions, policies, and triggers. The
9
+ default DATABASE and USERNAME variables are read from the environment and
10
+ then .prick.state.yml if present; otherwise the current user's database is
11
+ used
9
12
 
10
13
  FUNCTIONS
11
14
  todo
@@ -36,12 +39,15 @@ SPEC = %(
36
39
  implemented in Mikras
37
40
 
38
41
  OPTIONS
42
+ -d,dump
43
+ Dump internal format. Used for debugging
44
+
39
45
  -i,interactive
40
46
  Generate code for interactive use by added ON_ERROR_STOP and other variables
41
47
 
42
48
  -g,generate=LIST
43
49
  Generate only the given modules. LIST is a comma-separated list of
44
- modules. The following modules are currently available: id_functions,
50
+ modules. The following modules are currently available: seeds, id_functions,
45
51
  insert_triggers, rules, acl_functions, and role_functions
46
52
 
47
53
  -W,no-warn
@@ -52,14 +58,17 @@ SPEC = %(
52
58
  require 'yaml'
53
59
  require 'shellopts'
54
60
 
61
+ require_relative '../lib/mikras_utils/mikras.rb'
55
62
  require_relative '../lib/mikras_utils/mkacl.rb'
56
63
 
64
+ PRICK_STATE_FILE = "prick.state.yml"
65
+
57
66
  opts, args = ShellOpts::process(SPEC, ARGV)
58
67
  file = args.extract(-1)
59
- database, username = args.extract(0..2)
60
- username ||= database || ENV['PRICK_USERNAME']
61
- database ||= ENV['PRICK_DATABASE']
62
- conn = PgConn.new database, username
68
+ database = args.extract(0..1)
69
+
70
+ conn = PgConn.new *Mikras.credentials(database)
71
+ conn.schema.exist?("prick") or ShellOpts.error "Database '#{database}' is not a prick database"
63
72
 
64
73
  if opts.generate?
65
74
  modules = opts.generate.split(',').map(&:to_sym)
@@ -70,8 +79,10 @@ end
70
79
 
71
80
  spec = MkAcl::Parser.parse(file)
72
81
  MkAcl::Analyzer.analyze(spec, conn, warn: !opts.no_warn?)
73
- #spec.dump
74
- #exit
82
+ if opts.dump?
83
+ spec.dump
84
+ exit
85
+ end
75
86
  MkAcl::Generator.generate(spec, conn, modules, interactive: opts.interactive?)
76
87
 
77
88
 
data/exe/rls ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'indented_io'
4
+ require 'shellopts'
5
+ require 'forward_to'
6
+
7
+ include ForwardTo
8
+
9
+ require_relative '../lib/mikras_utils/rls/spec.rb'
10
+ require_relative '../lib/mikras_utils/rls/parser.rb'
11
+ require_relative '../lib/mikras_utils/rls/analyzer.rb'
12
+
13
+ def error(msg) = ShellOpts.error(msg)
14
+
15
+ SPEC = %(
16
+ Parse RLS spec file
17
+
18
+ -- FILE
19
+ )
20
+
21
+ opts, args = ShellOpts.process(SPEC, ARGV)
22
+
23
+ file = args.expect(1)
24
+ spec = Parser.parse(file)
25
+ Analyzer.analyze(spec)
26
+
27
+ spec.dump
28
+
29
+
30
+
@@ -0,0 +1,24 @@
1
+
2
+ module Mikras
3
+ def self.credentials(database_argument)
4
+ if database_argument
5
+ database = database_argument
6
+ username = database
7
+ else
8
+ username ||= database || ENV['PRICK_USERNAME']
9
+ database ||= ENV['PRICK_DATABASE']
10
+
11
+ if database.nil? && File.exist?(PRICK_STATE_FILE)
12
+ prick_state = YAML.load(IO.read PRICK_STATE_FILE)
13
+ database = prick_state["database"]
14
+ username = prick_state["username"]
15
+ else
16
+ database ||= ENV["USER"]
17
+ username ||= database
18
+ end
19
+ end
20
+ [database, username]
21
+ end
22
+ end
23
+
24
+
@@ -15,19 +15,21 @@ module MkAcl
15
15
  end
16
16
 
17
17
  def analyze
18
+ # Find child-parent relations between linked tables
18
19
  links = conn.tuples %(
19
20
  select
20
- table_name,
21
- ref_table_name,
22
- schema_name || '.' || table_name,
23
- ref_schema_name || '.' || ref_table_name
21
+ table_name as "child_table_name",
22
+ schema_name || '.' || table_name as "child_table_uid",
23
+ ref_table_name as "parent_table_name",
24
+ ref_schema_name || '.' || ref_table_name as "parent_table_uid",
25
+ column_name as "parent_link_field"
24
26
  from meta.links
25
- where schema_name = '#{spec.app_schema}'
27
+ where schema_name = '#{spec.app_schema}'
26
28
  and ref_schema_name = '#{spec.app_schema}'
27
29
  )
28
30
 
29
- # Link up tables
30
- for child_table_name, parent_table_name, child_table_uid, parent_table_uid in links
31
+ # Assign table references
32
+ for child_table_name, child_table_uid, parent_table_name, parent_table_uid, parent_link_field in links
31
33
  if !spec.key?(child_table_name)
32
34
  @uncovered_tables << child_table_name
33
35
  next
@@ -35,8 +37,11 @@ module MkAcl
35
37
  @uncovered_tables << parent_table_name
36
38
  next
37
39
  end
38
-
39
- spec[child_table_name].parents << spec[parent_table_name] if spec[parent_table_name]
40
+
41
+ child_table = spec[child_table_name] or raise "Can't find table #{parent_table_name.inspect}"
42
+ parent_table = spec[parent_table_name] or raise "Can't find referenced table #{parent_table_name.inspect}"
43
+
44
+ child_table.references[parent_table.name] = [parent_table, parent_link_field]
40
45
  end
41
46
 
42
47
  # if warn && !@uncovered_tables.empty?
@@ -44,29 +49,38 @@ module MkAcl
44
49
  # indent { puts uncovered_tables }
45
50
  # end
46
51
 
47
- # Assign domains
48
- spec.tables.each { |table|
49
- resolve_domain(table)
50
- }
51
-
52
- # Check that no table has more than one parent. FIXME
52
+ # Assign parents
53
53
  spec.tables.each { |table|
54
- table.parents.size <= 1 or raise ArgumentError, "Table '#{table.name}' has multiple parents"
54
+ if table.parent_name
55
+ table.parent = spec[table.parent_name] or raise ArgumentError, "Can't find '#{table.parent_name}'"
56
+ table.parent_link_field = table.references[table.parent.name].last
57
+ else
58
+ table.references.size <= 1 or raise ArgumentError, "Table '#{table.name}' has multiple references"
59
+ parent, link_field = table.references.values.first
60
+ if parent&.acl
61
+ table.parent = parent
62
+ table.parent_link_field = link_field
63
+ end
64
+ end
55
65
  }
56
66
 
67
+ # Resolve domains
68
+ spec.tables.select(&:acl).each { |t| resolve_domain(t) }
69
+
57
70
  spec
58
71
  end
59
72
 
60
73
  def self.analyze(spec, conn, **opts) self.new(spec, conn, **opts).analyze end
61
74
 
62
75
  private
63
- def find_tables
64
-
65
- end
66
-
67
76
  def resolve_domain(table)
68
- table.domain ||= table.parents.first && resolve_domain(table.parents.first)
77
+ if table.domain.nil?
78
+ !table.parent.nil? or raise ArgumentError, "Domain table without domain name - '#{table.name}'"
79
+ resolve_domain(table.parent)
80
+ table.domain = table.parent.domain
81
+ end
69
82
  end
70
83
  end
71
84
  end
72
85
 
86
+
@@ -4,10 +4,11 @@ require_relative './generators/acl_functions.rb'
4
4
  require_relative './generators/insert_triggers.rb'
5
5
  require_relative './generators/role_functions.rb'
6
6
  require_relative './generators/rules.rb'
7
+ require_relative './generators/seeds.rb'
7
8
 
8
9
  module MkAcl
9
10
  class Generator
10
- MODULES = [:id_functions, :insert_triggers, :rules, :acl_functions, :role_functions]
11
+ MODULES = [:id_functions, :insert_triggers, :rules, :acl_functions, :role_functions, :seeds]
11
12
 
12
13
  using String::Text
13
14
 
@@ -32,6 +33,7 @@ module MkAcl
32
33
 
33
34
  matches = modules.map { |k| [k, true] }.to_h
34
35
 
36
+ Seeds.generate(self) if matches.key?(:seeds)
35
37
  IdFunctions.generate(self) if matches.key?(:id_functions)
36
38
  for table in spec.tables
37
39
  InsertTriggers.generate(self, table) if matches.key?(:insert_triggers)
@@ -42,7 +44,7 @@ module MkAcl
42
44
  end
43
45
 
44
46
  def self.generate(spec, conn, modules = MODULES, **opts)
45
- self.new(spec, conn).generate(modules, **opts)
47
+ self.new(spec, conn).generate(modules, **opts)
46
48
  end
47
49
 
48
50
  def inspect() "Generator(#{@database.inspect}, #{@username.inspect}, #{@spec})" end
@@ -21,11 +21,28 @@ module MkAcl
21
21
  def self.generate(generator) self.new(generator).generate end
22
22
 
23
23
  private
24
+
25
+ # %(
26
+ # update TABLE tbl
27
+ # set read_acls = (
28
+ # select array_agg(role_id)
29
+ # from acl_portal.eff_object_roles eor
30
+ # where tbl.DOMAIN_ID = eor.object_id
31
+ # )
32
+ # )
33
+
34
+ # %(
35
+ # select
36
+ # from
37
+ # where
38
+ # domain_id_of(DOMAIN, TABLE_NAME, id)
39
+ # )
40
+
24
41
  def generate_acl_update_function
25
42
  signature = "#{acl_schema}.update_acls()"
26
43
 
27
- stmts = spec.tables.map { |table|
28
- "perform #{acl_schema}.#{table}_update_acls(id) from #{app_schema}.#{table};"
44
+ stmts = spec.tables.map { |table|
45
+ "perform #{acl_schema}.#{table}_update_acls(id) from #{app_schema}.#{table};"
29
46
  }
30
47
 
31
48
  puts %(
@@ -56,7 +73,7 @@ module MkAcl
56
73
  -- Note: The different table-level variations of this function can be
57
74
  -- collapsed into a single function but it requires dynamic execution of
58
75
  -- a delete statement with array-of-array arguments :-O
59
- --
76
+ --
60
77
  -- Note: This function is auto-generated by #{$PROGRAM_NAME} using #{spec.file}
61
78
  --
62
79
  drop function if exists #{signature} cascade;
@@ -68,7 +85,7 @@ module MkAcl
68
85
  _domain_id integer;
69
86
  _acls integer[]; -- per-rule ACLs
70
87
  _acl_select integer[][]; -- per-action ACLs
71
- _acl_update integer[][]; --
88
+ _acl_update integer[][]; --
72
89
  _acl_delete integer[][]; --
73
90
  begin
74
91
  ).align
@@ -132,7 +149,7 @@ module MkAcl
132
149
  end
133
150
  puts %(
134
151
  -- Update ACL fields
135
- update #{table.uid}
152
+ update #{table.uid}
136
153
  set acl_select = _acl_select,
137
154
  acl_update = _acl_update,
138
155
  acl_delete = _acl_delete
@@ -165,7 +182,7 @@ module MkAcl
165
182
 
166
183
  -- Create attach ACLs
167
184
  insert into acl_portal.attach_acls (parent_table, parent_id, child_table, child_field, acls)
168
- with
185
+ with
169
186
  case_role_acls as (
170
187
  select
171
188
  id
@@ -182,8 +199,8 @@ module MkAcl
182
199
  where rolename = any(array[#{auth_role_list}]::text[])
183
200
  ),
184
201
  role_acls as (
185
- select * from case_role_acls
186
- union
202
+ select * from case_role_acls
203
+ union
187
204
  select * from auth_role_acls
188
205
  )
189
206
  select
@@ -192,7 +209,7 @@ module MkAcl
192
209
  l.table_name as "child_table",
193
210
  l.column_name as "child_field",
194
211
  array_agg(ra.id) as "acls"
195
- from
212
+ from
196
213
  acl.links l,
197
214
  role_acls ra
198
215
  where l.table_name = '#{table}'
@@ -56,13 +56,13 @@ module MkAcl
56
56
  for action in %w(select update delete).map { table.actions[_1] }
57
57
  rule_expr = action.rules.map { |rule|
58
58
  exprs = []
59
- exprs << "acl_#{action.name}[#{rule.index}:#{rule.index}][1:] && public.current_role_ids()" \
59
+ exprs << "acl_#{action.name}[#{rule.ordinal}:#{rule.ordinal}][1:] && public.current_role_ids()" \
60
60
  if !rule.roles.empty?
61
61
  exprs << "(#{rule.using})" if rule.using
62
62
  "(#{exprs.join(' and ')})"
63
63
  }.join(" or ")
64
64
  rule_expr = "false" if rule_expr.empty?
65
-
65
+
66
66
  puts %(
67
67
  drop policy if exists rls_#{action.name} on #{table.uid} cascade;
68
68
  create policy rls_#{action.name} on #{table.uid} for #{action.name}
@@ -0,0 +1,80 @@
1
+
2
+ module MkAcl
3
+ class Generator
4
+ class Seeds
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
+ clean_tables
16
+ generate_seeds
17
+ end
18
+
19
+ def self.generate(generator) self.new(generator).generate end
20
+
21
+ private
22
+ def clean_tables
23
+ puts %(
24
+ delete from acl_portal.acl_rules;
25
+ delete from acl_portal.acl_actions;
26
+ delete from acl_portal.acl_tables;
27
+ ).align
28
+ end
29
+
30
+ def generate_seeds
31
+ for table in spec.tables
32
+ puts %(
33
+ insert into acl_portal.acl_tables (
34
+ schema_name, table_name, domain,
35
+ parent_schema_name, parent_table_name, parent_link_field,
36
+ acl)
37
+ values (
38
+ '#{app_schema}', '#{table}', #{conn.quote_value(table.domain)},
39
+ '#{table.parent && table.app_schema}', '#{table.parent}', '#{table.parent_link_field}',
40
+ #{table.acl || 'false'})
41
+ returning id as "table_id"
42
+ \\gset
43
+ ).align
44
+ puts
45
+
46
+ table.actions.values.each { |action|
47
+ puts %(
48
+ insert into acl_portal.acl_actions (acl_table_id, kind)
49
+ values (:table_id, '#{action.name.upcase}')
50
+ returning id as "action_id"
51
+ \\gset
52
+ ).align
53
+ puts
54
+
55
+ action.rules.each { |rule|
56
+ fields = %w(roles filter assert fields tables ordinal)
57
+ values = fields.map { |field| conn.quote_value(rule.send(field.to_sym), elem_type: :text) }
58
+ puts %(
59
+ insert into acl_portal.acl_rules (acl_action_id, roles, filter, assert, fields, tables, ordinal)
60
+ values (:action_id, #{values.join(', ')});
61
+ ).align
62
+ puts
63
+ }
64
+ }
65
+ end
66
+
67
+ puts %(
68
+ update acl_portal.acl_tables sub
69
+ set parent_id = (
70
+ select id
71
+ from acl_portal.acl_tables super
72
+ where super.schema_name = sub.parent_schema_name
73
+ and super.table_name = sub.parent_table_name
74
+ );
75
+ ).align
76
+ puts
77
+ end
78
+ end
79
+ end
80
+ end
@@ -2,14 +2,29 @@
2
2
  module MkAcl
3
3
  class Parser
4
4
  attr_reader :file
5
+ attr_reader :symtab
5
6
 
6
- def initialize(file) @file = file end
7
+ def initialize(file)
8
+ @file = file
9
+ @symtab = SimpleSymtab::SimpleSymtab.new
10
+ end
7
11
  def parse() parse_spec end
8
12
  def self.parse(file) Parser.new(file).parse end
9
13
 
10
14
  private
11
15
  def error(*msg) raise ParseError, *msg end
12
- def norm_array(value) value.is_a?(Array) ? value : value&.split end
16
+
17
+ def norm_value(value)
18
+ value && symtab.interpolate(value)
19
+ end
20
+
21
+ def norm_array(value)
22
+ if value.is_a?(Array)
23
+ value.map { |v| norm_value(v) }
24
+ else
25
+ norm_value(value)&.split
26
+ end
27
+ end
13
28
 
14
29
  def parse_spec
15
30
  hash = YAML.load(IO.read(file), symbolize_names: true)
@@ -18,21 +33,31 @@ module MkAcl
18
33
  app_schema = schema[:app] or raise ArgumentError, "Can't find 'schema.app' attribute"
19
34
  acl_schema = schema[:acl] or raise ArgumentError, "Can't find 'schema.acl' attribute"
20
35
  spec = Spec.new(file, app_schema, acl_schema)
36
+ parse_variables(hash)
21
37
  parse_tables(spec, hash)
22
38
  spec
23
39
  end
24
40
 
41
+ def parse_variables(hash)
42
+ hash.delete_if { |k,v| k.to_s =~ /^(\$[A-Za-z0-9_]+)$/ and symtab[k] = v }
43
+ end
44
+
25
45
  def parse_tables(spec, tables)
26
46
  for table_name, actions in tables
27
- table = Table.new(spec, table_name, actions.delete(:domain))
47
+ acl = actions.key?(:acl) ? actions.delete(:acl) : true
48
+ parent = actions.delete(:parent)
49
+ domain = actions.delete(:domain)
50
+ table = Table.new(spec, table_name, domain, parent, acl)
28
51
  parse_actions(table, actions)
29
52
  end
30
53
  end
31
54
 
32
55
  def parse_actions(table, actions)
33
56
  for action_name, rules in actions
34
- constrain?(action_name, :insert, :select, :update, :delete) or error "Illegal action '#{action_name}'"
35
- constrain?(rules, String, Array, Hash) or error "Illegal value for #{action} action '#{rules}'"
57
+ constrain?(action_name, :insert, :select, :update, :delete, :attach) or
58
+ error "Illegal action '#{action_name}'"
59
+ constrain?(rules, String, Array, Hash) or
60
+ error "Illegal value for #{action} action '#{rules}'"
36
61
  action = Action.new(table, action_name)
37
62
 
38
63
  # Normalize rules
@@ -40,7 +65,7 @@ module MkAcl
40
65
  when Hash
41
66
  rules = [rules]
42
67
  when String
43
- rules = [{ role: rules }]
68
+ rules = [{ roles: rules }]
44
69
  end
45
70
 
46
71
  parse_rules(action, rules)
@@ -48,23 +73,22 @@ module MkAcl
48
73
  end
49
74
 
50
75
  def parse_rules(action, rules)
51
- index = 0
76
+ ordinal = 0
52
77
  for entry in rules
53
- rule = Rule.new(action, index += 1)
54
-
78
+ rule = Rule.new(action, ordinal += 1)
55
79
  for key, value in entry
56
80
  case key
57
- when :role; rule.roles = norm_array(value)
58
- when :using; rule.using = value
59
- when :check; rule.check = value
60
- when :include; rule.include = norm_array(value)
61
- when :exclude; rule.exclude = norm_array(value)
81
+ when :roles; rule.roles = norm_array(value)
82
+ when :filter; rule.filter = norm_value(value)
83
+ when :assert; rule.assert = norm_value(value)
84
+ when :fields; rule.fields = norm_array(value)
85
+ when :tables; rule.tables = norm_array(value)
62
86
  else
63
87
  raise ArgumentError, "Illegal field '#{key}' in #{action.table}.#{action}"
64
88
  end
65
89
  end
66
90
 
67
- !action.rules.empty? or
91
+ !action.rules.empty? or
68
92
  error "At least one rule is required in #{action.table}.#{action}"
69
93
  end
70
94
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'forward_to'
5
+
6
+ include ForwardTo
7
+
8
+ module SimpleSymtab
9
+ class Error < StandardError; end
10
+
11
+ class SimpleSymtab
12
+ def [](key)
13
+ resolve if !resolved?
14
+ @variables[key]
15
+ end
16
+
17
+ def []=(key, value)
18
+ !@variables.key?(key) or raise Error, "Redefinition of '#{key}'"
19
+ @variables[key] = value
20
+ @unresolved << key
21
+ end
22
+
23
+ forward_to :"@variables", :key?, :size, :empty?
24
+
25
+ def initialize(hash = {})
26
+ @variables = hash.dup
27
+ @unresolved = @variables.keys
28
+ end
29
+
30
+ def interpolate(s)
31
+ resolve if !resolved?
32
+ resolve_string(s)
33
+ end
34
+
35
+ private
36
+ def resolved?() @unresolved.empty? end
37
+
38
+ def resolve_string(val)
39
+ val.is_a?(String) or raise ArgumentError
40
+ seen = Set.new
41
+ while val =~ /(\$[A-Za-z0-9_]+\b)/
42
+ var = $1
43
+ seen.add?(var) or raise Error, "Circular definition of '#{var}'"
44
+ rep = @variables[var.to_sym] or raise Error, "Unknown variable '#{var}'"
45
+ val.gsub!(var, rep)
46
+ end
47
+ val
48
+ end
49
+
50
+ def resolve
51
+ @unresolved.each { |var|
52
+ @variables[var] = resolve_string(@variables[var])
53
+ }
54
+ @unresolved = []
55
+ end
56
+ end
57
+ end
58
+
@@ -2,7 +2,7 @@
2
2
  module MkAcl
3
3
  class Spec
4
4
  # Source SPEC file. Only for informational purposes
5
- attr_reader :file
5
+ attr_reader :file
6
6
 
7
7
  # Application schema that contains the ACL controlled tables
8
8
  attr_reader :app_schema
@@ -11,7 +11,7 @@ module MkAcl
11
11
  # (relatively) clean
12
12
  attr_reader :acl_schema
13
13
 
14
- # List of tables. Initialized by Table::initialize through #attach_table
14
+ # List of tables. Maintained by #attach_table
15
15
  attr_reader :tables
16
16
 
17
17
  # Spec acts as a hash from table name to Table object. Initialized by
@@ -45,11 +45,35 @@ module MkAcl
45
45
 
46
46
  class Table
47
47
  attr_reader :spec
48
- attr_accessor :parents # Parent TableSpec objects. Initialized by the analyzer
49
- attr_reader :uid # SCHEMA.TABLE name
48
+ forward_to :@spec, :app_schema, :acl_schema
49
+
50
+ # Hash from referenced table name to a tuple of the table object and the
51
+ # link field. Initialized by the analyzer
52
+ attr_accessor :references
53
+
54
+ # Table name and uid
50
55
  attr_reader :name
51
- attr_reader :record_name # Associated record name. Used in function names
52
- attr_accessor :domain # Security domain - either 'case' or 'event'. Initialized by the analyzer
56
+ attr_reader :uid # SCHEMA.TABLE name
57
+
58
+ # Parent domain table. Initialized by the analyzer
59
+ attr_accessor :parent
60
+
61
+ # Name of parent table. May be nil. Initialized by the parser
62
+ attr_accessor :parent_name
63
+
64
+ # Name of link field to parent record. Initialized by the analyzer
65
+ attr_accessor :parent_link_field
66
+
67
+ # Security domain name for this object. Domain object have themselves as
68
+ # domain, all other portal objects use the parent's domain. Initialized by
69
+ # the analyzer
70
+ attr_accessor :domain
71
+
72
+ # SQL to create the ACL for a table. No ACL if false, default ACL if nil
73
+ attr_accessor :acl
74
+
75
+ # Associated record name. Used in function names
76
+ attr_reader :record_name
53
77
 
54
78
  # Action objects
55
79
  def insert = @actions["insert"]
@@ -60,13 +84,15 @@ module MkAcl
60
84
  # Hash from action name to action object
61
85
  attr_reader :actions
62
86
 
63
- def initialize(spec, name, domain)
87
+ def initialize(spec, name, domain, parent_name, acl)
64
88
  @spec = spec
65
- @parents = []
89
+ @references = {}
66
90
  @name = name.to_s
67
- @uid = "#{@spec.app_schema}.#{@name}"
91
+ @uid = "#{app_schema}.#{@name}"
68
92
  @record_name = Prick::Inflector.singularize(@name)
93
+ @parent_name = parent_name
69
94
  @domain = domain
95
+ @acl = acl
70
96
  @actions = {}
71
97
  @spec.send :attach_table, self
72
98
  for action_name in %w(insert select update delete)
@@ -79,12 +105,19 @@ module MkAcl
79
105
 
80
106
  def dump
81
107
  puts "#{name}:"
82
- indent {
108
+ indent {
83
109
  puts "domain: #{domain}" if domain
84
- puts "parents: #{parents.inspect}"
110
+ puts "parent: #{parent}" if parent
111
+ puts "references: [#{references.values.map(&:first).map(&:name).join(' ')}]"
85
112
  for action_name in %w(insert select update delete)
86
113
  actions[action_name]&.dump
87
114
  end
115
+ case acl
116
+ when false; puts "acl: false"
117
+ when true;
118
+ puts "acl:"
119
+ indent { puts acl }
120
+ end
88
121
  }
89
122
  end
90
123
 
@@ -106,8 +139,6 @@ module MkAcl
106
139
  @table.send :attach_action, self
107
140
  end
108
141
 
109
- def fields() @include + @exclude.map { "-#{_1}" } end
110
-
111
142
  def to_s() name end
112
143
 
113
144
  def dump
@@ -116,7 +147,7 @@ module MkAcl
116
147
  else
117
148
  puts name
118
149
  indent {
119
- for rule in rules
150
+ for rule in rules.sort_by(&:ordinal)
120
151
  print "- "
121
152
  indent(bol: false) { rule.dump }
122
153
  end
@@ -131,33 +162,40 @@ module MkAcl
131
162
  end
132
163
 
133
164
  class Rule
165
+ using String::Text
166
+
134
167
  attr_reader :action
135
168
  forward_to :action, :table, :name
136
- attr_reader :index
137
169
  attr_accessor :roles
138
- attr_accessor :using # Goes into the postgres policy
139
- attr_accessor :check # Goes into the postgres trigger
140
- attr_accessor :include
141
- attr_accessor :exclude
170
+ attr_accessor :filter # Goes into the postgres policy
171
+ attr_accessor :assert # Goes into the postgres trigger
172
+ attr_accessor :fields # Only used for insert and update
173
+ attr_accessor :tables # Only used for attach
174
+ attr_reader :ordinal
142
175
 
176
+ # admin, internal, etc.
143
177
  def auth_roles() @auth_roles ||= roles.select { _1 == _1.downcase } end
178
+
179
+ # KON, AKK, etc.
144
180
  def case_roles() @case_roles ||= roles.select { _1 == _1.upcase } end
145
181
 
146
- def initialize(action, index)
182
+ def initialize(action, ordinal)
147
183
  @action = action
148
- @index = index
149
- @roles, @check, @include, @exclude = [], nil, [], []
184
+ @ordinal = ordinal
185
+ @roles = []
186
+ @fields = []
187
+ @tables = []
188
+
150
189
  action.send :attach_rule, self
151
190
  end
152
191
 
153
- def fields() @include + @exclude.map { "-#{_1}" } end
154
-
155
192
  def dump
156
- puts "index: #{index}"
157
- puts "roles: #{roles.join(' ')}"
158
- puts "check: #{check}" if check
159
- puts "include: #{include.join(' ')}" if !include.empty?
160
- puts "exclude: #{exclude.join(' ')}" if !exclude.empty?
193
+ puts "roles: [#{roles.join(' ')}]"
194
+ puts "filter: #{filter}" if filter
195
+ puts "assert: #{assert}" if assert
196
+ puts "fields: [#{fields.join(' ')}]" if !fields.empty?
197
+ puts "tables: [#{tables.join(' ')}]" if !tables.empty?
198
+ puts "ordinal: #{ordinal}"
161
199
  end
162
200
  end
163
201
  end
@@ -1,7 +1,7 @@
1
1
 
2
2
  require 'pg_conn'
3
3
  require 'indented_io'
4
- require 'string-text'
4
+ require 'string-text'; using String::Text
5
5
  require 'forward_to'; include ForwardTo
6
6
  require 'constrain'; include Constrain
7
7
  require 'prick-inflector'
@@ -20,6 +20,7 @@ module MkAcl
20
20
  ROLES = CASE_ROLES + EVENT_ROLES + VISIT_ROLES
21
21
  end
22
22
 
23
+ require_relative 'mkacl/simple_symtab.rb'
23
24
  require_relative 'mkacl/spec.rb'
24
25
  require_relative 'mkacl/parser.rb'
25
26
  require_relative 'mkacl/analyzer.rb'
@@ -0,0 +1,108 @@
1
+
2
+ class Analyzer
3
+ attr_reader :spec
4
+ def vars = spec.variables
5
+
6
+ def initialize(spec)
7
+ @spec = spec
8
+ end
9
+
10
+ def analyze
11
+ interpolate
12
+ expand_rules
13
+ merge_rules
14
+ spec
15
+ end
16
+
17
+ def self.analyze(spec)
18
+ self.new(spec).analyze
19
+ end
20
+
21
+ private
22
+ #
23
+ # Interpolate variables
24
+ #
25
+ def interpolate
26
+ interpolate_variables
27
+ interpolate_spec
28
+ end
29
+
30
+ def interpolate_variables
31
+ spec.variables.transform_values! { |a|
32
+ a.map { |e|
33
+ if e =~ /^\$[A-Za-z0-9_]+$/
34
+ spec.variables[e] or ShellOpts::error "Unknown variable #{e}"
35
+ else
36
+ e
37
+ end
38
+ }.flatten
39
+ }
40
+ end
41
+
42
+ def interpolate_spec
43
+ spec.rules.each { |r| interpolate_rule(r) }
44
+ end
45
+
46
+ def interpolate_rule(rule)
47
+ rule.tables = interpolate_array(rule.tables)
48
+ p rule.opers.map(&:class)
49
+
50
+ rule.opers.each { |op| interpolate_oper(op) }
51
+ end
52
+
53
+ def interpolate_oper(oper)
54
+ oper.accesses.each { |a| interpolate_access(a) }
55
+ end
56
+
57
+ def interpolate_access(access)
58
+ access.roles = interpolate_array(access.roles)
59
+ access.tables = interpolate_string(access.tables)
60
+ access.fields = interpolate_array(access.fields)
61
+ access.where = interpolate_string(access.where)
62
+ end
63
+
64
+ def interpolate_string(string)
65
+ return nil if string.nil?
66
+ for key, value in vars
67
+ next if !value.is_a?(String)
68
+ string.gsub!(/#{key}/, value)
69
+ end
70
+ string
71
+ end
72
+
73
+ def interpolate_array(array)
74
+ return nil if array.nil?
75
+ array.map { |e| vars[e] || e }.flatten
76
+ end
77
+
78
+ #
79
+ # Expand rules
80
+ #
81
+ def expand_rules()
82
+ rules = []
83
+ spec.rules.each { |rule|
84
+ rule.tables.each { |t| r = rule.dup; r.tables = [t]; rules << r }
85
+ }
86
+ spec.rules = rules
87
+ end
88
+
89
+ #
90
+ # Merge rules
91
+ #
92
+ def merge_rules()
93
+ spec.rules = spec.rules.group_by { |rule| rule.tables.first }.map { |table, rules|
94
+ rule = Rule.new([table])
95
+ for oper in Spec::OPERS
96
+ rule[oper] = merge_opers(oper, rules.map { |r| r[oper] }.flatten)
97
+ end
98
+ rule
99
+ }
100
+ end
101
+
102
+ def merge_opers(oper, opers)
103
+ oper = Oper.new(oper)
104
+ oper.accesses = opers.map { |op| op.accesses }.flatten
105
+ oper
106
+ end
107
+ end
108
+
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'yaml'
4
+
5
+ include ForwardTo
6
+
7
+ class Parser
8
+ def initialize(file)
9
+ @file = file
10
+ end
11
+
12
+ def parse
13
+ yaml = YAML.load(IO.read(@file).sub(/^__END__$.*/m, ""), symbolize_names: true)
14
+ rules = nil
15
+ variables = {}
16
+ yaml.each { |key, value|
17
+ case key
18
+ when :rules; rules = value.map { |v| parse_rule(v) }
19
+ when /^\$[A-Z0-9_]+$/; variables[key.to_s] = parse_array(value)
20
+ else
21
+ raise "Illegal section: #{key}"
22
+ end
23
+ }
24
+ Spec.new(@file, rules, variables)
25
+ end
26
+
27
+ def self.parse(file) self.new(file).parse end
28
+
29
+ private
30
+ def parse_rule(hash)
31
+ tables = parse_array(hash[:tables]) or error "Can't find 'table' key"
32
+ rule = Rule.new(tables)
33
+ for oper in Spec::OPERS
34
+ value = hash[oper] or next
35
+ rule.send(:"#{oper}=", parse_oper(oper, value))
36
+ end
37
+ rule
38
+ end
39
+
40
+ def parse_oper(oper, value)
41
+ case value
42
+ when String; Oper.new(oper, [Access.new(parse_array(value), nil, nil, nil)])
43
+ when Hash; Oper.new(oper, [parse_access(value)])
44
+ when Array; Oper.new(oper, value.map { |hash| parse_access(hash) })
45
+ else
46
+ error "Illegal argument: #{value.invaluet}"
47
+ end
48
+ end
49
+
50
+ def parse_access(hash)
51
+ illegal_keys = hash.keys - Spec::FIELDS
52
+ illegal_keys.empty? or error "Illegal keys: #{illegal_keys.join(', ')}"
53
+ roles = parse_array(hash[:roles])
54
+ tables = parse_array(hash[:tables])
55
+ fields = parse_array(hash[:fields])
56
+ where = hash[:where]
57
+ Access.new(roles, tables, fields, where)
58
+ end
59
+
60
+ def parse_array(value)
61
+ case value
62
+ when String; value.split
63
+ when Array; value
64
+ when nil; nil
65
+ else
66
+ error "Illegal array value: #{value.inspect}"
67
+ end
68
+ end
69
+ end
70
+
@@ -0,0 +1,86 @@
1
+
2
+ class Spec
3
+ OPERS = [:select, :insert, :update, :delete, :attach]
4
+ FIELDS = [:roles, :tables, :fields, :where]
5
+
6
+ attr_reader :file
7
+ attr_accessor :rules
8
+ attr_accessor :variables
9
+
10
+ forward_to :variables, :[], :[]=
11
+
12
+ def initialize(file, rules, variables)
13
+ @file, @rules, @variables = file, rules, variables
14
+ end
15
+
16
+ def dump
17
+ puts "File: #{file}"
18
+ puts "Variables"
19
+ indent { variables.each { |k,v| puts "#{k} = #{v.inspect}" } }
20
+ puts "Rules"
21
+ indent { rules.sort_by { |rule| rule.tables.first }.each(&:dump) }
22
+ end
23
+ end
24
+
25
+ class Rule
26
+ attr_accessor :tables
27
+ attr_accessor *Spec::OPERS
28
+
29
+ def initialize(tables)
30
+ @tables = tables
31
+ Spec::OPERS.each { |op| self[op] = Oper.new(op) }
32
+ end
33
+
34
+ def opers() = Spec::OPERS.map { |op| self.send(op) }.compact
35
+ def [](key) = self.send(key)
36
+ def []=(key, value) self.send(:"#{key}=", value) end
37
+
38
+ def to_s() = "Rule, tables: #{tables.join(', ')}"
39
+
40
+ def dump
41
+ puts "Rule"
42
+ indent {
43
+ puts "tables: #{tables}"
44
+ opers.each { |op| op.dump }
45
+ }
46
+ end
47
+ end
48
+
49
+ class Oper
50
+ attr_reader :name
51
+ attr_accessor :accesses
52
+
53
+ def self.symbol = self.to_s.sub(/Decl/, "").downcase.to_sym
54
+
55
+ def initialize(name, accesses = [])
56
+ @name = name
57
+ @accesses = accesses
58
+ end
59
+
60
+ def dump
61
+ puts "#{name}"
62
+ indent { accesses.each(&:dump) }
63
+ end
64
+ end
65
+
66
+ class Access
67
+ attr_accessor *Spec::FIELDS
68
+
69
+ def initialize(roles, table, fields, where)
70
+ @roles = roles
71
+ @table = table
72
+ @fields = fields
73
+ @where = where
74
+ end
75
+
76
+ def dump
77
+ puts "access:"
78
+ indent {
79
+ Spec::FIELDS.each { |f|
80
+ value = self.send(f) or next
81
+ puts "#{f}: #{value}"
82
+ }
83
+ }
84
+ end
85
+ end
86
+
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MikrasUtils
4
- VERSION = "0.3.3"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mikras_utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claus Rasmussen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-13 00:00:00.000000000 Z
11
+ date: 2025-01-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg_conn
@@ -72,6 +72,7 @@ email:
72
72
  executables:
73
73
  - mikras_utils
74
74
  - mkacl
75
+ - rls
75
76
  extensions: []
76
77
  extra_rdoc_files: []
77
78
  files:
@@ -84,7 +85,9 @@ files:
84
85
  - build
85
86
  - exe/mikras_utils
86
87
  - exe/mkacl
88
+ - exe/rls
87
89
  - lib/mikras_utils.rb
90
+ - lib/mikras_utils/mikras.rb
88
91
  - lib/mikras_utils/mkacl.rb
89
92
  - lib/mikras_utils/mkacl/analyzer.rb
90
93
  - lib/mikras_utils/mkacl/generator.rb
@@ -93,8 +96,13 @@ files:
93
96
  - lib/mikras_utils/mkacl/generators/insert_triggers.rb
94
97
  - lib/mikras_utils/mkacl/generators/role_functions.rb
95
98
  - lib/mikras_utils/mkacl/generators/rules.rb
99
+ - lib/mikras_utils/mkacl/generators/seeds.rb
96
100
  - lib/mikras_utils/mkacl/parser.rb
101
+ - lib/mikras_utils/mkacl/simple_symtab.rb
97
102
  - lib/mikras_utils/mkacl/spec.rb
103
+ - lib/mikras_utils/rls/analyzer.rb
104
+ - lib/mikras_utils/rls/parser.rb
105
+ - lib/mikras_utils/rls/spec.rb
98
106
  - lib/mikras_utils/version.rb
99
107
  - sig/mikras_utils.rbs
100
108
  - tests/acl.fox