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.
@@ -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