├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── docker-compose.yml ├── exe └── posgra ├── lib ├── posgra.rb └── posgra │ ├── cli.rb │ ├── cli │ ├── app.rb │ ├── database.rb │ ├── grant.rb │ ├── helper.rb │ └── role.rb │ ├── client.rb │ ├── driver.rb │ ├── dsl.rb │ ├── dsl │ ├── converter.rb │ ├── database.rb │ ├── database │ │ ├── role.rb │ │ └── role │ │ │ └── database.rb │ ├── grants.rb │ ├── grants │ │ ├── role.rb │ │ └── role │ │ │ ├── schema.rb │ │ │ └── schema │ │ │ └── on.rb │ ├── roles.rb │ └── roles │ │ └── group.rb │ ├── exporter.rb │ ├── ext │ └── string_ext.rb │ ├── identifier.rb │ ├── identifier │ └── auto.rb │ ├── logger.rb │ ├── template.rb │ ├── utils.rb │ └── version.rb ├── posgra.gemspec └── spec ├── posgra_databases_spec.rb ├── posgra_grants_expired_spec.rb ├── posgra_grants_include_exclude_spec.rb ├── posgra_grants_include_space_hyphen_spec.rb ├── posgra_grants_spec.rb ├── posgra_roles_create_spec.rb ├── posgra_roles_delete_spec.rb ├── posgra_roles_update_spec.rb ├── posgra_template_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 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 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: ruby 4 | rvm: 5 | - 2.0.0 6 | - 2.1.8 7 | - 2.2.4 8 | - 2.3.0 9 | before_install: 10 | - gem install bundler 11 | - sudo service postgresql stop 12 | before_script: 13 | - docker-compose up -d 14 | - function pg_ping { PGPASSWORD=password pg_isready -U postgres -h 127.0.0.1 > /dev/null 2> /dev/null; } 15 | - for i in {1..60}; do pg_ping && break; sleep 1; done 16 | env: 17 | global: 18 | - POSGRA_TEST_USER=postgres 19 | services: 20 | - docker 21 | addons: 22 | apt: 23 | packages: 24 | - postgresql-client-9.4 25 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in posgra.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 | [![Gem Version](https://badge.fury.io/rb/posgra.svg)](https://badge.fury.io/rb/posgra) 8 | [![Build Status](https://travis-ci.org/winebarrel/posgra.svg?branch=master)](https://travis-ci.org/winebarrel/posgra) 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | ```ruby 15 | gem 'posgra' 16 | ``` 17 | 18 | And then execute: 19 | 20 | $ bundle 21 | 22 | Or install it yourself as: 23 | 24 | $ gem install posgra 25 | 26 | ## Usage 27 | 28 | ```sh 29 | $ posgra help 30 | Commands: 31 | posgra database SUBCOMMAND # Manage database grants 32 | posgra grant SUBCOMMAND # Manage grants 33 | posgra help [COMMAND] # Describe available commands or one specific command 34 | posgra role SUBCOMMAND # Manage roles 35 | 36 | Options: 37 | -h, [--host=HOST] 38 | # Default: localhost 39 | -p, [--port=N] 40 | # Default: 5432 41 | -d, [--dbname=DBNAME] 42 | # Default: postgres 43 | -U, [--user=USER] 44 | -P, [--password=PASSWORD] 45 | [--account-output=ACCOUNT-OUTPUT] 46 | # Default: account.csv 47 | [--color], [--no-color] 48 | # Default: true 49 | [--debug], [--no-debug] 50 | ``` 51 | 52 | A default connection to a database can be established by setting the following environment variables: 53 | - `POSGRA_DB_HOST`: database host 54 | - `POSGRA_DB_PORT`: database port 55 | - `POSGRA_DB_DATABASE`: database database name 56 | - `POSGRA_DB_USER`: database user 57 | - `POSGRA_DB_PASSWORD`: database password 58 | 59 | ```sh 60 | posgra role export pg_roles.rb 61 | vi pg_roles.rb 62 | posgra role apply --dry-run pg_roles.rb 63 | posgra role apply pg_roles.rb 64 | ``` 65 | 66 | ```sh 67 | posgra grant export pg_grants.rb 68 | vi pg_grants.rb 69 | posgra grant apply --dry-run pg_grants.rb 70 | posgra grant apply pg_grants.rb 71 | ``` 72 | 73 | ```sh 74 | posgra database export pg_dbgrants.rb 75 | vi pg_dbgrants.rb 76 | posgra database apply --dry-run pg_dbgrants.rb 77 | posgra database apply pg_dbgrants.rb 78 | ``` 79 | 80 | ### for Redshift 81 | 82 | ```sh 83 | export POSGRA_DEFAULT_ACL_PRIVS=arwdRxt 84 | posgra grant export pg_grants.rb 85 | ``` 86 | 87 | ## DSL Example 88 | 89 | ### Role 90 | 91 | ```ruby 92 | user "alice" 93 | 94 | group "staff" do 95 | user "bob" 96 | end 97 | ``` 98 | 99 | ### Grant 100 | 101 | ```ruby 102 | role "bob" do 103 | schema "main" do 104 | on "microposts" do 105 | grant "DELETE", grantable: true 106 | grant "INSERT" 107 | grant "REFERENCES" 108 | grant "SELECT" 109 | grant "TRIGGER" 110 | grant "TRUNCATE" 111 | grant "UPDATE" 112 | end 113 | on "microposts_id_seq", expired: '2014/10/07' do 114 | grant "SELECT" 115 | grant "UPDATE" 116 | end 117 | on /^user/ do 118 | grant "SELECT" 119 | end 120 | end 121 | end 122 | ``` 123 | 124 | ### DB Grant 125 | 126 | ```ruby 127 | role "alice" do 128 | database "my_database" do 129 | grant "CONNECT", :grantable => true 130 | grant "CREATE" 131 | grant "TEMPORARY" 132 | end 133 | end 134 | 135 | role "bob" do 136 | database "my_database" do 137 | grant "CONNECT" 138 | grant "CREATE" 139 | grant "TEMPORARY" 140 | end 141 | end 142 | ``` 143 | 144 | ### Template 145 | 146 | ```ruby 147 | template "all grants" do 148 | on context.object do 149 | grant "DELETE", grantable: true 150 | grant "INSERT" 151 | grant "REFERENCES" 152 | grant "SELECT" 153 | grant "TRIGGER" 154 | grant "TRUNCATE" 155 | grant "UPDATE" 156 | end 157 | end 158 | 159 | template "grant select" do 160 | grant "SELECT" 161 | end 162 | 163 | role "bob" do 164 | schema "main" do 165 | include_template "all grants", object: "microposts" 166 | on "microposts_id_seq", expired: '2014/10/07' do 167 | grant "SELECT" 168 | grant "UPDATE" 169 | end 170 | on /^user/ do 171 | include_template "grant select" 172 | end 173 | end 174 | end 175 | ``` 176 | 177 | ## Running tests 178 | 179 | ```sh 180 | docker-compose up -d 181 | bundle install 182 | bundle exec rake 183 | ``` 184 | 185 | ### on OS X (docker-machine & VirtualBox) 186 | 187 | Port forwarding is required. 188 | 189 | ```sh 190 | VBoxManage controlvm default natpf1 "psql,tcp,127.0.0.1,5432,,5432" 191 | ``` 192 | 193 | ## Similar tools 194 | * [Codenize.tools](http://codenize.tools/) 195 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 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 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 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 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | postgres: 2 | image: "postgres:9.4" 3 | ports: 4 | - "5432:5432" 5 | environment: 6 | POSTGRES_PASSWORD: password 7 | -------------------------------------------------------------------------------- /exe/posgra: -------------------------------------------------------------------------------- 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 16 | -------------------------------------------------------------------------------- /lib/posgra.rb: -------------------------------------------------------------------------------- 1 | require 'hashie' 2 | require 'logger' 3 | require 'pg' 4 | require 'singleton' 5 | require 'term/ansicolor' 6 | require 'time' 7 | require 'thor' 8 | 9 | module Posgra; end 10 | 11 | require 'posgra/ext/string_ext' 12 | require 'posgra/logger' 13 | require 'posgra/template' 14 | require 'posgra/utils' 15 | require 'posgra/cli' 16 | require 'posgra/cli' 17 | require 'posgra/cli/helper' 18 | require 'posgra/cli/database' 19 | require 'posgra/cli/grant' 20 | require 'posgra/cli/role' 21 | require 'posgra/cli/app' 22 | require 'posgra/client' 23 | require 'posgra/driver' 24 | require 'posgra/dsl' 25 | require 'posgra/dsl/database' 26 | require 'posgra/dsl/database/role' 27 | require 'posgra/dsl/database/role/database' 28 | require 'posgra/dsl/grants' 29 | require 'posgra/dsl/grants/role' 30 | require 'posgra/dsl/grants/role/schema' 31 | require 'posgra/dsl/grants/role/schema/on' 32 | require 'posgra/dsl/roles' 33 | require 'posgra/dsl/roles/group' 34 | require 'posgra/dsl/converter' 35 | require 'posgra/exporter' 36 | require 'posgra/identifier' 37 | require 'posgra/identifier/auto' 38 | require 'posgra/version' 39 | -------------------------------------------------------------------------------- /lib/posgra/cli.rb: -------------------------------------------------------------------------------- 1 | module Posgra::CLI 2 | MAGIC_COMMENT = <<-EOS 3 | # -*- mode: ruby -*- 4 | # vi: set ft=ruby : 5 | EOS 6 | end 7 | -------------------------------------------------------------------------------- /lib/posgra/cli/app.rb: -------------------------------------------------------------------------------- 1 | class Posgra::CLI::App < Thor 2 | class_option :host, :default => ENV['POSGRA_DB_HOST'] || 'localhost', :aliases => '-h' 3 | class_option :port, :type => :numeric, :default => ENV['POSGRA_DB_PORT'] || 5432, :aliases => '-p' 4 | class_option :dbname, :default => ENV['POSGRA_DB_DATABASE'] || 'postgres', :aliases => '-d' 5 | class_option :user, :default => ENV['POSGRA_DB_USER'], :aliases => '-U' 6 | class_option :password, :default => ENV['POSGRA_DB_PASSWORD'], :aliases => '-P' 7 | class_option :'account-output', :default => ENV['POSGRA_ACCOUNT_FILEPATH'] || '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 | 17 | desc 'database SUBCOMMAND', 'Manage database grants' 18 | subcommand :database, Posgra::CLI::Database 19 | end 20 | -------------------------------------------------------------------------------- /lib/posgra/cli/database.rb: -------------------------------------------------------------------------------- 1 | class Posgra::CLI::Database < Thor 2 | include Posgra::CLI::Helper 3 | include Posgra::Logger::Helper 4 | 5 | DEFAULT_FILENAME = 'pg_dbgrants.rb' 6 | 7 | class_option :'include-role' 8 | class_option :'exclude-role' 9 | class_option :'include-database' 10 | class_option :'exclude-database' 11 | 12 | desc 'apply FILE', 'Apply database grants' 13 | option :'dry-run', :type => :boolean, :default => false 14 | def apply(file) 15 | check_fileanem(file) 16 | updated = client.apply_databases(file) 17 | 18 | unless updated 19 | Posgra::Logger.instance.info('No change'.intense_blue) 20 | end 21 | end 22 | 23 | desc 'export [FILE]', 'Export database grants' 24 | option :split, :type => :boolean, :default => false 25 | def export(file = nil) 26 | check_fileanem(file) 27 | dsl = client.export_databases 28 | 29 | if options[:split] 30 | file = DEFAULT_FILENAME unless file 31 | 32 | log(:info, 'Export Database Grants') 33 | requires = [] 34 | 35 | dsl.each do |user, content| 36 | user = user.gsub(/\s+/, '_') 37 | user = '_' if user.empty? 38 | grant_file = "#{user}.rb" 39 | requires << grant_file 40 | log(:info, " write `#{grant_file}`") 41 | 42 | open(grant_file, 'wb') do |f| 43 | f.puts Posgra::CLI::MAGIC_COMMENT 44 | f.puts content 45 | end 46 | end 47 | 48 | log(:info, " write `#{file}`") 49 | 50 | open(file, 'wb') do |f| 51 | f.puts Posgra::CLI::MAGIC_COMMENT 52 | 53 | requires.each do |grant_file| 54 | f.puts "require '#{File.basename grant_file}'" 55 | end 56 | end 57 | else 58 | if file.nil? or file == '-' 59 | puts dsl 60 | else 61 | log(:info, "Export Database Grants to `#{file}`") 62 | 63 | open(file, 'wb') do |f| 64 | f.puts Posgra::CLI::MAGIC_COMMENT 65 | f.puts dsl 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/posgra/cli/grant.rb: -------------------------------------------------------------------------------- 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 | class_option :'include-object' 12 | class_option :'exclude-object' 13 | 14 | desc 'apply FILE', 'Apply grants' 15 | option :'dry-run', :type => :boolean, :default => false 16 | def apply(file) 17 | check_fileanem(file) 18 | updated = client.apply_grants(file) 19 | 20 | unless updated 21 | Posgra::Logger.instance.info('No change'.intense_blue) 22 | end 23 | end 24 | 25 | desc 'export [FILE]', 'Export grants' 26 | option :split, :type => :boolean, :default => false 27 | def export(file = nil) 28 | check_fileanem(file) 29 | dsl = client.export_grants 30 | 31 | if options[:split] 32 | file = DEFAULT_FILENAME unless file 33 | 34 | log(:info, 'Export Grants') 35 | requires = [] 36 | 37 | dsl.each do |user, content| 38 | user = user.gsub(/\s+/, '_') 39 | user = '_' if user.empty? 40 | grant_file = "#{user}.rb" 41 | requires << grant_file 42 | log(:info, " write `#{grant_file}`") 43 | 44 | open(grant_file, 'wb') do |f| 45 | f.puts Posgra::CLI::MAGIC_COMMENT 46 | f.puts content 47 | end 48 | end 49 | 50 | log(:info, " write `#{file}`") 51 | 52 | open(file, 'wb') do |f| 53 | f.puts Posgra::CLI::MAGIC_COMMENT 54 | 55 | requires.each do |grant_file| 56 | f.puts "require '#{File.basename grant_file}'" 57 | end 58 | end 59 | else 60 | if file.nil? or file == '-' 61 | puts dsl 62 | else 63 | log(:info, "Export Grants to `#{file}`") 64 | 65 | open(file, 'wb') do |f| 66 | f.puts Posgra::CLI::MAGIC_COMMENT 67 | f.puts dsl 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/posgra/cli/helper.rb: -------------------------------------------------------------------------------- 1 | module Posgra::CLI::Helper 2 | REGEXP_OPTIONS = [ 3 | :include_schema, 4 | :exclude_schema, 5 | :include_role, 6 | :exclude_role, 7 | :include_object, 8 | :exclude_object, 9 | ] 10 | 11 | def check_fileanem(file) 12 | if file =~ /\A-.+/ 13 | raise "Invalid failname: #{file}" 14 | end 15 | end 16 | 17 | def client 18 | client_options = {} 19 | String.colorize = options[:color] 20 | Posgra::Logger.instance.set_debug(options[:debug]) 21 | 22 | options.each do |key, value| 23 | if key.to_s =~ /-/ 24 | key = key.to_s.gsub('-', '_') 25 | end 26 | 27 | client_options[key.to_sym] = value if value 28 | end 29 | 30 | REGEXP_OPTIONS.each do |key| 31 | if client_options[key] 32 | client_options[key] = Regexp.new(client_options[key]) 33 | end 34 | end 35 | 36 | client_options[:identifier] = Posgra::Identifier::Auto.new(options['account-output'], client_options) 37 | Posgra::Client.new(client_options) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/posgra/cli/role.rb: -------------------------------------------------------------------------------- 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 | class_option :'password-length' 8 | 9 | desc 'apply FILE', 'Apply roles' 10 | option :'dry-run', :type => :boolean, :default => false 11 | def apply(file) 12 | check_fileanem(file) 13 | updated = client.apply_roles(file) 14 | 15 | unless updated 16 | Posgra::Logger.instance.info('No change'.intense_blue) 17 | end 18 | end 19 | 20 | desc 'export [FILE]', 'Export roles' 21 | def export(file = nil) 22 | check_fileanem(file) 23 | dsl = client.export_roles 24 | 25 | if file.nil? or file == '-' 26 | puts dsl 27 | else 28 | log(:info, "Export Roles to `#{file}`") 29 | 30 | open(file, 'wb') do |f| 31 | f.puts Posgra::CLI::MAGIC_COMMENT 32 | f.puts dsl 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/posgra/client.rb: -------------------------------------------------------------------------------- 1 | class Posgra::Client 2 | include Posgra::Utils::Helper 3 | 4 | DEFAULT_EXCLUDE_SCHEMA = /\A(?:pg_.*|information_schema)\z/ 5 | DEFAULT_EXCLUDE_ROLE = /\A\z/ 6 | DEFAULT_EXCLUDE_DATABASE = /\A(?:template\d+|postgres)\z/ 7 | 8 | def initialize(options = {}) 9 | if options[:exclude_schema] 10 | options[:exclude_schema] = Regexp.union( 11 | options[:exclude_schema], 12 | DEFAULT_EXCLUDE_SCHEMA 13 | ) 14 | else 15 | options[:exclude_schema] = DEFAULT_EXCLUDE_SCHEMA 16 | end 17 | 18 | if options[:exclude_role] 19 | options[:exclude_role] = Regexp.union( 20 | options[:exclude_role], 21 | DEFAULT_EXCLUDE_ROLE 22 | ) 23 | else 24 | options[:exclude_role] = DEFAULT_EXCLUDE_ROLE 25 | end 26 | 27 | if options[:exclude_database] 28 | options[:exclude_database] = Regexp.union( 29 | options[:exclude_database], 30 | DEFAULT_EXCLUDE_DATABASE 31 | ) 32 | else 33 | options[:exclude_database] = DEFAULT_EXCLUDE_DATABASE 34 | end 35 | 36 | @options = options 37 | @client = connect(options) 38 | @driver = Posgra::Driver.new(@client, options) 39 | end 40 | 41 | def export_roles(options = {}) 42 | options = @options.merge(options) 43 | exported = Posgra::Exporter.export_roles(@driver, options) 44 | Posgra::DSL.convert_roles(exported, options) 45 | end 46 | 47 | def export_grants(options = {}) 48 | options = @options.merge(options) 49 | exported = Posgra::Exporter.export_grants(@driver, options) 50 | 51 | if options[:split] 52 | dsl_h = Hash.new {|hash, key| hash[key] = {} } 53 | 54 | exported.each do |role, schemas| 55 | dsl = Posgra::DSL.convert_grants({role => schemas}, options) 56 | dsl_h[role] = dsl 57 | end 58 | 59 | dsl_h 60 | else 61 | Posgra::DSL.convert_grants(exported, options) 62 | end 63 | end 64 | 65 | def export_databases(options = {}) 66 | options = @options.merge(options) 67 | exported = Posgra::Exporter.export_databases(@driver, options) 68 | 69 | if options[:split] 70 | dsl_h = Hash.new {|hash, key| hash[key] = {} } 71 | 72 | exported.each do |role, databases| 73 | dsl = Posgra::DSL.convert_databases({role => databases}, options) 74 | dsl_h[role] = dsl 75 | end 76 | 77 | dsl_h 78 | else 79 | Posgra::DSL.convert_databases(exported, options) 80 | end 81 | end 82 | 83 | def apply_roles(file, options = {}) 84 | options = @options.merge(options) 85 | walk_for_roles(file, options) 86 | end 87 | 88 | def apply_grants(file, options = {}) 89 | options = @options.merge(options) 90 | walk_for_grants(file, options) 91 | end 92 | 93 | def apply_databases(file, options = {}) 94 | options = @options.merge(options) 95 | walk_for_database_grants(file, options) 96 | end 97 | 98 | def close 99 | @client.close 100 | end 101 | 102 | private 103 | 104 | def walk_for_roles(file, options) 105 | expected = load_file(file, :parse_roles, options) 106 | actual = Posgra::Exporter.export_roles(@driver, options) 107 | 108 | expected_users_by_group = expected.fetch(:users_by_group) 109 | actual_users_by_group = actual.fetch(:users_by_group) 110 | expected_users = (expected_users_by_group.values.flatten + expected.fetch(:users)).uniq 111 | actual_users = (actual_users_by_group.values.flatten + actual.fetch(:users)).uniq 112 | 113 | updated = pre_walk_groups(expected_users_by_group, actual_users_by_group) 114 | updated = walk_users(expected_users, actual_users) || updated 115 | walk_groups(expected_users_by_group, actual_users_by_group, expected_users) || updated 116 | end 117 | 118 | def walk_for_grants(file, options) 119 | expected = load_file(file, :parse_grants, options) 120 | actual = Posgra::Exporter.export_grants(@driver, options) 121 | walk_roles(expected, actual) 122 | end 123 | 124 | def walk_for_database_grants(file, options) 125 | expected = load_file(file, :parse_databases, options) 126 | actual = Posgra::Exporter.export_databases(@driver, options) 127 | walk_database_roles(expected, actual) 128 | end 129 | 130 | def walk_users(expected, actual) 131 | updated = false 132 | 133 | (expected - actual).each do |user| 134 | updated = @driver.create_user(user) || updated 135 | end 136 | 137 | (actual - expected).each do |user| 138 | updated = @driver.drop_user(user) || updated 139 | end 140 | 141 | updated 142 | end 143 | 144 | def pre_walk_groups(expected, actual) 145 | updated = false 146 | 147 | actual.reject {|group, _| 148 | expected.has_key?(group) 149 | }.each {|group, actual_users| 150 | if matched?(group, @options[:include_role], @options[:exclude_role]) 151 | updated = @driver.drop_group(group) || updated 152 | else 153 | actual_users.each do |user| 154 | updated = @driver.drop_user_from_group(user, group) || updated 155 | end 156 | end 157 | } 158 | 159 | updated 160 | end 161 | 162 | def walk_groups(expected, actual, current_users) 163 | updated = false 164 | 165 | expected.each do |expected_group, expected_users| 166 | actual_users = actual.delete(expected_group) 167 | 168 | unless actual_users 169 | updated = @driver.create_group(expected_group) || updated 170 | actual_users = [] 171 | end 172 | 173 | (expected_users - actual_users).each do |user| 174 | updated = @driver.add_user_to_group(user, expected_group) || updated 175 | end 176 | 177 | (actual_users - expected_users).each do |user| 178 | if current_users.include?(user) 179 | updated = @driver.drop_user_from_group(user, expected_group) || updated 180 | end 181 | end 182 | end 183 | 184 | updated 185 | end 186 | 187 | def walk_roles(expected, actual) 188 | updated = false 189 | 190 | expected.each do |expected_role, expected_schemas| 191 | actual_schemas = actual.delete(expected_role) || {} 192 | updated = walk_schemas(expected_schemas, actual_schemas, expected_role) || updated 193 | end 194 | 195 | actual.each do |actual_role, actual_schemas| 196 | actual_schemas.each do |schema, _| 197 | updated = @driver.revoke_all_on_schema(actual_role, schema) || updated 198 | end 199 | end 200 | 201 | updated 202 | end 203 | 204 | def walk_schemas(expected, actual, role) 205 | updated = false 206 | 207 | expected.each do |expected_schema, expected_objects| 208 | actual_objects = actual.delete(expected_schema) || {} 209 | updated = walk_objects(expected_objects, actual_objects, role, expected_schema) || updated 210 | end 211 | 212 | actual.each do |actual_schema, _| 213 | updated = @driver.revoke_all_on_schema(role, actual_schema) || updated 214 | end 215 | 216 | updated 217 | end 218 | 219 | def walk_objects(expected, actual, role, schema) 220 | updated = false 221 | 222 | expected.keys.each do |expected_object| 223 | if expected_object.is_a?(Regexp) 224 | expected_grants = expected.delete(expected_object) 225 | 226 | @driver.describe_objects(schema).each do |object| 227 | if object =~ expected_object 228 | expected[object] = expected_grants.dup 229 | end 230 | end 231 | end 232 | end 233 | 234 | expected.each do |expected_object, expected_grants| 235 | actual_grants = actual.delete(expected_object) || {} 236 | updated = walk_grants(expected_grants, actual_grants, role, schema, expected_object) || updated 237 | end 238 | 239 | actual.each do |actual_object, _| 240 | updated = @driver.revoke_all_on_object(role, schema, actual_object) || updated 241 | end 242 | 243 | updated 244 | end 245 | 246 | def walk_grants(expected, actual, role, schema, object) 247 | updated = false 248 | 249 | expected.each do |expected_priv, expected_options| 250 | actual_options = actual.delete(expected_priv) 251 | 252 | if actual_options 253 | if expected_options != actual_options 254 | updated = @driver.update_grant_options(role, expected_priv, expected_options, schema, object) || updated 255 | end 256 | else 257 | updated = @driver.grant(role, expected_priv, expected_options, schema, object) || updated 258 | end 259 | end 260 | 261 | actual.each do |actual_priv, _| 262 | updated = @driver.revoke(role, actual_priv, schema, object) || updated 263 | end 264 | 265 | updated 266 | end 267 | 268 | def walk_database_roles(expected, actual) 269 | updated = false 270 | 271 | expected.each do |expected_role, expected_databases| 272 | actual_databases = actual.delete(expected_role) || {} 273 | updated = walk_databases(expected_databases, actual_databases, expected_role) || updated 274 | end 275 | 276 | actual.each do |actual_role, actual_databases| 277 | actual_databases.each do |database, _| 278 | updated = @driver.revoke_all_on_database(actual_role, database) || updated 279 | end 280 | end 281 | 282 | updated 283 | end 284 | 285 | def walk_databases(expected, actual, role) 286 | updated = false 287 | 288 | expected.each do |expected_database, expected_grants| 289 | actual_grants = actual.delete(expected_database) || {} 290 | updated = walk_database_grants(expected_grants, actual_grants, role, expected_database) || updated 291 | end 292 | 293 | actual.each do |actual_database, _| 294 | updated = @driver.revoke_all_on_database(role, actual_database) || updated 295 | end 296 | 297 | updated 298 | end 299 | 300 | def walk_database_grants(expected, actual, role, database) 301 | updated = false 302 | 303 | expected.each do |expected_priv, expected_options| 304 | actual_options = actual.delete(expected_priv) 305 | 306 | if actual_options 307 | if expected_options != actual_options 308 | updated = @driver.update_database_grant_options(role, expected_priv, expected_options, database) || updated 309 | end 310 | else 311 | updated = @driver.database_grant(role, expected_priv, expected_options, database) || updated 312 | end 313 | end 314 | 315 | actual.each do |actual_priv, _| 316 | updated = @driver.database_revoke(role, actual_priv, database) || updated 317 | end 318 | 319 | updated 320 | end 321 | 322 | def load_file(file, method, options) 323 | if file.kind_of?(String) 324 | open(file) do |f| 325 | Posgra::DSL.send(method, f.read, file, options) 326 | end 327 | elsif file.respond_to?(:read) 328 | Posgra::DSL.send(method, file.read, file.path, options) 329 | else 330 | raise TypeError, "can't convert #{file} into File" 331 | end 332 | end 333 | 334 | def connect(options) 335 | connect_options = {} 336 | 337 | PG::Connection::CONNECT_ARGUMENT_ORDER.each do |key| 338 | value = options[key] || options[key.to_sym] 339 | 340 | if value 341 | connect_options[key] = value 342 | end 343 | end 344 | 345 | PG::Connection.connect(connect_options) 346 | end 347 | end 348 | -------------------------------------------------------------------------------- /lib/posgra/driver.rb: -------------------------------------------------------------------------------- 1 | class Posgra::Driver 2 | include Posgra::Logger::Helper 3 | include Posgra::Utils::Helper 4 | 5 | DEFAULT_ACL_PRIVS = ENV['POSGRA_DEFAULT_ACL_PRIVS'] || 'arwdDxt' 6 | DEFAULT_ACL = "{%s=#{DEFAULT_ACL_PRIVS}/%s}" 7 | 8 | DEFAULT_DATABASE_ACL = "{%s=CTc/%s}" 9 | 10 | DEFAULT_ACL_BY_KIND = { 11 | 'S' => '{%s=rwU/%s}' 12 | } 13 | 14 | PRIVILEGE_TYPES = { 15 | 'a' => 'INSERT', 16 | 'r' => 'SELECT', 17 | 'w' => 'UPDATE', 18 | 'd' => 'DELETE', 19 | 'D' => 'TRUNCATE', 20 | 'x' => 'REFERENCES', 21 | 't' => 'TRIGGER', 22 | 'U' => 'USAGE', 23 | 'R' => 'RULE', 24 | 'X' => 'EXECUTE', 25 | 'C' => 'CREATE', 26 | 'c' => 'CONNECT', 27 | 'T' => 'TEMPORARY', 28 | } 29 | 30 | def initialize(client, options = {}) 31 | unless client.type_map_for_results.is_a?(PG::TypeMapAllStrings) 32 | raise 'PG::Connection#type_map_for_results must be PG::TypeMapAllStrings' 33 | end 34 | 35 | @client = client 36 | @options = options 37 | @identifier = options.fetch(:identifier) 38 | end 39 | 40 | def create_user(user) 41 | updated = false 42 | 43 | password = @identifier.identify(user) 44 | sql = "CREATE USER #{@client.escape_identifier(user)} PASSWORD #{@client.escape_literal(password)}" 45 | log(:info, sql, :color => :cyan) 46 | 47 | unless @options[:dry_run] 48 | exec(sql) 49 | updated = true 50 | end 51 | 52 | updated 53 | end 54 | 55 | def drop_user(user) 56 | updated = false 57 | 58 | sql = "DROP USER #{@client.escape_identifier(user)}" 59 | log(:info, sql, :color => :red) 60 | 61 | unless @options[:dry_run] 62 | exec(sql) 63 | updated = true 64 | end 65 | 66 | updated 67 | end 68 | 69 | def create_group(group) 70 | updated = false 71 | 72 | sql = "CREATE GROUP #{@client.escape_identifier(group)}" 73 | log(:info, sql, :color => :cyan) 74 | 75 | unless @options[:dry_run] 76 | exec(sql) 77 | updated = true 78 | end 79 | 80 | updated 81 | end 82 | 83 | def add_user_to_group(user, group) 84 | updated = false 85 | 86 | sql = "ALTER GROUP #{@client.escape_identifier(group)} ADD USER #{@client.escape_identifier(user)}" 87 | log(:info, sql, :color => :green) 88 | 89 | unless @options[:dry_run] 90 | exec(sql) 91 | updated = true 92 | end 93 | 94 | updated 95 | end 96 | 97 | def drop_user_from_group(user, group) 98 | updated = false 99 | 100 | sql = "ALTER GROUP #{@client.escape_identifier(group)} DROP USER #{@client.escape_identifier(user)}" 101 | log(:info, sql, :color => :cyan) 102 | 103 | unless @options[:dry_run] 104 | exec(sql) 105 | updated = true 106 | end 107 | 108 | updated 109 | end 110 | 111 | def drop_group(group) 112 | updated = false 113 | 114 | sql = "DROP GROUP #{@client.escape_identifier(group)}" 115 | log(:info, sql, :color => :red) 116 | 117 | unless @options[:dry_run] 118 | exec(sql) 119 | updated = true 120 | end 121 | 122 | updated 123 | end 124 | 125 | def revoke_all_on_schema(role, schema) 126 | updated = false 127 | 128 | describe_objects(schema).each do |object| 129 | updated = revoke_all_on_object(role, schema, object) || updated 130 | end 131 | 132 | updated 133 | end 134 | 135 | def revoke_all_on_object(role, schema, object) 136 | updated = false 137 | 138 | sql = "REVOKE ALL ON #{@client.escape_identifier(schema)}.#{@client.escape_identifier(object)} FROM #{@client.escape_identifier(role)}" 139 | log(:info, sql, :color => :green) 140 | 141 | unless @options[:dry_run] 142 | exec(sql) 143 | updated = true 144 | end 145 | 146 | updated 147 | end 148 | 149 | def revoke_all_on_database(role, database) 150 | sql = "REVOKE ALL ON DATABASE #{@client.escape_identifier(database)} FROM #{@client.escape_identifier(role)}" 151 | log(:info, sql, :color => :green) 152 | 153 | unless @options[:dry_run] 154 | exec(sql) 155 | updated = true 156 | end 157 | 158 | updated 159 | end 160 | 161 | def grant(role, priv, options, schema, object) 162 | updated = false 163 | 164 | sql = "GRANT #{priv} ON #{@client.escape_identifier(schema)}.#{@client.escape_identifier(object)} TO #{@client.escape_identifier(role)}" 165 | 166 | if options['is_grantable'] 167 | sql << ' WITH GRANT OPTION' 168 | end 169 | 170 | log(:info, sql, :color => :green) 171 | 172 | unless @options[:dry_run] 173 | exec(sql) 174 | updated = true 175 | end 176 | 177 | updated 178 | end 179 | 180 | def update_grant_options(role, priv, options, schema, object) 181 | updated = false 182 | 183 | if options.fetch('is_grantable') 184 | updated = grant_grant_option(role, priv, schema, object) 185 | else 186 | updated = roveke_grant_option(role, priv, schema, object) 187 | end 188 | 189 | updated 190 | end 191 | 192 | def grant_grant_option(role, priv, schema, object) 193 | updated = false 194 | 195 | sql = "GRANT #{priv} ON #{@client.escape_identifier(schema)}.#{@client.escape_identifier(object)} TO #{@client.escape_identifier(role)} WITH GRANT OPTION" 196 | log(:info, sql, :color => :green) 197 | 198 | unless @options[:dry_run] 199 | exec(sql) 200 | updated = true 201 | end 202 | 203 | updated 204 | end 205 | 206 | def roveke_grant_option(role, priv, schema, object) 207 | updated = false 208 | 209 | sql = "REVOKE GRANT OPTION FOR #{priv} ON #{@client.escape_identifier(schema)}.#{@client.escape_identifier(object)} FROM #{@client.escape_identifier(role)}" 210 | log(:info, sql, :color => :green) 211 | 212 | unless @options[:dry_run] 213 | exec(sql) 214 | updated = true 215 | end 216 | 217 | updated 218 | end 219 | 220 | def revoke(role, priv, schema, object) 221 | updated = false 222 | 223 | sql = "REVOKE #{priv} ON #{@client.escape_identifier(schema)}.#{@client.escape_identifier(object)} FROM #{@client.escape_identifier(role)}" 224 | log(:info, sql, :color => :green) 225 | 226 | unless @options[:dry_run] 227 | exec(sql) 228 | updated = true 229 | end 230 | 231 | updated 232 | end 233 | 234 | def database_grant(role, priv, options, database) 235 | updated = false 236 | 237 | sql = "GRANT #{priv} ON DATABASE #{@client.escape_identifier(database)} TO #{@client.escape_identifier(role)}" 238 | 239 | if options['is_grantable'] 240 | sql << ' WITH GRANT OPTION' 241 | end 242 | 243 | log(:info, sql, :color => :green) 244 | 245 | unless @options[:dry_run] 246 | exec(sql) 247 | updated = true 248 | end 249 | 250 | updated 251 | end 252 | 253 | def update_database_grant_options(role, priv, options, database) 254 | updated = false 255 | 256 | if options.fetch('is_grantable') 257 | updated = grant_database_grant_option(role, priv, database) 258 | else 259 | updated = roveke_database_grant_option(role, priv, database) 260 | end 261 | 262 | updated 263 | end 264 | 265 | def grant_database_grant_option(role, priv, database) 266 | updated = false 267 | 268 | sql = "GRANT #{priv} ON DATABASE #{@client.escape_identifier(database)} TO #{@client.escape_identifier(role)} WITH GRANT OPTION" 269 | log(:info, sql, :color => :green) 270 | 271 | unless @options[:dry_run] 272 | exec(sql) 273 | updated = true 274 | end 275 | 276 | updated 277 | end 278 | 279 | def roveke_database_grant_option(role, priv, database) 280 | updated = false 281 | 282 | sql = "REVOKE GRANT OPTION FOR #{priv} ON DATABASE #{@client.escape_identifier(database)} FROM #{@client.escape_identifier(role)}" 283 | log(:info, sql, :color => :green) 284 | 285 | unless @options[:dry_run] 286 | exec(sql) 287 | updated = true 288 | end 289 | 290 | updated 291 | end 292 | 293 | def database_revoke(role, priv, database) 294 | updated = false 295 | 296 | sql = "REVOKE #{priv} ON DATABASE #{@client.escape_identifier(database)} FROM #{@client.escape_identifier(role)}" 297 | log(:info, sql, :color => :green) 298 | 299 | unless @options[:dry_run] 300 | exec(sql) 301 | updated = true 302 | end 303 | 304 | updated 305 | end 306 | 307 | def describe_objects(schema) 308 | rs = exec <<-SQL 309 | SELECT 310 | pg_class.relname, 311 | pg_namespace.nspname 312 | FROM 313 | pg_class 314 | INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid 315 | WHERE 316 | pg_namespace.nspname = #{@client.escape_literal(schema)} 317 | AND pg_class.relkind NOT IN ('i') 318 | SQL 319 | 320 | objects = [] 321 | 322 | rs.each do |row| 323 | relname = row.fetch('relname') 324 | next unless matched?(relname, @options[:include_object], @options[:exclude_object]) 325 | objects << relname 326 | end 327 | 328 | objects 329 | end 330 | 331 | def describe_users 332 | rs = exec('SELECT * FROM pg_user') 333 | 334 | options_by_user = {} 335 | 336 | rs.each do |row| 337 | user = row.fetch('usename') 338 | next unless matched?(user, @options[:include_role], @options[:exclude_role]) 339 | options_by_user[user] = row.select {|_, v| v == 't' }.keys 340 | end 341 | 342 | options_by_user 343 | end 344 | 345 | def describe_groups 346 | rs = exec <<-SQL 347 | SELECT 348 | pg_group.groname, 349 | pg_user.usename 350 | FROM 351 | pg_group 352 | LEFT JOIN pg_user ON pg_user.usesysid = ANY(pg_group.grolist) 353 | SQL 354 | 355 | users_by_group = {} 356 | 357 | rs.each do |row| 358 | group = row.fetch('groname') 359 | user = row.fetch('usename') 360 | next unless [group, user].any? {|i| not i.nil? and matched?(i, @options[:include_role], @options[:exclude_role]) } 361 | users_by_group[group] ||= [] 362 | users_by_group[group] << user if user 363 | end 364 | 365 | users_by_group 366 | end 367 | 368 | def describe_grants 369 | rs = exec <<-SQL 370 | SELECT 371 | pg_class.relname, 372 | pg_namespace.nspname, 373 | pg_class.relacl, 374 | pg_user.usename, 375 | pg_class.relkind 376 | FROM 377 | pg_class 378 | INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid 379 | INNER JOIN pg_user ON pg_class.relowner = pg_user.usesysid 380 | WHERE 381 | pg_class.relkind NOT IN ('i') 382 | SQL 383 | 384 | grants_by_role = {} 385 | 386 | rs.each do |row| 387 | relname = row.fetch('relname') 388 | nspname = row.fetch('nspname') 389 | relacl = row.fetch('relacl') 390 | usename = row.fetch('usename') 391 | relkind = row.fetch('relkind') 392 | 393 | next unless matched?(relname, @options[:include_object], @options[:exclude_object]) 394 | next unless matched?(nspname, @options[:include_schema], @options[:exclude_schema]) 395 | 396 | parse_aclitems(relacl, usename, relkind).each do |aclitem| 397 | role = aclitem.fetch('grantee') 398 | privs = aclitem.fetch('privileges') 399 | next unless matched?(role, @options[:include_role], @options[:exclude_role]) 400 | grants_by_role[role] ||= {} 401 | grants_by_role[role][nspname] ||= {} 402 | grants_by_role[role][nspname][relname] = privs 403 | end 404 | end 405 | 406 | grants_by_role 407 | end 408 | 409 | def describe_databases 410 | rs = exec <<-SQL 411 | SELECT 412 | pg_database.datname, 413 | pg_database.datacl, 414 | pg_user.usename 415 | FROM 416 | pg_database 417 | INNER JOIN pg_user ON pg_database.datdba = pg_user.usesysid 418 | SQL 419 | 420 | database_grants_by_role = {} 421 | 422 | rs.each do |row| 423 | datname = row.fetch('datname') 424 | datacl = row.fetch('datacl') 425 | usename = row.fetch('usename') 426 | 427 | next unless matched?(datname, @options[:include_database], @options[:exclude_database]) 428 | 429 | parse_database_aclitems(datacl, usename).each do |aclitem| 430 | role = aclitem.fetch('grantee') 431 | privs = aclitem.fetch('privileges') 432 | next unless matched?(role, @options[:include_role], @options[:exclude_role]) 433 | database_grants_by_role[role] ||= {} 434 | database_grants_by_role[role][datname] = privs 435 | end 436 | end 437 | 438 | database_grants_by_role 439 | end 440 | 441 | private 442 | 443 | def parse_aclitems(aclitems, owner, relkind) 444 | aclitems_fmt = DEFAULT_ACL_BY_KIND.fetch(relkind, DEFAULT_ACL) 445 | aclitems ||= aclitems_fmt % [owner, owner] 446 | parse_aclitems0(aclitems) 447 | end 448 | 449 | def parse_database_aclitems(aclitems, owner) 450 | aclitems ||= DEFAULT_DATABASE_ACL % [owner, owner] 451 | parse_aclitems0(aclitems) 452 | end 453 | 454 | def parse_aclitems0(aclitems) 455 | aclitems = aclitems[1..-2].split(',') 456 | 457 | aclitems.map do |aclitem| 458 | aclitem = unquote_aclitem(aclitem) 459 | grantee, privileges_grantor = aclitem.split('=', 2) 460 | privileges, grantor = privileges_grantor.split('/', 2) 461 | grantee = unescape_aclname(grantee) 462 | grantor = unescape_aclname(grantor) 463 | 464 | { 465 | 'grantee' => grantee, 466 | 'privileges' => expand_privileges(privileges), 467 | 'grantor' => grantor, 468 | } 469 | end 470 | end 471 | 472 | def expand_privileges(privileges) 473 | options_by_privilege = {} 474 | 475 | privileges.scan(/([a-z])(\*)?/i).each do |privilege_type_char,is_grantable| 476 | privilege_type = PRIVILEGE_TYPES[privilege_type_char] 477 | 478 | unless privilege_type 479 | log(:warn, "Unknown privilege type: #{privilege_type_char}", :color => :yellow) 480 | next 481 | end 482 | 483 | options_by_privilege[privilege_type] = { 484 | 'is_grantable' => !!is_grantable, 485 | } 486 | end 487 | 488 | options_by_privilege 489 | end 490 | 491 | def exec(sql) 492 | log(:debug, sql) 493 | @client.exec(sql) 494 | end 495 | 496 | def unquote_aclitem(str) 497 | str.sub(/\A"/, '').sub(/"\z/, '').gsub('\\', '') 498 | end 499 | 500 | def unescape_aclname(str) 501 | # Fix for Redshift: "group " 502 | str.sub(/\A"/, '').sub(/"\z/, '').gsub('""', '"').sub(/\Agroup /, '') 503 | end 504 | end 505 | -------------------------------------------------------------------------------- /lib/posgra/dsl.rb: -------------------------------------------------------------------------------- 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.convert_databases(exported, options = {}) 11 | Posgra::DSL::Converter.convert_databases(exported, options) 12 | end 13 | 14 | def self.parse_roles(dsl, path, options = {}) 15 | Posgra::DSL::Roles.eval(dsl, path, options).result 16 | end 17 | 18 | def self.parse_grants(dsl, path, options = {}) 19 | Posgra::DSL::Grants.eval(dsl, path, options).result 20 | end 21 | 22 | def self.parse_databases(dsl, path, options = {}) 23 | Posgra::DSL::Database.eval(dsl, path, options).result 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/posgra/dsl/converter.rb: -------------------------------------------------------------------------------- 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 self.convert_databases(exported, options = {}) 11 | self.new(exported, options).convert_databases 12 | end 13 | 14 | def initialize(exported, options = {}) 15 | @exported = exported 16 | @options = options 17 | end 18 | 19 | def convert_roles 20 | users_by_group = @exported[:users_by_group] || {} 21 | users = @exported.fetch(:users, []) - users_by_group.values.flatten 22 | 23 | [ 24 | output_users(users), 25 | output_groups(users_by_group), 26 | ].join("\n").strip 27 | end 28 | 29 | def convert_grants 30 | grants_by_role = @exported || {} 31 | output_roles(grants_by_role).strip 32 | end 33 | 34 | def convert_databases 35 | database_grants_by_role = @exported || {} 36 | output_database_roles(database_grants_by_role).strip 37 | end 38 | 39 | private 40 | 41 | def output_users(users) 42 | users.sort.map {|user| 43 | "user #{user.inspect}" 44 | }.join("\n") + "\n" 45 | end 46 | 47 | def output_groups(users_by_group) 48 | users_by_group.sort_by {|g, _| g }.map {|group, users| 49 | output_group(group, users) 50 | }.join("\n") 51 | end 52 | 53 | def output_group(group, users) 54 | if users.empty? 55 | users = "# no users" 56 | else 57 | users = users.sort.map {|user| 58 | "user #{user.inspect}" 59 | }.join("\n ") 60 | end 61 | 62 | <<-EOS 63 | group #{group.inspect} do 64 | #{users} 65 | end 66 | EOS 67 | end 68 | 69 | def output_roles(grants_by_role) 70 | grants_by_role.sort_by {|r, _| r }.map {|role, grants_by_schema| 71 | output_role(role, grants_by_schema) 72 | }.join("\n") 73 | end 74 | 75 | def output_role(role, grants_by_schema) 76 | if grants_by_schema.empty? 77 | schemas = "# no schemas" 78 | else 79 | schemas = output_schemas(grants_by_schema) 80 | end 81 | 82 | <<-EOS 83 | role #{role.inspect} do 84 | #{schemas} 85 | end 86 | EOS 87 | end 88 | 89 | def output_schemas(grants_by_schema) 90 | grants_by_schema.sort_by {|s, _| s }.map {|schema, grants_by_object| 91 | output_schema(schema, grants_by_object).strip 92 | }.join("\n ") 93 | end 94 | 95 | def output_schema(schema, grants_by_object) 96 | if grants_by_object.empty? 97 | objects = "# no objects" 98 | else 99 | objects = output_objects(grants_by_object) 100 | end 101 | 102 | <<-EOS 103 | schema #{schema.inspect} do 104 | #{objects} 105 | end 106 | EOS 107 | end 108 | 109 | def output_objects(grants_by_object) 110 | grants_by_object.sort_by {|o, _| o }.map {|object, grants| 111 | output_object(object, grants).strip 112 | }.join("\n ") 113 | end 114 | 115 | def output_object(object, grants) 116 | if grants.empty? 117 | grants = "# no grants" 118 | else 119 | grants = output_grants(grants) 120 | end 121 | 122 | <<-EOS 123 | on #{object.inspect} do 124 | #{grants} 125 | end 126 | EOS 127 | end 128 | 129 | def output_grants(grants, indent = " ") 130 | grants.sort_by {|g| g.to_s }.map {|privilege_type, options| 131 | output_grant(privilege_type, options).strip 132 | }.join("\n#{indent}") 133 | end 134 | 135 | def output_grant(privilege_type, options) 136 | is_grantable = options.fetch('is_grantable') 137 | out = "grant #{privilege_type.inspect}" 138 | 139 | if is_grantable 140 | out << ", :grantable => #{is_grantable}" 141 | end 142 | 143 | out 144 | end 145 | 146 | def output_database_roles(database_grants_by_role) 147 | database_grants_by_role.sort_by {|r, _| r }.map {|role, grants_by_database| 148 | output_database_role(role, grants_by_database) 149 | }.join("\n") 150 | end 151 | 152 | def output_database_role(role, grants_by_database) 153 | if grants_by_database.empty? 154 | databases = "# no databases" 155 | else 156 | databases = output_databases(grants_by_database) 157 | end 158 | 159 | <<-EOS 160 | role #{role.inspect} do 161 | #{databases} 162 | end 163 | EOS 164 | end 165 | 166 | def output_databases(grants_by_database) 167 | grants_by_database.sort_by {|s, _| s }.map {|database, grants| 168 | output_database(database, grants).strip 169 | }.join("\n ") 170 | end 171 | 172 | def output_database(database, grants) 173 | if grants.empty? 174 | grants = "# no grants" 175 | else 176 | grants = output_grants(grants, ' ') 177 | end 178 | 179 | <<-EOS 180 | database #{database.inspect} do 181 | #{grants} 182 | end 183 | EOS 184 | end 185 | 186 | end 187 | -------------------------------------------------------------------------------- /lib/posgra/dsl/database.rb: -------------------------------------------------------------------------------- 1 | class Posgra::DSL::Database 2 | include Posgra::Logger::Helper 3 | include Posgra::TemplateHelper 4 | include Posgra::Utils::Helper 5 | 6 | def self.eval(dsl, path, options = {}) 7 | self.new(path, options) do 8 | eval(dsl, binding, path) 9 | end 10 | end 11 | 12 | attr_reader :result 13 | 14 | def initialize(path, options = {}, &block) 15 | @path = path 16 | @options = options 17 | @result = {} 18 | 19 | @context = Hashie::Mash.new( 20 | :path => path, 21 | :options => options, 22 | :templates => {}, 23 | ) 24 | 25 | instance_eval(&block) 26 | end 27 | 28 | private 29 | 30 | def template(name, &block) 31 | @context.templates[name.to_s] = block 32 | end 33 | 34 | def require(file) 35 | pgrantfile = (file =~ %r|\A/|) ? file : File.expand_path(File.join(File.dirname(@path), file)) 36 | 37 | if File.exist?(pgrantfile) 38 | instance_eval(File.read(pgrantfile), pgrantfile) 39 | elsif File.exist?(pgrantfile + '.rb') 40 | instance_eval(File.read(pgrantfile + '.rb'), pgrantfile + '.rb') 41 | else 42 | Kernel.require(file) 43 | end 44 | end 45 | 46 | def role(name, &block) 47 | name = name.to_s 48 | 49 | if matched?(name, @options[:include_role], @options[:exclude_role]) 50 | @result[name] = Posgra::DSL::Database::Role.new(@context, name, @options, &block).result 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/posgra/dsl/database/role.rb: -------------------------------------------------------------------------------- 1 | class Posgra::DSL::Database::Role 2 | include Posgra::Logger::Helper 3 | include Posgra::TemplateHelper 4 | include Posgra::Utils::Helper 5 | 6 | attr_reader :result 7 | 8 | def initialize(context, role, options, &block) 9 | @role = role 10 | @options = options 11 | @context = context.merge(:role => role) 12 | @result = {} 13 | instance_eval(&block) 14 | end 15 | 16 | def database(name, &block) 17 | name = name.to_s 18 | 19 | if matched?(name, @options[:include_database], @options[:exclude_database]) 20 | @result[name] = Posgra::DSL::Database::Role::Database.new(@context, name, @options, &block).result 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/posgra/dsl/database/role/database.rb: -------------------------------------------------------------------------------- 1 | class Posgra::DSL::Database::Role::Database 2 | include Posgra::Logger::Helper 3 | include Posgra::TemplateHelper 4 | 5 | attr_reader :result 6 | 7 | def initialize(context, database, options, &block) 8 | @database = database 9 | @options = options 10 | @context = context.merge(:database => database) 11 | @result = {} 12 | instance_eval(&block) 13 | end 14 | 15 | def grant(name, options = {}) 16 | name = name.to_s 17 | 18 | @result[name] = { 19 | 'is_grantable' => !!options[:grantable] 20 | } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/posgra/dsl/grants.rb: -------------------------------------------------------------------------------- 1 | class Posgra::DSL::Grants 2 | include Posgra::Logger::Helper 3 | include Posgra::TemplateHelper 4 | include Posgra::Utils::Helper 5 | 6 | def self.eval(dsl, path, options = {}) 7 | self.new(path, options) do 8 | eval(dsl, binding, path) 9 | end 10 | end 11 | 12 | attr_reader :result 13 | 14 | def initialize(path, options = {}, &block) 15 | @path = path 16 | @options = options 17 | @result = {} 18 | 19 | @context = Hashie::Mash.new( 20 | :path => path, 21 | :options => options, 22 | :templates => {}, 23 | ) 24 | 25 | instance_eval(&block) 26 | end 27 | 28 | private 29 | 30 | def template(name, &block) 31 | @context.templates[name.to_s] = block 32 | end 33 | 34 | def require(file) 35 | pgrantfile = (file =~ %r|\A/|) ? file : File.expand_path(File.join(File.dirname(@path), file)) 36 | 37 | if File.exist?(pgrantfile) 38 | instance_eval(File.read(pgrantfile), pgrantfile) 39 | elsif File.exist?(pgrantfile + '.rb') 40 | instance_eval(File.read(pgrantfile + '.rb'), pgrantfile + '.rb') 41 | else 42 | Kernel.require(file) 43 | end 44 | end 45 | 46 | def role(name, &block) 47 | name = name.to_s 48 | 49 | if matched?(name, @options[:include_role], @options[:exclude_role]) 50 | @result[name] = Posgra::DSL::Grants::Role.new(@context, name, @options, &block).result 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/posgra/dsl/grants/role.rb: -------------------------------------------------------------------------------- 1 | class Posgra::DSL::Grants::Role 2 | include Posgra::Logger::Helper 3 | include Posgra::TemplateHelper 4 | include Posgra::Utils::Helper 5 | 6 | attr_reader :result 7 | 8 | def initialize(context, role, options, &block) 9 | @role = role 10 | @options = options 11 | @context = context.merge(:role => role) 12 | @result = {} 13 | instance_eval(&block) 14 | end 15 | 16 | def schema(name, &block) 17 | name = name.to_s 18 | 19 | if matched?(name, @options[:include_schema], @options[:exclude_schema]) 20 | @result[name] = Posgra::DSL::Grants::Role::Schema.new(@context, name, @options, &block).result 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/posgra/dsl/grants/role/schema.rb: -------------------------------------------------------------------------------- 1 | class Posgra::DSL::Grants::Role::Schema 2 | include Posgra::Logger::Helper 3 | include Posgra::TemplateHelper 4 | include Posgra::Utils::Helper 5 | 6 | attr_reader :result 7 | 8 | def initialize(context, schema, options, &block) 9 | @schema = schema 10 | @options = options 11 | @context = context.merge(:schema => schema) 12 | @result = {} 13 | instance_eval(&block) 14 | end 15 | 16 | def on(name, options = {}, &block) 17 | unless name.is_a?(Regexp) 18 | name = name.to_s 19 | return unless matched?(name, @options[:include_object], @options[:exclude_object]) 20 | end 21 | 22 | if options[:expired] 23 | expired = Time.parse(options[:expired]) 24 | 25 | if Time.new >= expired 26 | log(:warn, "Privilege for `#{name}` has expired", :color => :yellow) 27 | return 28 | end 29 | end 30 | 31 | @result[name] = Posgra::DSL::Grants::Role::Schema::On.new(@context, name, @options, &block).result 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/posgra/dsl/grants/role/schema/on.rb: -------------------------------------------------------------------------------- 1 | class Posgra::DSL::Grants::Role::Schema::On 2 | include Posgra::Logger::Helper 3 | include Posgra::TemplateHelper 4 | 5 | attr_reader :result 6 | 7 | def initialize(context, object, options, &block) 8 | @object = object 9 | @options = options 10 | @context = context.merge(:object => object) 11 | @result = {} 12 | instance_eval(&block) 13 | end 14 | 15 | def grant(name, options = {}) 16 | name = name.to_s 17 | 18 | @result[name] = { 19 | 'is_grantable' => !!options[:grantable] 20 | } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/posgra/dsl/roles.rb: -------------------------------------------------------------------------------- 1 | class Posgra::DSL::Roles 2 | include Posgra::Logger::Helper 3 | include Posgra::TemplateHelper 4 | include Posgra::Utils::Helper 5 | 6 | def self.eval(dsl, path, options = {}) 7 | self.new(path, options) do 8 | eval(dsl, binding, path) 9 | end 10 | end 11 | 12 | def result 13 | @result[:users].uniq! 14 | 15 | group_users = @result[:users_by_group].flat_map do |group, users| 16 | if users.empty? 17 | [group, nil] 18 | else 19 | users.map {|u| [group, u] } 20 | end 21 | end 22 | 23 | new_users_by_group = {} 24 | 25 | group_users.each do |group, user| 26 | next unless [group, user].any? {|i| not i.nil? and matched?(i, @options[:include_role], @options[:exclude_role]) } 27 | new_users_by_group[group] ||= [] 28 | new_users_by_group[group] << user if user 29 | end 30 | 31 | new_users_by_group.values.each(&:uniq!) 32 | @result[:users_by_group] = new_users_by_group 33 | 34 | @result 35 | end 36 | 37 | def initialize(path, options = {}, &block) 38 | @path = path 39 | @options = options 40 | @result = { 41 | :users => [], 42 | :users_by_group => {}, 43 | } 44 | 45 | @context = Hashie::Mash.new( 46 | :path => path, 47 | :options => options, 48 | :templates => {}, 49 | ) 50 | 51 | instance_eval(&block) 52 | end 53 | 54 | private 55 | 56 | def template(name, &block) 57 | @context.templates[name.to_s] = block 58 | end 59 | 60 | def require(file) 61 | pgrantfile = (file =~ %r|\A/|) ? file : File.expand_path(File.join(File.dirname(@path), file)) 62 | 63 | if File.exist?(pgrantfile) 64 | instance_eval(File.read(pgrantfile), pgrantfile) 65 | elsif File.exist?(pgrantfile + '.rb') 66 | instance_eval(File.read(pgrantfile + '.rb'), pgrantfile + '.rb') 67 | else 68 | Kernel.require(file) 69 | end 70 | end 71 | 72 | def user(name, &block) 73 | name = name.to_s 74 | 75 | if matched?(name, @options[:include_role], @options[:exclude_role]) 76 | @result[:users] << name 77 | end 78 | end 79 | 80 | def group(name, &block) 81 | name = name.to_s 82 | @result[:users_by_group][name] = Posgra::DSL::Roles::Group.new(@context, name, @options, &block).result 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/posgra/dsl/roles/group.rb: -------------------------------------------------------------------------------- 1 | class Posgra::DSL::Roles::Group 2 | include Posgra::Logger::Helper 3 | include Posgra::TemplateHelper 4 | include Posgra::Utils::Helper 5 | 6 | attr_reader :result 7 | 8 | def initialize(context, group, options, &block) 9 | @group = group 10 | @options = options 11 | @context = context.merge(:group => group) 12 | @result = [] 13 | instance_eval(&block) if block 14 | end 15 | 16 | def user(name) 17 | name = name.to_s 18 | @result << name 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/posgra/exporter.rb: -------------------------------------------------------------------------------- 1 | class Posgra::Exporter 2 | def self.export_roles(driver, options = {}) 3 | self.new(driver, options).export_roles 4 | end 5 | 6 | def self.export_grants(driver, options = {}) 7 | self.new(driver, options).export_grants 8 | end 9 | 10 | def self.export_databases(driver, options = {}) 11 | self.new(driver, options).export_databases 12 | end 13 | 14 | def initialize(driver, options = {}) 15 | @driver = driver 16 | @options = options 17 | end 18 | 19 | def export_roles 20 | { 21 | :users => @driver.describe_users.keys, 22 | :users_by_group => @driver.describe_groups, 23 | } 24 | end 25 | 26 | def export_grants 27 | @driver.describe_grants 28 | end 29 | 30 | def export_databases 31 | @driver.describe_databases 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/posgra/ext/string_ext.rb: -------------------------------------------------------------------------------- 1 | class String 2 | @@colorize = false 3 | 4 | class << self 5 | def colorize=(value) 6 | @@colorize = value 7 | end 8 | 9 | def colorize 10 | @@colorize 11 | end 12 | end # of class methods 13 | 14 | Term::ANSIColor::Attribute.named_attributes.map do |attribute| 15 | class_eval(<<-EOS, __FILE__, __LINE__ + 1) 16 | def #{attribute.name} 17 | if @@colorize 18 | Term::ANSIColor.send(#{attribute.name.inspect}, self) 19 | else 20 | self 21 | end 22 | end 23 | EOS 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/posgra/identifier.rb: -------------------------------------------------------------------------------- 1 | module Posgra::Identifier 2 | end 3 | -------------------------------------------------------------------------------- /lib/posgra/identifier/auto.rb: -------------------------------------------------------------------------------- 1 | class Posgra::Identifier::Auto 2 | def initialize(output, options = {}) 3 | @output = output 4 | @options = options 5 | end 6 | 7 | def identify(user) 8 | password = mkpasswd((@options[:password_length] || 8).to_i) 9 | puts_password(user, password) 10 | password 11 | end 12 | 13 | private 14 | 15 | def mkpasswd(len) 16 | sources = [ 17 | (1..9).to_a, 18 | ('A'..'Z').to_a, 19 | ('a'..'z').to_a, 20 | ].shuffle 21 | 22 | passwd = [] 23 | 24 | len.times do |i| 25 | src = sources[i % sources.length] 26 | passwd << src.shuffle.shift 27 | end 28 | 29 | passwd.join 30 | end 31 | 32 | def puts_password(user, password) 33 | open_output do |f| 34 | f.puts("#{user},#{password}") 35 | end 36 | end 37 | 38 | def open_output 39 | return if @options[:dry_run] 40 | 41 | if @output == '-' 42 | yield($stdout) 43 | $stdout.flush 44 | else 45 | open(@output, 'a') do |f| 46 | yield(f) 47 | f.flush 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/posgra/logger.rb: -------------------------------------------------------------------------------- 1 | class Posgra::Logger < ::Logger 2 | include Singleton 3 | 4 | def initialize 5 | super($stdout) 6 | 7 | self.formatter = proc do |severity, datetime, progname, msg| 8 | "#{msg}\n" 9 | end 10 | 11 | self.level = Logger::INFO 12 | end 13 | 14 | def set_debug(value) 15 | self.level = value ? Logger::DEBUG : Logger::INFO 16 | end 17 | 18 | module Helper 19 | def log(level, message, options = {}) 20 | global_options = @options || {} 21 | message = "#{@object_identifier}: #{message}" if @object_identifier 22 | message = "[#{level.to_s.upcase}] #{message}" unless level == :info 23 | 24 | if global_options[:dry_run] and options[:dry_run] != false 25 | message << ' (dry-run)' if global_options[:dry_run] 26 | end 27 | 28 | message = message.send(options[:color]) if options[:color] 29 | logger = global_options[:logger] || Posgra::Logger.instance 30 | logger.send(level, message) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/posgra/template.rb: -------------------------------------------------------------------------------- 1 | module Posgra::TemplateHelper 2 | def include_template(template_name, context = {}) 3 | tmplt = @context.templates[template_name.to_s] 4 | 5 | unless tmplt 6 | raise "Template `#{template_name}` is not defined" 7 | end 8 | 9 | context_orig = @context 10 | @context = @context.merge(context) 11 | instance_eval(&tmplt) 12 | @context = context_orig 13 | end 14 | 15 | def context 16 | @context 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/posgra/utils.rb: -------------------------------------------------------------------------------- 1 | class Posgra::Utils 2 | module Helper 3 | def matched?(name, include_r, exclude_r) 4 | result = true 5 | 6 | if exclude_r 7 | result &&= name !~ exclude_r 8 | end 9 | 10 | if include_r 11 | result &&= name =~ include_r 12 | end 13 | 14 | result 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/posgra/version.rb: -------------------------------------------------------------------------------- 1 | module Posgra 2 | VERSION = "0.2.3.beta" 3 | end 4 | -------------------------------------------------------------------------------- /posgra.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'posgra/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'posgra' 8 | spec.version = Posgra::VERSION 9 | spec.authors = ['winebarrel'] 10 | spec.email = ['sgwr_dts@yahoo.co.jp'] 11 | 12 | spec.summary = %q{Posgra is a tool to manage PostgreSQL roles/permissions.} 13 | spec.description = %q{Posgra is a tool to manage PostgreSQL roles/permissions.} 14 | spec.homepage = 'https://github.com/winebarrel/posgra' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = 'exe' 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ['lib'] 21 | 22 | spec.add_dependency 'pg' 23 | spec.add_dependency 'term-ansicolor' 24 | spec.add_dependency 'hashie' 25 | spec.add_dependency 'thor' 26 | 27 | spec.add_development_dependency 'bundler' 28 | spec.add_development_dependency 'rake' 29 | spec.add_development_dependency 'rspec', '~> 3.0' 30 | spec.add_development_dependency 'coveralls' 31 | spec.add_development_dependency 'timecop' 32 | spec.add_development_dependency 'rspec-match_fuzzy' 33 | end 34 | -------------------------------------------------------------------------------- /spec/posgra_databases_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'databases' do 2 | include SpecHelper 3 | 4 | subject { export_databases } 5 | 6 | before do 7 | apply_roles do 8 | <<-RUBY 9 | group "engineer" do 10 | user "bob" 11 | end 12 | 13 | group "staff" do 14 | user "alice" 15 | end 16 | RUBY 17 | end 18 | 19 | apply_databases do 20 | <<-RUBY 21 | role "alice" do 22 | database "#{SpecHelper::DBNAME}" do 23 | grant "CONNECT", :grantable => true 24 | grant "CREATE" 25 | end 26 | end 27 | RUBY 28 | end 29 | end 30 | 31 | context 'nothing to do' do 32 | it do 33 | expect( 34 | apply_databases do 35 | <<-RUBY 36 | role "alice" do 37 | database "#{SpecHelper::DBNAME}" do 38 | grant "CONNECT", :grantable => true 39 | grant "CREATE" 40 | end 41 | end 42 | RUBY 43 | end 44 | ).to be_falsey 45 | 46 | is_expected.to match_fuzzy <<-RUBY 47 | role "alice" do 48 | database "#{SpecHelper::DBNAME}" do 49 | grant "CONNECT", :grantable => true 50 | grant "CREATE" 51 | end 52 | end 53 | RUBY 54 | end 55 | end 56 | context 'when grant' do 57 | it do 58 | expect( 59 | apply_databases do 60 | <<-RUBY 61 | role "alice" do 62 | database "#{SpecHelper::DBNAME}" do 63 | grant "CONNECT", :grantable => true 64 | grant "CREATE" 65 | grant "TEMPORARY" 66 | end 67 | end 68 | 69 | role "bob" do 70 | database "#{SpecHelper::DBNAME}" do 71 | grant "CONNECT" 72 | grant "CREATE" 73 | grant "TEMPORARY" 74 | end 75 | end 76 | RUBY 77 | end 78 | ).to be_truthy 79 | 80 | is_expected.to match_fuzzy <<-RUBY 81 | role "alice" do 82 | database "#{SpecHelper::DBNAME}" do 83 | grant "CONNECT", :grantable => true 84 | grant "CREATE" 85 | grant "TEMPORARY" 86 | end 87 | end 88 | 89 | role "bob" do 90 | database "#{SpecHelper::DBNAME}" do 91 | grant "CONNECT" 92 | grant "CREATE" 93 | grant "TEMPORARY" 94 | end 95 | end 96 | RUBY 97 | end 98 | end 99 | 100 | context 'when revoke' do 101 | it do 102 | expect( 103 | apply_databases do 104 | <<-RUBY 105 | role "alice" do 106 | database "#{SpecHelper::DBNAME}" do 107 | grant "CONNECT", :grantable => true 108 | end 109 | end 110 | 111 | role "bob" do 112 | database "#{SpecHelper::DBNAME}" do 113 | grant "CONNECT" 114 | grant "CREATE" 115 | grant "TEMPORARY" 116 | end 117 | end 118 | RUBY 119 | end 120 | ).to be_truthy 121 | 122 | is_expected.to match_fuzzy <<-RUBY 123 | role "alice" do 124 | database "#{SpecHelper::DBNAME}" do 125 | grant "CONNECT", :grantable => true 126 | end 127 | end 128 | 129 | role "bob" do 130 | database "#{SpecHelper::DBNAME}" do 131 | grant "CONNECT" 132 | grant "CREATE" 133 | grant "TEMPORARY" 134 | end 135 | end 136 | RUBY 137 | end 138 | end 139 | 140 | context 'when revoke all' do 141 | it do 142 | expect( 143 | apply_databases do 144 | <<-RUBY 145 | role "bob" do 146 | database "#{SpecHelper::DBNAME}" do 147 | grant "CONNECT" 148 | grant "CREATE" 149 | grant "TEMPORARY" 150 | end 151 | end 152 | RUBY 153 | end 154 | ).to be_truthy 155 | 156 | is_expected.to match_fuzzy <<-RUBY 157 | role "bob" do 158 | database "#{SpecHelper::DBNAME}" do 159 | grant "CONNECT" 160 | grant "CREATE" 161 | grant "TEMPORARY" 162 | end 163 | end 164 | RUBY 165 | end 166 | end 167 | 168 | context 'when grant grant_option' do 169 | it do 170 | expect( 171 | apply_databases do 172 | <<-RUBY 173 | role "alice" do 174 | database "#{SpecHelper::DBNAME}" do 175 | grant "CONNECT", :grantable => true 176 | grant "CREATE", :grantable => true 177 | end 178 | end 179 | RUBY 180 | end 181 | ).to be_truthy 182 | 183 | is_expected.to match_fuzzy <<-RUBY 184 | role "alice" do 185 | database "#{SpecHelper::DBNAME}" do 186 | grant "CONNECT", :grantable => true 187 | grant "CREATE", :grantable => true 188 | end 189 | end 190 | RUBY 191 | end 192 | end 193 | 194 | context 'when revoke grant_option' do 195 | it do 196 | expect( 197 | apply_databases do 198 | <<-RUBY 199 | role "alice" do 200 | database "#{SpecHelper::DBNAME}" do 201 | grant "CONNECT" 202 | grant "CREATE" 203 | end 204 | end 205 | RUBY 206 | end 207 | ).to be_truthy 208 | 209 | is_expected.to match_fuzzy <<-RUBY 210 | role "alice" do 211 | database "#{SpecHelper::DBNAME}" do 212 | grant "CONNECT" 213 | grant "CREATE" 214 | end 215 | end 216 | RUBY 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /spec/posgra_grants_expired_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'grants' do 2 | include SpecHelper 3 | 4 | let(:logger) { Logger.new('/dev/null') } 5 | 6 | subject { export_grants } 7 | 8 | before do 9 | apply_roles do 10 | <<-RUBY 11 | group "engineer" do 12 | user "bob" 13 | end 14 | 15 | group "staff" do 16 | user "alice" 17 | end 18 | RUBY 19 | end 20 | 21 | Timecop.freeze(Time.parse('2014/10/06')) do 22 | apply_grants do 23 | <<-RUBY 24 | role "bob" do 25 | schema "main" do 26 | on "microposts", expired: '2014/10/07' do 27 | grant "DELETE", grantable: true 28 | grant "INSERT" 29 | grant "REFERENCES" 30 | grant "SELECT" 31 | grant "TRIGGER" 32 | grant "TRUNCATE" 33 | grant "UPDATE" 34 | end 35 | on "microposts_id_seq" do 36 | grant "SELECT" 37 | grant "UPDATE" 38 | end 39 | end 40 | end 41 | RUBY 42 | end 43 | end 44 | end 45 | 46 | context 'when expired to do' do 47 | it do 48 | expect(logger).to receive(:warn).with('[WARN] Privilege for `microposts` has expired') 49 | 50 | Timecop.freeze(Time.parse('2014/10/08')) do 51 | expect( 52 | apply_grants(logger: logger) do 53 | <<-RUBY 54 | role "bob" do 55 | schema "main" do 56 | on "microposts", expired: '2014/10/07' do 57 | grant "DELETE", :grantable => true 58 | grant "INSERT" 59 | grant "REFERENCES" 60 | grant "SELECT" 61 | grant "TRIGGER" 62 | grant "TRUNCATE" 63 | grant "UPDATE" 64 | end 65 | on "microposts_id_seq" do 66 | grant "SELECT" 67 | grant "UPDATE" 68 | end 69 | end 70 | end 71 | RUBY 72 | end 73 | ).to be_truthy 74 | end 75 | 76 | is_expected.to match_fuzzy <<-RUBY 77 | role "bob" do 78 | schema "main" do 79 | on "microposts_id_seq" do 80 | grant "SELECT" 81 | grant "UPDATE" 82 | end 83 | end 84 | end 85 | RUBY 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/posgra_grants_include_exclude_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'grants' do 2 | include SpecHelper 3 | 4 | subject { export_grants } 5 | 6 | before do 7 | apply_roles do 8 | <<-RUBY 9 | group "engineer" do 10 | user "bob" 11 | end 12 | 13 | group "staff" do 14 | user "alice" 15 | end 16 | RUBY 17 | end 18 | 19 | apply_grants do 20 | <<-RUBY 21 | role "bob" do 22 | schema "main" do 23 | on "microposts" do 24 | grant "DELETE", grantable: true 25 | grant "INSERT" 26 | grant "REFERENCES" 27 | grant "SELECT" 28 | grant "TRIGGER" 29 | grant "TRUNCATE" 30 | grant "UPDATE" 31 | end 32 | on "microposts_id_seq" do 33 | grant "SELECT" 34 | grant "UPDATE" 35 | end 36 | end 37 | end 38 | RUBY 39 | end 40 | end 41 | 42 | context 'include object' do 43 | let(:include_object) { /^microposts$/ } 44 | 45 | it do 46 | expect( 47 | apply_grants(include_object: include_object) do 48 | <<-RUBY 49 | role "bob" do 50 | schema "main" do 51 | on "microposts" do 52 | grant "DELETE", :grantable => true 53 | grant "INSERT" 54 | grant "REFERENCES" 55 | grant "SELECT" 56 | grant "TRIGGER" 57 | grant "TRUNCATE" 58 | end 59 | on "microposts_id_seq" do 60 | grant "SELECT" 61 | end 62 | end 63 | end 64 | RUBY 65 | end 66 | ).to be_truthy 67 | 68 | is_expected.to match_fuzzy <<-RUBY 69 | role "bob" do 70 | schema "main" do 71 | on "microposts" do 72 | grant "DELETE", :grantable => true 73 | grant "INSERT" 74 | grant "REFERENCES" 75 | grant "SELECT" 76 | grant "TRIGGER" 77 | grant "TRUNCATE" 78 | end 79 | on "microposts_id_seq" do 80 | grant "SELECT" 81 | grant "UPDATE" 82 | end 83 | end 84 | end 85 | RUBY 86 | 87 | expect(export_grants(include_object: include_object)).to match_fuzzy <<-RUBY 88 | role "bob" do 89 | schema "main" do 90 | on "microposts" do 91 | grant "DELETE", :grantable => true 92 | grant "INSERT" 93 | grant "REFERENCES" 94 | grant "SELECT" 95 | grant "TRIGGER" 96 | grant "TRUNCATE" 97 | end 98 | end 99 | end 100 | RUBY 101 | end 102 | end 103 | 104 | context 'exclude object' do 105 | let(:exclude_object) { /^microposts_id_seq$/ } 106 | 107 | it do 108 | expect( 109 | apply_grants(exclude_object: exclude_object) do 110 | <<-RUBY 111 | role "bob" do 112 | schema "main" do 113 | on "microposts" do 114 | grant "DELETE", :grantable => true 115 | grant "INSERT" 116 | grant "REFERENCES" 117 | grant "SELECT" 118 | grant "TRIGGER" 119 | grant "TRUNCATE" 120 | end 121 | on "microposts_id_seq" do 122 | grant "SELECT" 123 | end 124 | end 125 | end 126 | RUBY 127 | end 128 | ).to be_truthy 129 | 130 | is_expected.to match_fuzzy <<-RUBY 131 | role "bob" do 132 | schema "main" do 133 | on "microposts" do 134 | grant "DELETE", :grantable => true 135 | grant "INSERT" 136 | grant "REFERENCES" 137 | grant "SELECT" 138 | grant "TRIGGER" 139 | grant "TRUNCATE" 140 | end 141 | on "microposts_id_seq" do 142 | grant "SELECT" 143 | grant "UPDATE" 144 | end 145 | end 146 | end 147 | RUBY 148 | 149 | expect(export_grants(exclude_object: exclude_object)).to match_fuzzy <<-RUBY 150 | role "bob" do 151 | schema "main" do 152 | on "microposts" do 153 | grant "DELETE", :grantable => true 154 | grant "INSERT" 155 | grant "REFERENCES" 156 | grant "SELECT" 157 | grant "TRIGGER" 158 | grant "TRUNCATE" 159 | end 160 | end 161 | end 162 | RUBY 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /spec/posgra_grants_include_space_hyphen_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'grants (include space/hyphen)' do 2 | include SpecHelper 3 | 4 | subject { export_grants } 5 | 6 | before do 7 | apply_roles do 8 | <<-RUBY 9 | group "engineer" do 10 | user "bob" 11 | end 12 | 13 | group "staff" do 14 | user "alice" 15 | end 16 | 17 | group "engineer-engineer" do 18 | user "bob-bob" 19 | end 20 | 21 | group "staff staff" do 22 | user "alice alice" 23 | end 24 | RUBY 25 | end 26 | 27 | apply_grants do 28 | <<-RUBY 29 | role "bob" do 30 | schema "main" do 31 | on "microposts" do 32 | grant "DELETE", :grantable => true 33 | grant "INSERT" 34 | grant "REFERENCES" 35 | grant "SELECT" 36 | grant "TRIGGER" 37 | grant "TRUNCATE" 38 | grant "UPDATE" 39 | end 40 | on "microposts_id_seq" do 41 | grant "SELECT" 42 | grant "UPDATE" 43 | end 44 | end 45 | end 46 | 47 | role "bob-bob" do 48 | schema "main" do 49 | on "microposts" do 50 | grant "DELETE", :grantable => true 51 | grant "INSERT" 52 | grant "REFERENCES" 53 | grant "SELECT" 54 | grant "TRIGGER" 55 | grant "TRUNCATE" 56 | grant "UPDATE" 57 | end 58 | on "microposts_id_seq" do 59 | grant "SELECT" 60 | grant "UPDATE" 61 | end 62 | end 63 | end 64 | 65 | role "staff staff" do 66 | schema "main" do 67 | on "microposts" do 68 | grant "DELETE", :grantable => true 69 | grant "INSERT" 70 | grant "REFERENCES" 71 | grant "SELECT" 72 | grant "TRIGGER" 73 | grant "TRUNCATE" 74 | grant "UPDATE" 75 | end 76 | on "microposts_id_seq" do 77 | grant "SELECT" 78 | grant "UPDATE" 79 | end 80 | end 81 | end 82 | RUBY 83 | end 84 | end 85 | 86 | context 'nothing to do' do 87 | it do 88 | expect( 89 | apply_grants do 90 | <<-RUBY 91 | role "bob" do 92 | schema "main" do 93 | on "microposts" do 94 | grant "DELETE", :grantable => true 95 | grant "INSERT" 96 | grant "REFERENCES" 97 | grant "SELECT" 98 | grant "TRIGGER" 99 | grant "TRUNCATE" 100 | grant "UPDATE" 101 | end 102 | on "microposts_id_seq" do 103 | grant "SELECT" 104 | grant "UPDATE" 105 | end 106 | end 107 | end 108 | 109 | role "bob-bob" do 110 | schema "main" do 111 | on "microposts" do 112 | grant "DELETE", :grantable => true 113 | grant "INSERT" 114 | grant "REFERENCES" 115 | grant "SELECT" 116 | grant "TRIGGER" 117 | grant "TRUNCATE" 118 | grant "UPDATE" 119 | end 120 | on "microposts_id_seq" do 121 | grant "SELECT" 122 | grant "UPDATE" 123 | end 124 | end 125 | end 126 | 127 | role "staff staff" do 128 | schema "main" do 129 | on "microposts" do 130 | grant "DELETE", :grantable => true 131 | grant "INSERT" 132 | grant "REFERENCES" 133 | grant "SELECT" 134 | grant "TRIGGER" 135 | grant "TRUNCATE" 136 | grant "UPDATE" 137 | end 138 | on "microposts_id_seq" do 139 | grant "SELECT" 140 | grant "UPDATE" 141 | end 142 | end 143 | end 144 | RUBY 145 | end 146 | ).to be_falsey 147 | 148 | is_expected.to match_fuzzy <<-RUBY 149 | role "bob" do 150 | schema "main" do 151 | on "microposts" do 152 | grant "DELETE", :grantable => true 153 | grant "INSERT" 154 | grant "REFERENCES" 155 | grant "SELECT" 156 | grant "TRIGGER" 157 | grant "TRUNCATE" 158 | grant "UPDATE" 159 | end 160 | on "microposts_id_seq" do 161 | grant "SELECT" 162 | grant "UPDATE" 163 | end 164 | end 165 | end 166 | 167 | role "bob-bob" do 168 | schema "main" do 169 | on "microposts" do 170 | grant "DELETE", :grantable => true 171 | grant "INSERT" 172 | grant "REFERENCES" 173 | grant "SELECT" 174 | grant "TRIGGER" 175 | grant "TRUNCATE" 176 | grant "UPDATE" 177 | end 178 | on "microposts_id_seq" do 179 | grant "SELECT" 180 | grant "UPDATE" 181 | end 182 | end 183 | end 184 | 185 | role "staff staff" do 186 | schema "main" do 187 | on "microposts" do 188 | grant "DELETE", :grantable => true 189 | grant "INSERT" 190 | grant "REFERENCES" 191 | grant "SELECT" 192 | grant "TRIGGER" 193 | grant "TRUNCATE" 194 | grant "UPDATE" 195 | end 196 | on "microposts_id_seq" do 197 | grant "SELECT" 198 | grant "UPDATE" 199 | end 200 | end 201 | end 202 | RUBY 203 | end 204 | end 205 | 206 | context 'when grant' do 207 | it do 208 | expect( 209 | apply_grants do 210 | <<-RUBY 211 | role "bob" do 212 | schema "main" do 213 | on "microposts" do 214 | grant "DELETE", :grantable => true 215 | grant "INSERT" 216 | grant "REFERENCES" 217 | grant "SELECT" 218 | grant "TRIGGER" 219 | grant "TRUNCATE" 220 | grant "UPDATE" 221 | end 222 | on "microposts_id_seq" do 223 | grant "SELECT" 224 | grant "UPDATE" 225 | end 226 | end 227 | end 228 | 229 | role "bob-bob" do 230 | schema "main" do 231 | on "microposts" do 232 | grant "DELETE", :grantable => true 233 | grant "INSERT" 234 | grant "REFERENCES" 235 | grant "SELECT" 236 | grant "TRIGGER" 237 | grant "TRUNCATE" 238 | grant "UPDATE" 239 | end 240 | on "microposts_id_seq" do 241 | grant "SELECT" 242 | grant "UPDATE" 243 | end 244 | end 245 | end 246 | 247 | role "staff staff" do 248 | schema "main" do 249 | on "microposts" do 250 | grant "DELETE", :grantable => true 251 | grant "INSERT" 252 | grant "REFERENCES" 253 | grant "SELECT" 254 | grant "TRIGGER" 255 | grant "TRUNCATE" 256 | grant "UPDATE" 257 | end 258 | on "microposts_id_seq" do 259 | grant "SELECT" 260 | grant "UPDATE" 261 | end 262 | end 263 | end 264 | 265 | role "alice alice" do 266 | schema "main" do 267 | on "microposts" do 268 | grant "DELETE", :grantable => true 269 | grant "INSERT" 270 | grant "REFERENCES" 271 | grant "SELECT" 272 | grant "TRIGGER" 273 | grant "TRUNCATE" 274 | grant "UPDATE" 275 | end 276 | on "microposts_id_seq" do 277 | grant "SELECT" 278 | grant "UPDATE" 279 | end 280 | end 281 | end 282 | 283 | role "engineer-engineer" do 284 | schema "main" do 285 | on "microposts" do 286 | grant "DELETE", :grantable => true 287 | grant "INSERT" 288 | grant "REFERENCES" 289 | grant "SELECT" 290 | grant "TRIGGER" 291 | grant "TRUNCATE" 292 | grant "UPDATE" 293 | end 294 | on "microposts_id_seq" do 295 | grant "SELECT" 296 | grant "UPDATE" 297 | end 298 | end 299 | end 300 | RUBY 301 | end 302 | ).to be_truthy 303 | 304 | is_expected.to match_fuzzy <<-RUBY 305 | role "alice alice" do 306 | schema "main" do 307 | on "microposts" do 308 | grant "DELETE", :grantable => true 309 | grant "INSERT" 310 | grant "REFERENCES" 311 | grant "SELECT" 312 | grant "TRIGGER" 313 | grant "TRUNCATE" 314 | grant "UPDATE" 315 | end 316 | on "microposts_id_seq" do 317 | grant "SELECT" 318 | grant "UPDATE" 319 | end 320 | end 321 | end 322 | 323 | role "bob" do 324 | schema "main" do 325 | on "microposts" do 326 | grant "DELETE", :grantable => true 327 | grant "INSERT" 328 | grant "REFERENCES" 329 | grant "SELECT" 330 | grant "TRIGGER" 331 | grant "TRUNCATE" 332 | grant "UPDATE" 333 | end 334 | on "microposts_id_seq" do 335 | grant "SELECT" 336 | grant "UPDATE" 337 | end 338 | end 339 | end 340 | 341 | role "bob-bob" do 342 | schema "main" do 343 | on "microposts" do 344 | grant "DELETE", :grantable => true 345 | grant "INSERT" 346 | grant "REFERENCES" 347 | grant "SELECT" 348 | grant "TRIGGER" 349 | grant "TRUNCATE" 350 | grant "UPDATE" 351 | end 352 | on "microposts_id_seq" do 353 | grant "SELECT" 354 | grant "UPDATE" 355 | end 356 | end 357 | end 358 | 359 | role "engineer-engineer" do 360 | schema "main" do 361 | on "microposts" do 362 | grant "DELETE", :grantable => true 363 | grant "INSERT" 364 | grant "REFERENCES" 365 | grant "SELECT" 366 | grant "TRIGGER" 367 | grant "TRUNCATE" 368 | grant "UPDATE" 369 | end 370 | on "microposts_id_seq" do 371 | grant "SELECT" 372 | grant "UPDATE" 373 | end 374 | end 375 | end 376 | 377 | role "staff staff" do 378 | schema "main" do 379 | on "microposts" do 380 | grant "DELETE", :grantable => true 381 | grant "INSERT" 382 | grant "REFERENCES" 383 | grant "SELECT" 384 | grant "TRIGGER" 385 | grant "TRUNCATE" 386 | grant "UPDATE" 387 | end 388 | on "microposts_id_seq" do 389 | grant "SELECT" 390 | grant "UPDATE" 391 | end 392 | end 393 | end 394 | RUBY 395 | end 396 | end 397 | 398 | context 'when revoke' do 399 | it do 400 | expect( 401 | apply_grants do 402 | <<-RUBY 403 | role "bob" do 404 | schema "main" do 405 | on "microposts" do 406 | grant "DELETE", :grantable => true 407 | grant "INSERT" 408 | grant "REFERENCES" 409 | grant "SELECT" 410 | grant "TRIGGER" 411 | grant "TRUNCATE" 412 | grant "UPDATE" 413 | end 414 | on "microposts_id_seq" do 415 | grant "SELECT" 416 | grant "UPDATE" 417 | end 418 | end 419 | end 420 | RUBY 421 | end 422 | ).to be_truthy 423 | 424 | is_expected.to match_fuzzy <<-RUBY 425 | role "bob" do 426 | schema "main" do 427 | on "microposts" do 428 | grant "DELETE", :grantable => true 429 | grant "INSERT" 430 | grant "REFERENCES" 431 | grant "SELECT" 432 | grant "TRIGGER" 433 | grant "TRUNCATE" 434 | grant "UPDATE" 435 | end 436 | on "microposts_id_seq" do 437 | grant "SELECT" 438 | grant "UPDATE" 439 | end 440 | end 441 | end 442 | RUBY 443 | end 444 | end 445 | end 446 | -------------------------------------------------------------------------------- /spec/posgra_grants_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'grants' do 2 | include SpecHelper 3 | 4 | subject { export_grants } 5 | 6 | before do 7 | apply_roles do 8 | <<-RUBY 9 | group "engineer" do 10 | user "bob" 11 | end 12 | 13 | group "staff" do 14 | user "alice" 15 | end 16 | RUBY 17 | end 18 | 19 | apply_grants do 20 | <<-RUBY 21 | role "bob" do 22 | schema "main" do 23 | on "microposts" do 24 | grant "DELETE", grantable: true 25 | grant "INSERT" 26 | grant "REFERENCES" 27 | grant "SELECT" 28 | grant "TRIGGER" 29 | grant "TRUNCATE" 30 | grant "UPDATE" 31 | end 32 | on "microposts_id_seq" do 33 | grant "SELECT" 34 | grant "UPDATE" 35 | end 36 | end 37 | end 38 | RUBY 39 | end 40 | end 41 | 42 | context 'nothing to do' do 43 | it do 44 | expect( 45 | apply_grants do 46 | <<-RUBY 47 | role "bob" do 48 | schema "main" do 49 | on "microposts" do 50 | grant "DELETE", :grantable => true 51 | grant "INSERT" 52 | grant "REFERENCES" 53 | grant "SELECT" 54 | grant "TRIGGER" 55 | grant "TRUNCATE" 56 | grant "UPDATE" 57 | end 58 | on "microposts_id_seq" do 59 | grant "SELECT" 60 | grant "UPDATE" 61 | end 62 | end 63 | end 64 | RUBY 65 | end 66 | ).to be_falsey 67 | 68 | is_expected.to match_fuzzy <<-RUBY 69 | role "bob" do 70 | schema "main" do 71 | on "microposts" do 72 | grant "DELETE", :grantable => true 73 | grant "INSERT" 74 | grant "REFERENCES" 75 | grant "SELECT" 76 | grant "TRIGGER" 77 | grant "TRUNCATE" 78 | grant "UPDATE" 79 | end 80 | on "microposts_id_seq" do 81 | grant "SELECT" 82 | grant "UPDATE" 83 | end 84 | end 85 | end 86 | RUBY 87 | end 88 | end 89 | 90 | context 'when grant' do 91 | it do 92 | expect( 93 | apply_grants do 94 | <<-RUBY 95 | role "alice" do 96 | schema "master" do 97 | on "users" do 98 | grant "DELETE" 99 | grant "INSERT" 100 | grant "REFERENCES" 101 | grant "SELECT" 102 | grant "TRIGGER" 103 | grant "TRUNCATE" 104 | grant "UPDATE" 105 | end 106 | on "users_id_seq" do 107 | grant "SELECT" 108 | grant "UPDATE" 109 | end 110 | end 111 | end 112 | 113 | role "bob" do 114 | schema "main" do 115 | on "microposts" do 116 | grant "DELETE", :grantable => true 117 | grant "INSERT" 118 | grant "REFERENCES" 119 | grant "SELECT" 120 | grant "TRIGGER" 121 | grant "TRUNCATE" 122 | grant "UPDATE" 123 | end 124 | on "microposts_id_seq" do 125 | grant "SELECT" 126 | grant "UPDATE" 127 | end 128 | end 129 | end 130 | RUBY 131 | end 132 | ).to be_truthy 133 | 134 | is_expected.to match_fuzzy <<-RUBY 135 | role "alice" do 136 | schema "master" do 137 | on "users" do 138 | grant "DELETE" 139 | grant "INSERT" 140 | grant "REFERENCES" 141 | grant "SELECT" 142 | grant "TRIGGER" 143 | grant "TRUNCATE" 144 | grant "UPDATE" 145 | end 146 | on "users_id_seq" do 147 | grant "SELECT" 148 | grant "UPDATE" 149 | end 150 | end 151 | end 152 | 153 | role "bob" do 154 | schema "main" do 155 | on "microposts" do 156 | grant "DELETE", :grantable => true 157 | grant "INSERT" 158 | grant "REFERENCES" 159 | grant "SELECT" 160 | grant "TRIGGER" 161 | grant "TRUNCATE" 162 | grant "UPDATE" 163 | end 164 | on "microposts_id_seq" do 165 | grant "SELECT" 166 | grant "UPDATE" 167 | end 168 | end 169 | end 170 | RUBY 171 | end 172 | end 173 | 174 | context 'when revoke' do 175 | it do 176 | expect( 177 | apply_grants do 178 | <<-RUBY 179 | role "bob" do 180 | schema "main" do 181 | on "microposts" do 182 | grant "DELETE", :grantable => true 183 | grant "INSERT" 184 | grant "SELECT" 185 | grant "UPDATE" 186 | end 187 | end 188 | end 189 | RUBY 190 | end 191 | ).to be_truthy 192 | 193 | is_expected.to match_fuzzy <<-RUBY 194 | role "bob" do 195 | schema "main" do 196 | on "microposts" do 197 | grant "DELETE", :grantable => true 198 | grant "INSERT" 199 | grant "SELECT" 200 | grant "UPDATE" 201 | end 202 | end 203 | end 204 | RUBY 205 | end 206 | end 207 | 208 | context 'when grant grant_option' do 209 | it do 210 | expect( 211 | apply_grants do 212 | <<-RUBY 213 | role "bob" do 214 | schema "main" do 215 | on "microposts" do 216 | grant "DELETE", :grantable => true 217 | grant "INSERT", :grantable => true 218 | grant "REFERENCES" 219 | grant "SELECT" 220 | grant "TRIGGER" 221 | grant "TRUNCATE" 222 | grant "UPDATE" 223 | end 224 | on "microposts_id_seq" do 225 | grant "SELECT" 226 | grant "UPDATE" 227 | end 228 | end 229 | end 230 | RUBY 231 | end 232 | ).to be_truthy 233 | 234 | is_expected.to match_fuzzy <<-RUBY 235 | role "bob" do 236 | schema "main" do 237 | on "microposts" do 238 | grant "DELETE", :grantable => true 239 | grant "INSERT", :grantable => true 240 | grant "REFERENCES" 241 | grant "SELECT" 242 | grant "TRIGGER" 243 | grant "TRUNCATE" 244 | grant "UPDATE" 245 | end 246 | on "microposts_id_seq" do 247 | grant "SELECT" 248 | grant "UPDATE" 249 | end 250 | end 251 | end 252 | RUBY 253 | end 254 | end 255 | 256 | context 'when revoke grant_option' do 257 | it do 258 | expect( 259 | apply_grants do 260 | <<-RUBY 261 | role "bob" do 262 | schema "main" do 263 | on "microposts" do 264 | grant "DELETE" 265 | grant "INSERT" 266 | grant "REFERENCES" 267 | grant "SELECT" 268 | grant "TRIGGER" 269 | grant "TRUNCATE" 270 | grant "UPDATE" 271 | end 272 | on "microposts_id_seq" do 273 | grant "SELECT" 274 | grant "UPDATE" 275 | end 276 | end 277 | end 278 | RUBY 279 | end 280 | ).to be_truthy 281 | 282 | is_expected.to match_fuzzy <<-RUBY 283 | role "bob" do 284 | schema "main" do 285 | on "microposts" do 286 | grant "DELETE" 287 | grant "INSERT" 288 | grant "REFERENCES" 289 | grant "SELECT" 290 | grant "TRIGGER" 291 | grant "TRUNCATE" 292 | grant "UPDATE" 293 | end 294 | on "microposts_id_seq" do 295 | grant "SELECT" 296 | grant "UPDATE" 297 | end 298 | end 299 | end 300 | RUBY 301 | end 302 | end 303 | 304 | context 'when grant using regexp' do 305 | it do 306 | expect( 307 | apply_grants do 308 | <<-RUBY 309 | role "alice" do 310 | schema "master" do 311 | on /^users/ do 312 | grant "SELECT" 313 | grant "UPDATE" 314 | end 315 | end 316 | end 317 | 318 | role "bob" do 319 | schema "main" do 320 | on "microposts" do 321 | grant "DELETE", :grantable => true 322 | grant "INSERT" 323 | grant "REFERENCES" 324 | grant "SELECT" 325 | grant "TRIGGER" 326 | grant "TRUNCATE" 327 | grant "UPDATE" 328 | end 329 | on "microposts_id_seq" do 330 | grant "SELECT" 331 | grant "UPDATE" 332 | end 333 | end 334 | end 335 | RUBY 336 | end 337 | ).to be_truthy 338 | 339 | is_expected.to match_fuzzy <<-RUBY 340 | role "alice" do 341 | schema "master" do 342 | on "users" do 343 | grant "SELECT" 344 | grant "UPDATE" 345 | end 346 | on "users_id_seq" do 347 | grant "SELECT" 348 | grant "UPDATE" 349 | end 350 | end 351 | end 352 | 353 | role "bob" do 354 | schema "main" do 355 | on "microposts" do 356 | grant "DELETE", :grantable => true 357 | grant "INSERT" 358 | grant "REFERENCES" 359 | grant "SELECT" 360 | grant "TRIGGER" 361 | grant "TRUNCATE" 362 | grant "UPDATE" 363 | end 364 | on "microposts_id_seq" do 365 | grant "SELECT" 366 | grant "UPDATE" 367 | end 368 | end 369 | end 370 | RUBY 371 | end 372 | end 373 | end 374 | -------------------------------------------------------------------------------- /spec/posgra_roles_create_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'roles (create)' do 2 | include SpecHelper 3 | 4 | subject { export_roles } 5 | 6 | context 'nothing to do' do 7 | it do 8 | expect( 9 | apply_roles { '' } 10 | ).to be_falsey 11 | 12 | is_expected.to be_empty 13 | end 14 | end 15 | 16 | context 'when create user' do 17 | it do 18 | expect( 19 | apply_roles do 20 | <<-RUBY 21 | user "alice" 22 | user "bob" 23 | RUBY 24 | end 25 | ).to be_truthy 26 | 27 | is_expected.to match_fuzzy <<-RUBY 28 | user "alice" 29 | user "bob" 30 | RUBY 31 | end 32 | end 33 | 34 | context 'when create group only' do 35 | it do 36 | expect( 37 | apply_roles do 38 | <<-RUBY 39 | group "staff" 40 | group "engineer" 41 | RUBY 42 | end 43 | ).to be_truthy 44 | 45 | is_expected.to match_fuzzy <<-RUBY 46 | group "engineer" do 47 | # no users 48 | end 49 | 50 | group "staff" do 51 | # no users 52 | end 53 | RUBY 54 | end 55 | end 56 | 57 | context 'when create group and user' do 58 | it do 59 | expect( 60 | apply_roles do 61 | <<-RUBY 62 | group "staff" do 63 | user "alice" 64 | user "bob" 65 | end 66 | 67 | group "engineer" do 68 | user "bob" 69 | end 70 | RUBY 71 | end 72 | ).to be_truthy 73 | 74 | is_expected.to match_fuzzy <<-RUBY 75 | group "engineer" do 76 | user "bob" 77 | end 78 | 79 | group "staff" do 80 | user "alice" 81 | user "bob" 82 | end 83 | RUBY 84 | end 85 | end 86 | 87 | context 'when create group and toplevel user' do 88 | it do 89 | expect( 90 | apply_roles do 91 | <<-RUBY 92 | user "alice" 93 | 94 | group "staff" do 95 | user "bob" 96 | end 97 | RUBY 98 | end 99 | ).to be_truthy 100 | 101 | is_expected.to match_fuzzy <<-RUBY 102 | user "alice" 103 | 104 | group "staff" do 105 | user "bob" 106 | end 107 | RUBY 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /spec/posgra_roles_delete_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'roles (delete)' do 2 | include SpecHelper 3 | 4 | subject { export_roles } 5 | 6 | before do 7 | apply_roles do 8 | <<-RUBY 9 | user "alice" 10 | 11 | group "staff" do 12 | user "bob" 13 | end 14 | RUBY 15 | end 16 | end 17 | 18 | context 'when drop user' do 19 | it do 20 | expect( 21 | apply_roles do 22 | <<-RUBY 23 | group "staff" do 24 | user "bob" 25 | end 26 | RUBY 27 | end 28 | ).to be_truthy 29 | 30 | is_expected.to match_fuzzy <<-RUBY 31 | group "staff" do 32 | user "bob" 33 | end 34 | RUBY 35 | end 36 | end 37 | 38 | context 'when drop user in group' do 39 | it do 40 | expect( 41 | apply_roles do 42 | <<-RUBY 43 | user "alice" 44 | group "staff" 45 | RUBY 46 | end 47 | ).to be_truthy 48 | 49 | is_expected.to match_fuzzy <<-RUBY 50 | user "alice" 51 | 52 | group "staff" do 53 | # no users 54 | end 55 | RUBY 56 | end 57 | end 58 | 59 | context 'when drop group' do 60 | it do 61 | expect( 62 | apply_roles do 63 | <<-RUBY 64 | user "alice" 65 | user "bob" 66 | RUBY 67 | end 68 | ).to be_truthy 69 | 70 | is_expected.to match_fuzzy <<-RUBY 71 | user "alice" 72 | user "bob" 73 | RUBY 74 | end 75 | end 76 | 77 | context 'when drop user and group' do 78 | it do 79 | apply_roles { '' } 80 | is_expected.to be_empty 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/posgra_roles_update_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'roles (update)' do 2 | include SpecHelper 3 | 4 | subject { export_roles } 5 | 6 | before do 7 | apply_roles do 8 | <<-RUBY 9 | user "alice" 10 | 11 | group "staff" do 12 | user "bob" 13 | end 14 | RUBY 15 | end 16 | end 17 | 18 | context 'when add member' do 19 | it do 20 | expect( 21 | apply_roles do 22 | <<-RUBY 23 | group "staff" do 24 | user "alice" 25 | user "bob" 26 | end 27 | RUBY 28 | end 29 | ).to be_truthy 30 | 31 | is_expected.to match_fuzzy <<-RUBY 32 | group "staff" do 33 | user "alice" 34 | user "bob" 35 | end 36 | RUBY 37 | end 38 | end 39 | 40 | context 'when drop member' do 41 | it do 42 | expect( 43 | apply_roles do 44 | <<-RUBY 45 | user "alice" 46 | user "bob" 47 | RUBY 48 | end 49 | ).to be_truthy 50 | 51 | is_expected.to match_fuzzy <<-RUBY 52 | user "alice" 53 | user "bob" 54 | RUBY 55 | end 56 | end 57 | 58 | context 'when group -> user' do 59 | it do 60 | expect( 61 | apply_roles do 62 | <<-RUBY 63 | user "alice" 64 | user "bob" 65 | user "staff" 66 | RUBY 67 | end 68 | ).to be_truthy 69 | 70 | is_expected.to match_fuzzy <<-RUBY 71 | user "alice" 72 | user "bob" 73 | user "staff" 74 | RUBY 75 | end 76 | end 77 | 78 | context 'when user -> group' do 79 | it do 80 | expect( 81 | apply_roles do 82 | <<-RUBY 83 | group "alice" 84 | 85 | group "staff" do 86 | user "bob" 87 | end 88 | RUBY 89 | end 90 | ).to be_truthy 91 | 92 | is_expected.to match_fuzzy <<-RUBY 93 | group "alice" do 94 | # no users 95 | end 96 | 97 | group "staff" do 98 | user "bob" 99 | end 100 | RUBY 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /spec/posgra_template_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'template' do 2 | include SpecHelper 3 | 4 | describe "roles" do 5 | subject { export_roles } 6 | 7 | context 'when use template' do 8 | it do 9 | expect( 10 | apply_roles do 11 | <<-RUBY 12 | template "my tmpl" do 13 | user context.name 14 | end 15 | 16 | template "bob tmpl" do 17 | user "bob" 18 | end 19 | 20 | include_template "my tmpl", name: "alice" 21 | 22 | group "staff" do 23 | include_template "bob tmpl" 24 | end 25 | RUBY 26 | end 27 | ).to be_truthy 28 | 29 | is_expected.to match_fuzzy <<-RUBY 30 | user "alice" 31 | 32 | group "staff" do 33 | user "bob" 34 | end 35 | RUBY 36 | end 37 | end 38 | end 39 | 40 | describe "grants" do 41 | subject { export_grants } 42 | 43 | context 'when use template' do 44 | before do 45 | apply_roles do 46 | <<-RUBY 47 | group "engineer" do 48 | user "bob" 49 | end 50 | 51 | group "staff" do 52 | user "alice" 53 | end 54 | RUBY 55 | end 56 | end 57 | 58 | it do 59 | expect( 60 | apply_grants do 61 | <<-RUBY 62 | template "object tmpl" do 63 | on context.object do 64 | grant "DELETE", grantable: true 65 | grant "INSERT" 66 | grant "REFERENCES" 67 | grant "SELECT" 68 | grant "TRIGGER" 69 | grant "TRUNCATE" 70 | grant "UPDATE" 71 | end 72 | end 73 | 74 | template "grant tmpl" do 75 | grant "SELECT" 76 | end 77 | 78 | role "bob" do 79 | schema "main" do 80 | include_template "object tmpl", object: "microposts" 81 | 82 | on "microposts_id_seq" do 83 | include_template "grant tmpl" 84 | grant "UPDATE" 85 | end 86 | end 87 | end 88 | RUBY 89 | end 90 | ).to be_truthy 91 | 92 | is_expected.to match_fuzzy <<-RUBY 93 | role "bob" do 94 | schema "main" do 95 | on "microposts" do 96 | grant "DELETE", :grantable => true 97 | grant "INSERT" 98 | grant "REFERENCES" 99 | grant "SELECT" 100 | grant "TRIGGER" 101 | grant "TRUNCATE" 102 | grant "UPDATE" 103 | end 104 | on "microposts_id_seq" do 105 | grant "SELECT" 106 | grant "UPDATE" 107 | end 108 | end 109 | end 110 | RUBY 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | ENV['TZ'] = 'UTC' 4 | 5 | if ENV['TRAVIS'] 6 | require 'simplecov' 7 | require 'coveralls' 8 | 9 | SimpleCov.formatter = Coveralls::SimpleCov::Formatter 10 | SimpleCov.start do 11 | add_filter "spec/" 12 | end 13 | end 14 | 15 | require 'posgra' 16 | require 'tempfile' 17 | require 'timecop' 18 | require 'rspec/match_fuzzy' 19 | 20 | RSpec.configure do |config| 21 | config.before(:all) do 22 | drop_test_db 23 | end 24 | 25 | config.before(:each) do 26 | create_test_db 27 | end 28 | 29 | config.after(:each) do 30 | drop_test_db 31 | end 32 | end 33 | 34 | module SpecHelper 35 | DBHOST = ENV['POSGRA_TEST_HOST'] || '127.0.0.1' 36 | DBPORT = (ENV['POSGRA_TEST_PORT'] || 5432).to_i 37 | DBUSER = ENV['POSGRA_TEST_USER'] || 'postgres' 38 | DBPASS = ENV['POSGRA_TEST_PASS'] || 'password' 39 | DBNAME = 'posgra_test' 40 | DEFAULT_DBNAME = ENV['POSGRA_TEST_DEFAULT_DB'] || 'postgres' 41 | 42 | def apply_roles(options = {}) 43 | tempfile(yield) do |f| 44 | run_client(options) do |client| 45 | client.apply_roles(f.path) 46 | end 47 | end 48 | end 49 | 50 | def export_roles(options = {}) 51 | run_client(options) do |client| 52 | client.export_roles 53 | end 54 | end 55 | 56 | def apply_grants(options = {}) 57 | tempfile(yield) do |f| 58 | run_client(options) do |client| 59 | client.apply_grants(f.path) 60 | end 61 | end 62 | end 63 | 64 | def export_grants(options = {}) 65 | run_client(options) do |client| 66 | client.export_grants 67 | end 68 | end 69 | 70 | def apply_databases(options = {}) 71 | tempfile(yield) do |f| 72 | run_client(options) do |client| 73 | client.apply_databases(f.path) 74 | end 75 | end 76 | end 77 | 78 | def export_databases(options = {}) 79 | run_client(options) do |client| 80 | client.export_databases 81 | end 82 | end 83 | 84 | def run_client(options = {}) 85 | options = { 86 | host: DBHOST, 87 | port: DBPORT, 88 | user: DBUSER, 89 | password: DBPASS, 90 | dbname: DBNAME, 91 | logger: Logger.new('/dev/null'), 92 | include_role: /\A(?:alice|bob|staff|engineer)/, 93 | exclude_role: /\A#{DBUSER}\z/, 94 | identifier: Posgra::Identifier::Auto.new('/dev/null') 95 | }.merge(options) 96 | 97 | if ENV['DEBUG'] 98 | logger = Posgra::Logger.instance 99 | logger.set_debug(true) 100 | 101 | options.update( 102 | debug: true, 103 | logger: logger 104 | ) 105 | end 106 | 107 | client = Posgra::Client.new(options) 108 | retval = nil 109 | 110 | begin 111 | retval = yield(client) 112 | ensure 113 | client.close 114 | end 115 | 116 | retval 117 | end 118 | 119 | def tempfile(content, options = {}) 120 | basename = "#{File.basename __FILE__}.#{$$}" 121 | basename = [basename, options[:ext]] if options[:ext] 122 | 123 | Tempfile.open(basename) do |f| 124 | f.puts(content) 125 | f.flush 126 | f.rewind 127 | yield(f) 128 | end 129 | end 130 | 131 | def pg(dbname = DBNAME) 132 | begin 133 | conn = PG::Connection.connect( 134 | host: DBHOST, 135 | port: DBPORT, 136 | user: DBUSER, 137 | password: DBPASS, 138 | dbname: dbname, 139 | ) 140 | retval = yield(conn) 141 | ensure 142 | conn.close if conn 143 | end 144 | 145 | retval 146 | end 147 | 148 | def create_test_db 149 | pg(DEFAULT_DBNAME) {|conn| conn.exec "CREATE DATABASE #{DBNAME}" } 150 | 151 | pg do |conn| 152 | conn.exec <<-SQL 153 | /* --- main --- */ 154 | CREATE SCHEMA main; 155 | 156 | set search_path to main; 157 | 158 | /* microposts */ 159 | CREATE TABLE microposts ( 160 | id integer NOT NULL, 161 | content character varying(255), 162 | user_id integer, 163 | inserted_at timestamp without time zone NOT NULL, 164 | updated_at timestamp without time zone NOT NULL 165 | ); 166 | 167 | CREATE SEQUENCE microposts_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; 168 | ALTER TABLE ONLY microposts ALTER COLUMN id SET DEFAULT nextval('microposts_id_seq'::regclass); 169 | 170 | ALTER TABLE ONLY microposts ADD CONSTRAINT microposts_pkey PRIMARY KEY (id); 171 | 172 | /* --- master --- */ 173 | CREATE SCHEMA master; 174 | 175 | set search_path to master; 176 | 177 | /* users */ 178 | CREATE TABLE users ( 179 | id integer NOT NULL, 180 | name character varying(255), 181 | email character varying(255), 182 | inserted_at timestamp without time zone NOT NULL, 183 | updated_at timestamp without time zone NOT NULL 184 | ); 185 | 186 | CREATE SEQUENCE users_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; 187 | ALTER TABLE ONLY users ALTER COLUMN id SET DEFAULT nextval('users_id_seq'::regclass); 188 | 189 | ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id); 190 | 191 | /* schema_migrations */ 192 | CREATE TABLE schema_migrations ( 193 | version bigint NOT NULL, 194 | inserted_at timestamp without time zone 195 | ); 196 | 197 | ALTER TABLE ONLY schema_migrations ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); 198 | SQL 199 | end 200 | end 201 | 202 | def drop_test_db 203 | pg(DEFAULT_DBNAME) do |conn| 204 | conn.exec "SET client_min_messages = WARNING" 205 | conn.exec "DROP DATABASE IF EXISTS #{DBNAME}" 206 | 207 | conn.exec <<-SQL 208 | DROP ROLE IF EXISTS alice; 209 | DROP ROLE IF EXISTS bob; 210 | DROP ROLE IF EXISTS staff; 211 | DROP ROLE IF EXISTS engineer; 212 | DROP ROLE IF EXISTS "alice alice"; 213 | DROP ROLE IF EXISTS "bob-bob"; 214 | DROP ROLE IF EXISTS "staff staff"; 215 | DROP ROLE IF EXISTS "engineer-engineer"; 216 | SQL 217 | end 218 | end 219 | end 220 | --------------------------------------------------------------------------------