mikras_utils 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,21 @@
1
1
  module MkAcl
2
+ # Generates the functions
3
+ #
4
+ # acl_portal.user_is_role(_user_id integer, _object_id integer, _roles text[])
5
+ # acl_portal.user_is_role(_user_id integer, _object_id integer, _roles text)
6
+ # Returns true if the user has one of the given roles on the domain
7
+ # object
8
+ #
9
+ # public.current_is_role(_object_id integer, _roles text[])
10
+ # public.current_is_role(_object_id integer, _roles text)
11
+ # Returns true if the current user has one of the given roles on the
12
+ # domain object
13
+ #
14
+ # public.current_is_ROLE(_object_id integer)
15
+ # ROLE is one of the role kinds. Returns true if the current user has the
16
+ # given role
17
+ #
18
+
2
19
  class Generator
3
20
  class RoleFunctions
4
21
  using String::Text
@@ -20,54 +37,34 @@ module MkAcl
20
37
  private
21
38
  def generate_user_role_functions
22
39
  # TODO: Test. Then combine with per-table methods
23
- signature = "#{acl_schema}.user_is_role(_user_id integer, _domain_id integer, _roles text[])"
40
+ signature = "#{acl_schema}.user_is_role(_user_id integer, _object_id integer, _roles text[])"
24
41
  puts %(
25
42
  -- Return true if the user possess one or more of the given roles on the
26
- -- domain record (cases, events, visits) with the given ID
43
+ -- object (cases, events, visits) with the given ID
27
44
  --
28
45
  drop function if exists #{signature} cascade;
29
46
  create function #{signature} returns boolean as $$
30
47
  select exists (
31
48
  select
32
- from #{app_schema}.case_roles cr
33
- join #{app_schema}.case_role_users cru on cru.case_role_id = cr.id
34
- where cr.case_id = _domain_id
35
- and cru.user_id = _user_id
36
- and cr.kind = any(_roles)
37
-
38
- union
39
-
40
- select
41
- from #{app_schema}.event_roles cr
42
- join #{app_schema}.event_role_users cru on cru.event_role_id = cr.id
43
- where cr.event_id = _domain_id
44
- and cru.user_id = _user_id
45
- and cr.kind = any(_roles)
46
-
47
- union
48
-
49
- select
50
- from #{app_schema}.visit_roles cr
51
- join #{app_schema}.visit_role_users cru on cru.visit_role_id = cr.id
52
- where cr.visit_id = _domain_id
53
- and cru.user_id = _user_id
54
- and cr.kind = any(_roles)
55
-
56
- );
49
+ from app_portal.domain_users
50
+ where domain_id = _domain_id
51
+ and user_id = _user_id
52
+ and role = any(_roles)
53
+ )
57
54
  $$ language sql
58
55
  security definer;
59
56
  ).align
60
57
  puts
61
58
 
62
- signature = "#{acl_schema}.user_is_role(_user_id integer, _domain_id integer, role text)"
59
+ signature = "#{acl_schema}.user_is_role(_user_id integer, _object_id integer, role text)"
63
60
  puts %(
64
- -- Return true if the user has the given role on the domain record
61
+ -- Return true if the user has the given role on the object
65
62
  -- (cases, events, visits). Note that this function overloads the multi-role
66
63
  -- version
67
64
  --
68
65
  drop function if exists #{signature} cascade;
69
66
  create function #{signature} returns boolean as $$
70
- select #{acl_schema}.user_is_role(_user_id, _domain_id, array[role]);
67
+ select #{acl_schema}.user_is_role(_user_id, _object_id, array[role]);
71
68
  $$ language sql
72
69
  security definer;
73
70
  ).align
@@ -75,13 +72,13 @@ module MkAcl
75
72
 
76
73
  for domain, roles in DOMAINS.zip([CASE_ROLES, EVENT_ROLES, VISIT_ROLES])
77
74
  for role in roles
