posgra 0.1.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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +99 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/posgra +15 -0
- data/lib/posgra.rb +33 -0
- data/lib/posgra/cli.rb +6 -0
- data/lib/posgra/cli/app.rb +16 -0
- data/lib/posgra/cli/grant.rb +68 -0
- data/lib/posgra/cli/helper.rb +37 -0
- data/lib/posgra/cli/role.rb +35 -0
- data/lib/posgra/client.rb +247 -0
- data/lib/posgra/driver.rb +354 -0
- data/lib/posgra/dsl.rb +17 -0
- data/lib/posgra/dsl/converter.rb +136 -0
- data/lib/posgra/dsl/grants.rb +53 -0
- data/lib/posgra/dsl/grants/role.rb +23 -0
- data/lib/posgra/dsl/grants/role/schema.rb +22 -0
- data/lib/posgra/dsl/grants/role/schema/on.rb +22 -0
- data/lib/posgra/dsl/roles.rb +67 -0
- data/lib/posgra/dsl/roles/group.rb +24 -0
- data/lib/posgra/exporter.rb +25 -0
- data/lib/posgra/ext/string_ext.rb +25 -0
- data/lib/posgra/identifier.rb +2 -0
- data/lib/posgra/identifier/auto.rb +38 -0
- data/lib/posgra/logger.rb +33 -0
- data/lib/posgra/template.rb +18 -0
- data/lib/posgra/utils.rb +17 -0
- data/lib/posgra/version.rb +3 -0
- data/posgra.gemspec +32 -0
- metadata +206 -0
@@ -0,0 +1,354 @@
|
|
1
|
+
class Posgra::Driver
|
2
|
+
include Posgra::Logger::Helper
|
3
|
+
include Posgra::Utils::Helper
|
4
|
+
|
5
|
+
DEFAULT_ACL = '{%s=arwdDxt/%s}'
|
6
|
+
|
7
|
+
PRIVILEGE_TYPES = {
|
8
|
+
'a' => 'INSERT',
|
9
|
+
'r' => 'SELECT',
|
10
|
+
'w' => 'UPDATE',
|
11
|
+
'd' => 'DELETE',
|
12
|
+
'D' => 'TRUNCATE',
|
13
|
+
'x' => 'REFERENCES',
|
14
|
+
't' => 'TRIGGER',
|
15
|
+
}
|
16
|
+
|
17
|
+
def initialize(client, options = {})
|
18
|
+
unless client.type_map_for_results.is_a?(PG::TypeMapAllStrings)
|
19
|
+
raise 'PG::Connection#type_map_for_results must be PG::TypeMapAllStrings'
|
20
|
+
end
|
21
|
+
|
22
|
+
@client = client
|
23
|
+
@options = options
|
24
|
+
@identifier = options.fetch(:identifier)
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_user(user)
|
28
|
+
updated = false
|
29
|
+
|
30
|
+
password = @identifier.identify(user)
|
31
|
+
sql = "CREATE USER #{@client.escape_identifier(user)} PASSWORD #{@client.escape_literal(password)}"
|
32
|
+
log(:info, sql, :color => :cyan)
|
33
|
+
|
34
|
+
unless @options[:dry_run]
|
35
|
+
@client.query(sql)
|
36
|
+
updated = true
|
37
|
+
end
|
38
|
+
|
39
|
+
updated
|
40
|
+
end
|
41
|
+
|
42
|
+
def drop_user(user)
|
43
|
+
updated = false
|
44
|
+
|
45
|
+
sql = "DROP USER #{@client.escape_identifier(user)}"
|
46
|
+
log(:info, sql, :color => :red)
|
47
|
+
|
48
|
+
unless @options[:dry_run]
|
49
|
+
@client.query(sql)
|
50
|
+
updated = true
|
51
|
+
end
|
52
|
+
|
53
|
+
updated
|
54
|
+
end
|
55
|
+
|
56
|
+
def create_group(group)
|
57
|
+
updated = false
|
58
|
+
|
59
|
+
sql = "CREATE GROUP #{@client.escape_identifier(group)}"
|
60
|
+
log(:info, sql, :color => :cyan)
|
61
|
+
|
62
|
+
unless @options[:dry_run]
|
63
|
+
@client.query(sql)
|
64
|
+
updated = true
|
65
|
+
end
|
66
|
+
|
67
|
+
updated
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_user_to_group(user, group)
|
71
|
+
updated = false
|
72
|
+
|
73
|
+
sql = "ALTER GROUP #{@client.escape_identifier(group)} ADD USER #{@client.escape_identifier(user)}"
|
74
|
+
log(:info, sql, :color => :green)
|
75
|
+
|
76
|
+
unless @options[:dry_run]
|
77
|
+
@client.query(sql)
|
78
|
+
updated = true
|
79
|
+
end
|
80
|
+
|
81
|
+
updated
|
82
|
+
end
|
83
|
+
|
84
|
+
def drop_user_from_group(user, group)
|
85
|
+
updated = false
|
86
|
+
|
87
|
+
sql = "ALTER GROUP #{@client.escape_identifier(group)} DROP USER #{@client.escape_identifier(user)}"
|
88
|
+
log(:info, sql, :color => :cyan)
|
89
|
+
|
90
|
+
unless @options[:dry_run]
|
91
|
+
@client.query(sql)
|
92
|
+
updated = true
|
93
|
+
end
|
94
|
+
|
95
|
+
updated
|
96
|
+
end
|
97
|
+
|
98
|
+
def drop_group(group)
|
99
|
+
updated = false
|
100
|
+
|
101
|
+
sql = "DROP GROUP #{@client.escape_identifier(group)}"
|
102
|
+
log(:info, sql, :color => :red)
|
103
|
+
|
104
|
+
unless @options[:dry_run]
|
105
|
+
@client.query(sql)
|
106
|
+
updated = true
|
107
|
+
end
|
108
|
+
|
109
|
+
updated
|
110
|
+
end
|
111
|
+
|
112
|
+
def revoke_all_on_schema(role, schema)
|
113
|
+
updated = false
|
114
|
+
|
115
|
+
sql = "REVOKE ALL ON ALL TABLES IN SCHEMA #{@client.escape_identifier(schema)} FROM #{@client.escape_identifier(role)}"
|
116
|
+
log(:info, sql, :color => :green)
|
117
|
+
|
118
|
+
unless @options[:dry_run]
|
119
|
+
@client.query(sql)
|
120
|
+
updated = true
|
121
|
+
end
|
122
|
+
|
123
|
+
updated
|
124
|
+
end
|
125
|
+
|
126
|
+
def revoke_all_on_object(role, schema, object)
|
127
|
+
updated = false
|
128
|
+
|
129
|
+
sql = "REVOKE ALL ON #{@client.escape_identifier(schema)}.#{@client.escape_identifier(object)} FROM #{@client.escape_identifier(role)}"
|
130
|
+
log(:info, sql, :color => :green)
|
131
|
+
|
132
|
+
unless @options[:dry_run]
|
133
|
+
@client.query(sql)
|
134
|
+
updated = true
|
135
|
+
end
|
136
|
+
|
137
|
+
updated
|
138
|
+
end
|
139
|
+
|
140
|
+
def grant(role, priv, options, schema, object)
|
141
|
+
updated = false
|
142
|
+
|
143
|
+
sql = "GRANT #{priv} ON #{@client.escape_identifier(schema)}.#{@client.escape_identifier(object)} TO #{@client.escape_identifier(role)}"
|
144
|
+
|
145
|
+
if options['is_grantable']
|
146
|
+
sql << ' WITH GRANT OPTION'
|
147
|
+
end
|
148
|
+
|
149
|
+
log(:info, sql, :color => :green)
|
150
|
+
|
151
|
+
unless @options[:dry_run]
|
152
|
+
@client.query(sql)
|
153
|
+
updated = true
|
154
|
+
end
|
155
|
+
|
156
|
+
updated
|
157
|
+
end
|
158
|
+
|
159
|
+
def update_grant_options(role, priv, options, schema, object)
|
160
|
+
updated = false
|
161
|
+
|
162
|
+
if options.fetch('is_grantable')
|
163
|
+
updated = grant_grant_option(role, priv, schema, object)
|
164
|
+
else
|
165
|
+
updated = roveke_grant_option(role, priv, schema, object)
|
166
|
+
end
|
167
|
+
|
168
|
+
updated
|
169
|
+
end
|
170
|
+
|
171
|
+
def grant_grant_option(role, priv, schema, object)
|
172
|
+
updated = false
|
173
|
+
|
174
|
+
sql = "GRANT #{priv} ON #{@client.escape_identifier(schema)}.#{@client.escape_identifier(object)} TO #{@client.escape_identifier(role)} WITH GRANT OPTION"
|
175
|
+
log(:info, sql, :color => :green)
|
176
|
+
|
177
|
+
unless @options[:dry_run]
|
178
|
+
@client.query(sql)
|
179
|
+
updated = true
|
180
|
+
end
|
181
|
+
|
182
|
+
updated
|
183
|
+
end
|
184
|
+
|
185
|
+
def roveke_grant_option(role, priv, schema, object)
|
186
|
+
updated = false
|
187
|
+
|
188
|
+
sql = "REVOKE GRANT OPTION FOR #{priv} ON #{@client.escape_identifier(schema)}.#{@client.escape_identifier(object)} FROM #{@client.escape_identifier(role)}"
|
189
|
+
log(:info, sql, :color => :green)
|
190
|
+
|
191
|
+
unless @options[:dry_run]
|
192
|
+
@client.query(sql)
|
193
|
+
updated = true
|
194
|
+
end
|
195
|
+
|
196
|
+
updated
|
197
|
+
end
|
198
|
+
|
199
|
+
def revoke(role, priv, schema, object)
|
200
|
+
updated = false
|
201
|
+
|
202
|
+
sql = "REVOKE #{priv} ON #{@client.escape_identifier(schema)}.#{@client.escape_identifier(object)} FROM #{@client.escape_identifier(role)}"
|
203
|
+
log(:info, sql, :color => :green)
|
204
|
+
|
205
|
+
unless @options[:dry_run]
|
206
|
+
@client.query(sql)
|
207
|
+
updated = true
|
208
|
+
end
|
209
|
+
|
210
|
+
updated
|
211
|
+
end
|
212
|
+
|
213
|
+
def describe_objects(schema)
|
214
|
+
rs = @client.exec <<-SQL
|
215
|
+
SELECT
|
216
|
+
pg_class.relname,
|
217
|
+
pg_namespace.nspname
|
218
|
+
FROM
|
219
|
+
pg_class
|
220
|
+
INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
|
221
|
+
WHERE
|
222
|
+
pg_namespace.nspname = #{@client.escape_literal(schema)}
|
223
|
+
AND pg_class.relkind NOT IN ('i')
|
224
|
+
SQL
|
225
|
+
|
226
|
+
rs.map do |row|
|
227
|
+
row.fetch('relname')
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def describe_users
|
232
|
+
rs = @client.exec('SELECT * FROM pg_user')
|
233
|
+
|
234
|
+
options_by_user = {}
|
235
|
+
|
236
|
+
rs.each do |row|
|
237
|
+
user = row.fetch('usename')
|
238
|
+
next unless matched?(user, @options[:include_role], @options[:exclude_role])
|
239
|
+
options_by_user[user] = row.select {|_, v| v == 't' }.keys
|
240
|
+
end
|
241
|
+
|
242
|
+
options_by_user
|
243
|
+
end
|
244
|
+
|
245
|
+
def describe_groups
|
246
|
+
rs = @client.exec <<-SQL
|
247
|
+
SELECT
|
248
|
+
pg_group.groname,
|
249
|
+
pg_user.usename
|
250
|
+
FROM
|
251
|
+
pg_group
|
252
|
+
LEFT JOIN pg_user ON pg_user.usesysid = ANY(pg_group.grolist)
|
253
|
+
SQL
|
254
|
+
|
255
|
+
users_by_group = {}
|
256
|
+
|
257
|
+
rs.each do |row|
|
258
|
+
group = row.fetch('groname')
|
259
|
+
user = row.fetch('usename')
|
260
|
+
next unless [group, user].any? {|i| matched?(i, @options[:include_role], @options[:exclude_role]) }
|
261
|
+
users_by_group[group] ||= []
|
262
|
+
users_by_group[group] << user if user
|
263
|
+
end
|
264
|
+
|
265
|
+
users_by_group
|
266
|
+
end
|
267
|
+
|
268
|
+
def describe_grants
|
269
|
+
rs = @client.exec <<-SQL
|
270
|
+
SELECT
|
271
|
+
pg_class.relname,
|
272
|
+
pg_namespace.nspname,
|
273
|
+
pg_class.relacl,
|
274
|
+
pg_user.usename
|
275
|
+
FROM
|
276
|
+
pg_class
|
277
|
+
INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
|
278
|
+
INNER JOIN pg_user ON pg_class.relowner = pg_user.usesysid
|
279
|
+
WHERE
|
280
|
+
pg_class.relkind NOT IN ('i')
|
281
|
+
SQL
|
282
|
+
|
283
|
+
grants_by_role = {}
|
284
|
+
rs.each do |row|
|
285
|
+
relname = row.fetch('relname')
|
286
|
+
nspname = row.fetch('nspname')
|
287
|
+
relacl = row.fetch('relacl')
|
288
|
+
usename = row.fetch('usename')
|
289
|
+
|
290
|
+
next unless matched?(nspname, @options[:include_schema], @options[:exclude_schema])
|
291
|
+
|
292
|
+
parse_aclitems(relacl, usename).each do |aclitem|
|
293
|
+
role = aclitem.fetch('grantee')
|
294
|
+
privs = aclitem.fetch('privileges')
|
295
|
+
next unless matched?(role, @options[:include_role], @options[:exclude_role])
|
296
|
+
grants_by_role[role] ||= {}
|
297
|
+
grants_by_role[role][nspname] ||= {}
|
298
|
+
grants_by_role[role][nspname][relname] = privs
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
grants_by_role
|
303
|
+
end
|
304
|
+
|
305
|
+
def describe_schemas
|
306
|
+
rs = @client.exec <<-SQL
|
307
|
+
SELECT
|
308
|
+
nspname
|
309
|
+
FROM
|
310
|
+
pg_namespace
|
311
|
+
SQL
|
312
|
+
|
313
|
+
rs.map do |row|
|
314
|
+
row.fetch('nspname')
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
private
|
319
|
+
|
320
|
+
def parse_aclitems(aclitems, owner)
|
321
|
+
aclitems ||= DEFAULT_ACL % [owner, owner]
|
322
|
+
aclitems = aclitems[1..-2].split(',')
|
323
|
+
|
324
|
+
aclitems.map do |aclitem|
|
325
|
+
grantee, privileges_grantor = aclitem.split('=', 2)
|
326
|
+
privileges, grantor = privileges_grantor.split('/', 2)
|
327
|
+
|
328
|
+
{
|
329
|
+
'grantee' => grantee,
|
330
|
+
'privileges' => expand_privileges(privileges),
|
331
|
+
'grantor' => grantor,
|
332
|
+
}
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def expand_privileges(privileges)
|
337
|
+
options_by_privilege = {}
|
338
|
+
|
339
|
+
privileges.scan(/([a-z])(\*)?/i).each do |privilege_type_char,is_grantable|
|
340
|
+
privilege_type = PRIVILEGE_TYPES[privilege_type_char]
|
341
|
+
|
342
|
+
unless privilege_type
|
343
|
+
log(:warn, "unknown privilege type: #{privilege_type_char}", :color => :yellow)
|
344
|
+
next
|
345
|
+
end
|
346
|
+
|
347
|
+
options_by_privilege[privilege_type] = {
|
348
|
+
'is_grantable' => !!is_grantable,
|
349
|
+
}
|
350
|
+
end
|
351
|
+
|
352
|
+
options_by_privilege
|
353
|
+
end
|
354
|
+
end
|
data/lib/posgra/dsl.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class Posgra::DSL
|
2
|
+
def self.convert_roles(exported, options = {})
|
3
|
+
Posgra::DSL::Converter.convert_roles(exported, options)
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.convert_grants(exported, options = {})
|
7
|
+
Posgra::DSL::Converter.convert_grants(exported, options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.parse_roles(dsl, path, options = {})
|
11
|
+
Posgra::DSL::Roles.eval(dsl, path, options).result
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.parse_grants(dsl, path, options = {})
|
15
|
+
Posgra::DSL::Grants.eval(dsl, path, options).result
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
class Posgra::DSL::Converter
|
2
|
+
def self.convert_roles(exported, options = {})
|
3
|
+
self.new(exported, options).convert_roles
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.convert_grants(exported, options = {})
|
7
|
+
self.new(exported, options).convert_grants
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(exported, options = {})
|
11
|
+
@exported = exported
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def convert_roles
|
16
|
+
users_by_group = @exported[:users_by_group] || {}
|
17
|
+
users = @exported.fetch(:users, []) - users_by_group.values.flatten
|
18
|
+
|
19
|
+
[
|
20
|
+
output_users(users),
|
21
|
+
output_groups(users_by_group),
|
22
|
+
].join("\n").strip
|
23
|
+
end
|
24
|
+
|
25
|
+
def convert_grants
|
26
|
+
grants_by_role = @exported || {}
|
27
|
+
output_roles(grants_by_role).strip
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def output_users(users)
|
33
|
+
users.sort.map {|user|
|
34
|
+
"user #{user.inspect}"
|
35
|
+
}.join("\n") + "\n"
|
36
|
+
end
|
37
|
+
|
38
|
+
def output_groups(users_by_group)
|
39
|
+
users_by_group.sort_by {|g, _| g }.map {|group, users|
|
40
|
+
output_group(group, users)
|
41
|
+
}.join("\n")
|
42
|
+
end
|
43
|
+
|
44
|
+
def output_group(group, users)
|
45
|
+
if users.empty?
|
46
|
+
users = "# no users"
|
47
|
+
else
|
48
|
+
users = users.sort.map {|user|
|
49
|
+
"user #{user.inspect}"
|
50
|
+
}.join("\n ")
|
51
|
+
end
|
52
|
+
|
53
|
+
<<-EOS
|
54
|
+
group #{group.inspect} do
|
55
|
+
#{users}
|
56
|
+
end
|
57
|
+
EOS
|
58
|
+
end
|
59
|
+
|
60
|
+
def output_roles(grants_by_role)
|
61
|
+
grants_by_role.sort_by {|r, _| r }.map {|role, grants_by_schema|
|
62
|
+
output_role(role, grants_by_schema)
|
63
|
+
}.join("\n")
|
64
|
+
end
|
65
|
+
|
66
|
+
def output_role(role, grants_by_schema)
|
67
|
+
if grants_by_schema.empty?
|
68
|
+
schemas = "# no schemas"
|
69
|
+
else
|
70
|
+
schemas = output_schemas(grants_by_schema)
|
71
|
+
end
|
72
|
+
|
73
|
+
<<-EOS
|
74
|
+
role #{role.inspect} do
|
75
|
+
#{schemas}
|
76
|
+
end
|
77
|
+
EOS
|
78
|
+
end
|
79
|
+
|
80
|
+
def output_schemas(grants_by_schema)
|
81
|
+
grants_by_schema.sort_by {|s, _| s }.map {|schema, grants_by_object|
|
82
|
+
output_schema(schema, grants_by_object).strip
|
83
|
+
}.join("\n ")
|
84
|
+
end
|
85
|
+
|
86
|
+
def output_schema(schema, grants_by_object)
|
87
|
+
if grants_by_object.empty?
|
88
|
+
objects = "# no objects"
|
89
|
+
else
|
90
|
+
objects = output_objects(grants_by_object)
|
91
|
+
end
|
92
|
+
|
93
|
+
<<-EOS
|
94
|
+
schema #{schema.inspect} do
|
95
|
+
#{objects}
|
96
|
+
end
|
97
|
+
EOS
|
98
|
+
end
|
99
|
+
|
100
|
+
def output_objects(grants_by_object)
|
101
|
+
grants_by_object.sort_by {|o, _| o }.map {|object, grants|
|
102
|
+
output_object(object, grants).strip
|
103
|
+
}.join("\n ")
|
104
|
+
end
|
105
|
+
|
106
|
+
def output_object(object, grants)
|
107
|
+
if grants.empty?
|
108
|
+
grants = "# no grants"
|
109
|
+
else
|
110
|
+
grants = output_grants(grants)
|
111
|
+
end
|
112
|
+
|
113
|
+
<<-EOS
|
114
|
+
on #{object.inspect} do
|
115
|
+
#{grants}
|
116
|
+
end
|
117
|
+
EOS
|
118
|
+
end
|
119
|
+
|
120
|
+
def output_grants(grants)
|
121
|
+
grants.sort_by {|g| g.to_s }.map {|privilege_type, options|
|
122
|
+
output_grant(privilege_type, options).strip
|
123
|
+
}.join("\n ")
|
124
|
+
end
|
125
|
+
|
126
|
+
def output_grant(privilege_type, options)
|
127
|
+
is_grantable = options.fetch('is_grantable')
|
128
|
+
out = "grant #{privilege_type.inspect}"
|
129
|
+
|
130
|
+
if is_grantable
|
131
|
+
out << ", :grantable => #{is_grantable}"
|
132
|
+
end
|
133
|
+
|
134
|
+
out
|
135
|
+
end
|
136
|
+
end
|