mikras_utils 0.3.3 → 0.4.0

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