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 +4 -4
- data/exe/mkacl +20 -9
- data/exe/rls +30 -0
- data/lib/mikras_utils/mikras.rb +24 -0
- data/lib/mikras_utils/mkacl/analyzer.rb +35 -21
- data/lib/mikras_utils/mkacl/generator.rb +4 -2
- data/lib/mikras_utils/mkacl/generators/acl_functions.rb +26 -9
- data/lib/mikras_utils/mkacl/generators/rules.rb +2 -2
- data/lib/mikras_utils/mkacl/generators/seeds.rb +80 -0
- data/lib/mikras_utils/mkacl/parser.rb +39 -15
- data/lib/mikras_utils/mkacl/simple_symtab.rb +58 -0
- data/lib/mikras_utils/mkacl/spec.rb +67 -29
- data/lib/mikras_utils/mkacl.rb +2 -1
- data/lib/mikras_utils/rls/analyzer.rb +108 -0
- data/lib/mikras_utils/rls/parser.rb +70 -0
- data/lib/mikras_utils/rls/spec.rb +86 -0
- data/lib/mikras_utils/version.rb +1 -1
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 74256281ee10bf5186723bfa260cd62dda3057216660aa945c7b591fa1581177
|
4
|
+
data.tar.gz: 882636fab035922c146cc33c33a5af9deaf0da818bec6897e17a84ad8f1f2289
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
60
|
-
|
61
|
-
|
62
|
-
conn
|
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
|
-
|
74
|
-
|
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
|
-
|
22
|
-
|
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
|
-
#
|
30
|
-
for child_table_name,
|
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]
|
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
|
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
|
-
|
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
|
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.
|
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)
|
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
|
-
|
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
|
-
|
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
|
35
|
-
|
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 = [{
|
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
|
-
|
76
|
+
ordinal = 0
|
52
77
|
for entry in rules
|
53
|
-
rule = Rule.new(action,
|
54
|
-
|
78
|
+
rule = Rule.new(action, ordinal += 1)
|
55
79
|
for key, value in entry
|
56
80
|
case key
|
57
|
-
when :
|
58
|
-
when :
|
59
|
-
when :
|
60
|
-
when :
|
61
|
-
when :
|
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.
|
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
|
-
|
49
|
-
|
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 :
|
52
|
-
|
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
|
-
@
|
89
|
+
@references = {}
|
66
90
|
@name = name.to_s
|
67
|
-
@uid = "#{
|
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 "
|
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 :
|
139
|
-
attr_accessor :
|
140
|
-
attr_accessor :
|
141
|
-
attr_accessor :
|
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,
|
182
|
+
def initialize(action, ordinal)
|
147
183
|
@action = action
|
148
|
-
@
|
149
|
-
@roles
|
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 "
|
157
|
-
puts "
|
158
|
-
puts "
|
159
|
-
puts "
|
160
|
-
puts "
|
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
|
data/lib/mikras_utils/mkacl.rb
CHANGED
@@ -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
|
+
|
data/lib/mikras_utils/version.rb
CHANGED
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.
|
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:
|
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
|