posgra 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 779439047ffa052724a0188fddd2d07291d632be
4
+ data.tar.gz: dc13051ea5f743c90bb03f17e8552b8042403804
5
+ SHA512:
6
+ metadata.gz: 969080594fab8c8fd0f4b99d7c7188ab52e15e9147a51caf55c41df74bc9485245460439639cde467e3f7fa491eca91382630e5983b9f884af162b5e877f44dd
7
+ data.tar.gz: 685cbe3e19daba11a16d2f4852ccab18794c4eebb8c183128ee0428e557a34d66f0b91ddb724bad9bc173aadd90dcdf0293fd7f154a88dbb1b9e3ba6afea4d88
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /*.rb
11
+ account.csv
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,16 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1.8
6
+ - 2.2.4
7
+ - 2.3.0
8
+ before_install: gem install bundler -v 1.11.2
9
+ script:
10
+ - bundle install
11
+ - bundle exec rake
12
+ addons:
13
+ postgresql: "9.4"
14
+ env:
15
+ global:
16
+ - POSGRA_TEST_USER=postgres
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in posgra.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Genki Sugawara
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,99 @@
1
+ # Posgra
2
+
3
+ Posgra is a tool to manage PostgreSQL roles/permissions.
4
+
5
+ It defines the state of PostgreSQL roles/permissions using Ruby DSL, and updates roles/permissions according to DSL.
6
+
7
+ [![Build Status](https://travis-ci.org/winebarrel/posgra.svg?branch=master)](https://travis-ci.org/winebarrel/posgra)
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'posgra'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install posgra
24
+
25
+ ## Usage
26
+
27
+ ```sh
28
+ $ posgra help
29
+ Commands:
30
+ posgra grant SUBCOMMAND # Manage grants
31
+ posgra help [COMMAND] # Describe available commands or one specific command
32
+ posgra role SUBCOMMAND # Manage roles
33
+
34
+ Options:
35
+ -h, [--host=HOST]
36
+ # Default: localhost
37
+ -p, [--port=N]
38
+ # Default: 5432
39
+ -d, [--dbname=DBNAME]
40
+ # Default: postgres
41
+ -U, [--user=USER]
42
+ -P, [--password=PASSWORD]
43
+ [--account-output=ACCOUNT-OUTPUT]
44
+ # Default: account.csv
45
+ [--color], [--no-color]
46
+ # Default: true
47
+ [--debug], [--no-debug]
48
+ ```
49
+
50
+ ```sh
51
+ posgra role export pg_roles.rb
52
+ vi pg_roles.rb
53
+ posgra role apply --dry-run pg_roles.rb
54
+ posgra role apply pg_roles.rb
55
+ ```
56
+
57
+ ```sh
58
+ posgra grant export pg_grants.rb
59
+ vi pg_grants.rb
60
+ posgra grant apply --dry-run pg_grants.rb
61
+ posgra grant apply pg_grants.rb
62
+ ```
63
+
64
+ ## DSL Example
65
+
66
+ ### Role
67
+
68
+ ```ruby
69
+ user "alice"
70
+
71
+ group "staff" do
72
+ user "bob"
73
+ end
74
+ ```
75
+
76
+ ### Grant
77
+
78
+ ```ruby
79
+ role "bob" do
80
+ schema "main" do
81
+ on "microposts" do
82
+ grant "DELETE", grantable: true
83
+ grant "INSERT"
84
+ grant "REFERENCES"
85
+ grant "SELECT"
86
+ grant "TRIGGER"
87
+ grant "TRUNCATE"
88
+ grant "UPDATE"
89
+ end
90
+ on "microposts_id_seq" do
91
+ grant "SELECT"
92
+ grant "UPDATE"
93
+ end
94
+ on /^user/ do
95
+ grant "SELECT"
96
+ end
97
+ end
98
+ end
99
+ ```
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "posgra"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path('../../lib', __FILE__)
3
+ require 'posgra'
4
+
5
+ debug = ARGV.any? {|i| i == '--debug' }
6
+
7
+ begin
8
+ Posgra::CLI::App.start(ARGV)
9
+ rescue => e
10
+ if debug
11
+ raise e
12
+ else
13
+ $stderr.puts "ERROR: #{e}".red
14
+ end
15
+ end
@@ -0,0 +1,33 @@
1
+ require 'hashie'
2
+ require 'logger'
3
+ require 'pg'
4
+ require 'singleton'
5
+ require 'term/ansicolor'
6
+ require 'thor'
7
+
8
+ module Posgra; end
9
+
10
+ require 'posgra/ext/string_ext'
11
+ require 'posgra/logger'
12
+ require 'posgra/template'
13
+ require 'posgra/utils'
14
+ require 'posgra/cli'
15
+ require 'posgra/cli'
16
+ require 'posgra/cli/helper'
17
+ require 'posgra/cli/grant'
18
+ require 'posgra/cli/role'
19
+ require 'posgra/cli/app'
20
+ require 'posgra/client'
21
+ require 'posgra/driver'
22
+ require 'posgra/dsl'
23
+ require 'posgra/dsl/grants'
24
+ require 'posgra/dsl/grants/role'
25
+ require 'posgra/dsl/grants/role/schema'
26
+ require 'posgra/dsl/grants/role/schema/on'
27
+ require 'posgra/dsl/roles'
28
+ require 'posgra/dsl/roles/group'
29
+ require 'posgra/dsl/converter'
30
+ require 'posgra/exporter'
31
+ require 'posgra/identifier'
32
+ require 'posgra/identifier/auto'
33
+ require 'posgra/version'
@@ -0,0 +1,6 @@
1
+ module Posgra::CLI
2
+ MAGIC_COMMENT = <<-EOS
3
+ # -*- mode: ruby -*-
4
+ # vi: set ft=ruby :
5
+ EOS
6
+ end
@@ -0,0 +1,16 @@
1
+ class Posgra::CLI::App < Thor
2
+ class_option :host, :default => 'localhost', :aliases => '-h'
3
+ class_option :port, :type => :numeric, :default => 5432, :aliases => '-p'
4
+ class_option :dbname, :default => 'postgres', :aliases => '-d'
5
+ class_option :user, :aliases => '-U'
6
+ class_option :password, :aliases => '-P'
7
+ class_option :'account-output', :default => 'account.csv'
8
+ class_option :color, :type => :boolean, :default => true
9
+ class_option :debug, :type => :boolean, :default => false
10
+
11
+ desc 'role SUBCOMMAND', 'Manage roles'
12
+ subcommand :role, Posgra::CLI::Role
13
+
14
+ desc 'grant SUBCOMMAND', 'Manage grants'
15
+ subcommand :grant, Posgra::CLI::Grant
16
+ end
@@ -0,0 +1,68 @@
1
+ class Posgra::CLI::Grant < Thor
2
+ include Posgra::CLI::Helper
3
+ include Posgra::Logger::Helper
4
+
5
+ DEFAULT_FILENAME = 'pg_grants.rb'
6
+
7
+ class_option :'include-schema'
8
+ class_option :'exclude-schema'
9
+ class_option :'include-role'
10
+ class_option :'exclude-role'
11
+
12
+ desc 'apply FILE', 'Apply grants'
13
+ option :'dry-run', :type => :boolean, :default => false
14
+ def apply(file)
15
+ check_fileanem(file)
16
+ updated = client.apply_grants(file)
17
+
18
+ unless updated
19
+ Posgra::Logger.instance.info('No change'.intense_blue)
20
+ end
21
+ end
22
+
23
+ desc 'export [FILE]', 'Export grants'
24
+ option :split, :type => :boolean, :default => false
25
+ def export(file = nil)
26
+ check_fileanem(file)
27
+ dsl = client.export_grants
28
+
29
+ if options[:split]
30
+ file = DEFAULT_FILENAME unless file
31
+
32
+ log(:info, 'Export Grants')
33
+ requires = []
34
+
35
+ dsl.each do |user, content|
36
+ grant_file = "#{user}.rb"
37
+ requires << grant_file
38
+ log(:info, " write `#{grant_file}`")
39
+
40
+ open(grant_file, 'wb') do |f|
41
+ f.puts Posgra::CLI::MAGIC_COMMENT
42
+ f.puts content
43
+ end
44
+ end
45
+
46
+ log(:info, " write `#{file}`")
47
+
48
+ open(file, 'wb') do |f|
49
+ f.puts Posgra::CLI::MAGIC_COMMENT
50
+
51
+ requires.each do |grant_file|
52
+ f.puts "require '#{File.basename grant_file}'"
53
+ end
54
+ end
55
+ else
56
+ if file.nil? or file == '-'
57
+ puts dsl
58
+ else
59
+ log(:info, "Export Grants to `#{file}`")
60
+
61
+ open(file, 'wb') do |f|
62
+ f.puts Posgra::CLI::MAGIC_COMMENT
63
+ f.puts dsl
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,37 @@
1
+ module Posgra::CLI::Helper
2
+ REGEXP_OPTIONS = [
3
+ :include_schema,
4
+ :exclude_schema,
5
+ :include_role,
6
+ :exclude_role,
7
+ ]
8
+
9
+ def check_fileanem(file)
10
+ if file =~ /\A-.+/
11
+ raise "Invalid failname: #{file}"
12
+ end
13
+ end
14
+
15
+ def client
16
+ client_options = {}
17
+ String.colorize = options[:color]
18
+ Posgra::Logger.instance.set_debug(options[:debug])
19
+
20
+ options.each do |key, value|
21
+ if key.to_s =~ /-/
22
+ key = key.to_s.gsub('-', '_')
23
+ end
24
+
25
+ client_options[key.to_sym] = value if value
26
+ end
27
+
28
+ REGEXP_OPTIONS.each do |key|
29
+ if client_options[key]
30
+ client_options[key] = Regexp.new(client_options[key])
31
+ end
32
+ end
33
+
34
+ client_options[:identifier] = Posgra::Identifier::Auto.new(options['account-output'])
35
+ Posgra::Client.new(client_options)
36
+ end
37
+ end
@@ -0,0 +1,35 @@
1
+ class Posgra::CLI::Role < Thor
2
+ include Posgra::CLI::Helper
3
+ include Posgra::Logger::Helper
4
+
5
+ class_option :'include-role'
6
+ class_option :'exclude-role'
7
+
8
+ desc 'apply FILE', 'Apply roles'
9
+ option :'dry-run', :type => :boolean, :default => false
10
+ def apply(file)
11
+ check_fileanem(file)
12
+ updated = client.apply_roles(file)
13
+
14
+ unless updated
15
+ Posgra::Logger.instance.info('No change'.intense_blue)
16
+ end
17
+ end
18
+
19
+ desc 'export [FILE]', 'Export roles'
20
+ def export(file = nil)
21
+ check_fileanem(file)
22
+ dsl = client.export_roles
23
+
24
+ if file.nil? or file == '-'
25
+ puts dsl
26
+ else
27
+ log(:info, "Export Roles to `#{file}`")
28
+
29
+ open(file, 'wb') do |f|
30
+ f.puts Posgra::CLI::MAGIC_COMMENT
31
+ f.puts dsl
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,247 @@
1
+ class Posgra::Client
2
+ DEFAULT_EXCLUDE_SCHEMA = /\A(?:pg_.*|information_schema)\z/
3
+ DEFAULT_EXCLUDE_ROLE = /\A\z/
4
+
5
+ def initialize(options = {})
6
+ if options[:exclude_schema]
7
+ options[:exclude_schema] = Regexp.union(
8
+ options[:exclude_schema],
9
+ DEFAULT_EXCLUDE_SCHEMA
10
+ )
11
+ else
12
+ options[:exclude_schema] = DEFAULT_EXCLUDE_SCHEMA
13
+ end
14
+
15
+ if options[:exclude_role]
16
+ options[:exclude_role] = Regexp.union(
17
+ options[:exclude_role],
18
+ DEFAULT_EXCLUDE_SCHEMA
19
+ )
20
+ else
21
+ options[:exclude_role] = DEFAULT_EXCLUDE_ROLE
22
+ end
23
+
24
+ @options = options
25
+ @client = connect(options)
26
+ @driver = Posgra::Driver.new(@client, options)
27
+ end
28
+
29
+ def export_roles(options = {})
30
+ options = @options.merge(options)
31
+ exported = Posgra::Exporter.export_roles(@driver, options)
32
+ Posgra::DSL.convert_roles(exported, options)
33
+ end
34
+
35
+ def export_grants(options = {})
36
+ options = @options.merge(options)
37
+ exported = Posgra::Exporter.export_grants(@driver, options)
38
+
39
+ if options[:split]
40
+ dsl_h = Hash.new {|hash, key| hash[key] = {} }
41
+
42
+
43
+ exported.each do |role, schemas|
44
+ dsl = Posgra::DSL.convert_grants({role => schemas}, options)
45
+ dsl_h[role] = dsl
46
+ end
47
+
48
+ dsl_h
49
+ else
50
+ Posgra::DSL.convert_grants(exported, options)
51
+ end
52
+ end
53
+
54
+ def apply_roles(file, options = {})
55
+ options = @options.merge(options)
56
+ walk_for_roles(file, options)
57
+ end
58
+
59
+ def apply_grants(file, options = {})
60
+ options = @options.merge(options)
61
+ walk_for_grants(file, options)
62
+ end
63
+
64
+ def close
65
+ @client.close
66
+ end
67
+
68
+ private
69
+
70
+ def walk_for_roles(file, options)
71
+ expected = load_file(file, :parse_roles, options)
72
+ actual = Posgra::Exporter.export_roles(@driver, options)
73
+
74
+ expected_users_by_group = expected.fetch(:users_by_group)
75
+ actual_users_by_group = actual.fetch(:users_by_group)
76
+ expected_users = (expected_users_by_group.values.flatten + expected.fetch(:users)).uniq
77
+ actual_users = actual.fetch(:users)
78
+
79
+ updated = pre_walk_groups(expected_users_by_group, actual_users_by_group)
80
+ updated = walk_users(expected_users, actual_users) || updated
81
+ walk_groups(expected_users_by_group, actual_users_by_group, expected_users) || updated
82
+ end
83
+
84
+ def walk_for_grants(file, options)
85
+ expected = load_file(file, :parse_grants, options)
86
+ actual = Posgra::Exporter.export_grants(@driver, options)
87
+ walk_roles(expected, actual)
88
+ end
89
+
90
+ def walk_users(expected, actual)
91
+ updated = false
92
+
93
+ (expected - actual).each do |user|
94
+ updated = @driver.create_user(user) || updated
95
+ end
96
+
97
+ (actual - expected).each do |user|
98
+ updated = @driver.drop_user(user) || updated
99
+ end
100
+
101
+ updated
102
+ end
103
+
104
+ def pre_walk_groups(expected, actual)
105
+ updated = false
106
+
107
+ actual.reject {|group, _|
108
+ expected.has_key?(group)
109
+ }.each {|group, _|
110
+ updated = @driver.drop_group(group) || updated
111
+ }
112
+
113
+ updated
114
+ end
115
+
116
+ def walk_groups(expected, actual, expected_users)
117
+ updated = false
118
+
119
+ expected.each do |expected_group, expected_users|
120
+ actual_users = actual.delete(expected_group)
121
+
122
+ unless actual_users
123
+ updated = @driver.create_group(expected_group) || updated
124
+ actual_users = []
125
+ end
126
+
127
+ (expected_users - actual_users).each do |user|
128
+ updated = @driver.add_user_to_group(user, expected_group) || updated
129
+ end
130
+
131
+ (actual_users - expected_users).each do |user|
132
+ if expected_users.include?(user)
133
+ updated = @driver.drop_user_from_group(user, expected_group) || updated
134
+ end
135
+ end
136
+ end
137
+
138
+ updated
139
+ end
140
+
141
+ def walk_roles(expected, actual)
142
+ updated = false
143
+
144
+ expected.each do |expected_role, expected_schemas|
145
+ actual_schemas = actual.delete(expected_role) || {}
146
+ updated = walk_schemas(expected_schemas, actual_schemas, expected_role) || updated
147
+ end
148
+
149
+ actual.each do |actual_role, actual_schemas|
150
+ actual_schemas.each do |schema, _|
151
+ updated = @driver.revoke_all_on_schema(actual_role, schema) || updated
152
+ end
153
+ end
154
+
155
+ updated
156
+ end
157
+
158
+ def walk_schemas(expected, actual, role)
159
+ updated = false
160
+
161
+ expected.each do |expected_schema, expected_objects|
162
+ actual_objects = actual.delete(expected_schema) || {}
163
+ updated = walk_objects(expected_objects, actual_objects, role, expected_schema) || updated
164
+ end
165
+
166
+ actual.each do |actual_schema, _|
167
+ updated = @driver.revoke_all_on_schema(role, actual_schema) || updated
168
+ end
169
+
170
+ updated
171
+ end
172
+
173
+ def walk_objects(expected, actual, role, schema)
174
+ updated = false
175
+
176
+ expected.keys.each do |expected_object|
177
+ if expected_object.is_a?(Regexp)
178
+ expected_grants = expected.delete(expected_object)
179
+
180
+ @driver.describe_objects(schema).each do |object|
181
+ if object =~ expected_object
182
+ expected[object] = expected_grants.dup
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ expected.each do |expected_object, expected_grants|
189
+ actual_grants = actual.delete(expected_object) || {}
190
+ updated = walk_grants(expected_grants, actual_grants, role, schema, expected_object) || updated
191
+ end
192
+
193
+ actual.each do |actual_object, _|
194
+ updated = @driver.revoke_all_on_object(role, schema, actual_object) || updated
195
+ end
196
+
197
+ updated
198
+ end
199
+
200
+ def walk_grants(expected, actual, role, schema, object)
201
+ updated = false
202
+
203
+ expected.each do |expected_priv, expected_options|
204
+ actual_options = actual.delete(expected_priv)
205
+
206
+ if actual_options
207
+ if expected_options != actual_options
208
+ updated = @driver.update_grant_options(role, expected_priv, expected_options, schema, object) || updated
209
+ end
210
+ else
211
+ updated = @driver.grant(role, expected_priv, expected_options, schema, object) || updated
212
+ end
213
+ end
214
+
215
+ actual.each do |actual_priv, _|
216
+ updated = @driver.revoke(role, actual_priv, schema, object) || updated
217
+ end
218
+
219
+ updated
220
+ end
221
+
222
+ def load_file(file, method, options)
223
+ if file.kind_of?(String)
224
+ open(file) do |f|
225
+ Posgra::DSL.send(method, f.read, file, options)
226
+ end
227
+ elsif file.respond_to?(:read)
228
+ Posgra::DSL.send(method, file.read, file.path, options)
229
+ else
230
+ raise TypeError, "can't convert #{file} into File"
231
+ end
232
+ end
233
+
234
+ def connect(options)
235
+ connect_options = {}
236
+
237
+ PG::Connection::CONNECT_ARGUMENT_ORDER.each do |key|
238
+ value = options[key] || options[key.to_sym]
239
+
240
+ if value
241
+ connect_options[key] = value
242
+ end
243
+ end
244
+
245
+ PGconn.connect(connect_options)
246
+ end
247
+ end