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