78
- signature = "#{acl_schema}.user_is_#{role.downcase}(_user_id integer, _domain_id integer)"
75
+ signature = "#{acl_schema}.user_is_#{role.downcase}(_user_id integer, _object_id integer)"
79
76
  puts %(
80
77
  -- Return true if the user possess the '#{role}' role
81
78
  --
82
79
  drop function if exists #{signature} cascade;
83
80
  create function #{signature} returns boolean as $$
84
- select #{acl_schema}.user_is_role(_user_id, _domain_id, '#{role}');
81
+ select #{acl_schema}.user_is_role(_user_id, _object_id, '#{role}');
85
82
  $$ language sql
86
83
  security definer;
87
84
  ).align
@@ -92,28 +89,28 @@ module MkAcl
92
89
 
93
90
  def generate_current_role_functions
94
91
  # TODO: Test. Then combine with per-table methods
95
- signature = "public.current_is_role(_domain_id integer, _roles text[])"
92
+ signature = "public.current_is_role(_object_id integer, _roles text[])"
96
93
  puts %(
97
94
  -- Return true if the current user possess one or more of the given roles on the
98
- -- domain record (cases, events, visits) with the given ID
95
+ -- object (cases, events, visits) with the given ID
99
96
  --
100
97
  drop function if exists #{signature} cascade;
101
98
  create function #{signature} returns boolean as $$
102
- select #{acl_schema}.user_is_role(public.current_user_id(), _domain_id, _roles);
99
+ select #{acl_schema}.user_is_role(public.current_user_id(), _object_id, _roles);
103
100
  $$ language sql
104
101
  security definer;
105
102
  ).align
106
103
  puts
107
104
 
108
- signature = "public.current_is_role(_domain_id integer, _role text)"
105
+ signature = "public.current_is_role(_object_id integer, _role text)"
109
106
  puts %(
110
- -- Return true if the current user possess the given role on the domain record
107
+ -- Return true if the current user possess the given role on the object
111
108
  -- (cases, events, visits). Note that this function overloads the multi-role
112
109
  -- version
113
110
  --
114
111
  drop function if exists #{signature} cascade;
115
112
  create function #{signature} returns boolean as $$
116
- select #{acl_schema}.user_is_role(public.current_user_id(), _domain_id, array[_role]);
113
+ select #{acl_schema}.user_is_role(public.current_user_id(), _object_id, array[_role]);
117
114
  $$ language sql
118
115
  security definer;
119
116
  ).align
@@ -121,13 +118,13 @@ module MkAcl
121
118
 
122
119
  for domain, roles in DOMAINS.zip([CASE_ROLES, EVENT_ROLES, VISIT_ROLES])
123
120
  for role in roles
124
- signature = "public.current_is_#{role.downcase}(_domain_id integer)"
121
+ signature = "public.current_is_#{role.downcase}(_object_id integer)"
125
122
  puts %(
126
123
  -- Return true if the current user possess the '#{role}' role
127
124
  --
128
125
  drop function if exists #{signature} cascade;
129
126
  create function #{signature} returns boolean as $$
130
- select #{acl_schema}.user_is_role(public.current_user_id(), _domain_id, '#{role}');
127
+ select #{acl_schema}.user_is_role(public.current_user_id(), _object_id, '#{role}');
131
128
  $$ language sql
132
129
  security definer;
133
130
  ).align
@@ -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'
@@ -12,6 +12,7 @@ module MkAcl
12
12
  DOMAINS = %w(case event visit)
13
13
  DOMAIN_TABLES = DOMAINS.map { "#{_1}s" }
14
14
 
15
+ # TODO Read from database (or maybe not)
15
16
  CASE_ROLES = %w(LA TA KON AKK RLA CLA CTA)
16
17
  EVENT_ROLES = %w(ELA ETA)
17
18
  VISIT_ROLES = %w(VLA VTA)
@@ -19,6 +20,7 @@ module MkAcl
19
20
  ROLES = CASE_ROLES + EVENT_ROLES + VISIT_ROLES
20
21
  end
21
22
 
23
+ require_relative 'mkacl/simple_symtab.rb'
22
24
  require_relative 'mkacl/spec.rb'
23
25
  require_relative 'mkacl/parser.rb'
24
26
  require_relative 'mkacl/analyzer.rb'