grantinee 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rubocop/rake_task'
5
+ require 'rspec/core/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ RuboCop::RakeTask.new do |task|
10
+ task.options = %w[-a]
11
+ end
12
+
13
+ task(:default).clear
14
+ task default: %i[rubocop spec]
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'awesome_print'
6
+ require 'grantinee'
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+
11
+ # (If you use this, don't forget to add pry to your Gemfile!)
12
+ # require "pry"
13
+ # Pry.start
14
+
15
+ require 'irb'
16
+ IRB.start(__FILE__)
@@ -0,0 +1,10 @@
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
+ RACK_ENV=mysql bundle exec rake db:create
10
+ RACK_ENV=postgresql bundle exec rake db:create
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is a sample configuration file for you to play with
4
+ Grantinee.configure do |c|
5
+ c.engine = :postgresql
6
+
7
+ case c.engine
8
+ when :mysql
9
+ c.username = 'root'
10
+ c.password = 'mysql'
11
+ c.hostname = ENV['MYSQL_HOST'] || 'localhost'
12
+ c.port = 3306
13
+ c.database = 'grantinee_development'
14
+
15
+ when :mysql_url
16
+ c.url = 'mysql://root:mysql@localhost:3306/grantinee_development'
17
+
18
+ when :postgresql
19
+ c.username = 'postgres'
20
+ c.password = 'postgres'
21
+ c.hostname = ENV['POSTGRES_HOST'] || 'localhost'
22
+ c.port = 5432
23
+ c.database = 'grantinee_development'
24
+
25
+ when :postgresql_url
26
+ c.url = 'postgres://postgres:postgres@localhost:5432/grantinee_development'
27
+
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ version: '3'
2
+ services:
3
+
4
+ mysql:
5
+ image: mysql:5.7.15
6
+ volumes:
7
+ - ./mysql:/var/lib/mysql
8
+ ports:
9
+ - "3306:3306"
10
+ environment:
11
+ MYSQL_ROOT_PASSWORD: mysql
12
+
13
+ postgres:
14
+ image: postgres:9.6
15
+ volumes:
16
+ - ./postgres/data:/var/lib/postgresql/data
17
+ ports:
18
+ - "5432:5432"
19
+ environment:
20
+ - POSTGRES_PASSWORD=postgres
21
+
22
+ rspec:
23
+ environment:
24
+ MYSQL_HOST: 'mysql'
25
+ POSTGRES_HOST: 'postgres'
26
+ tty: true
27
+ stdin_open: true
28
+ build: .
29
+ command: bundle exec rspec
30
+ volumes:
31
+ - .:/myapp
32
+ depends_on:
33
+ - mysql
34
+ - postgres
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'grantinee'
6
+ require 'optparse'
7
+
8
+ # Autodetect plug-and-play connection
9
+ Bundler.require if File.exist? 'Gemfile.lock'
10
+
11
+ # Logger setup
12
+ logger = Logger.new(STDOUT)
13
+ logger.level = Logger::INFO
14
+
15
+ Grantinee::CLI.new(ARGV, logger).run!
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "lib"))
4
+
5
+ require "grantinee/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "grantinee"
9
+ spec.version = Grantinee::VERSION
10
+ spec.authors = ["Paweł Komarnicki"]
11
+ spec.email = ["pawel@blinkist.com"]
12
+
13
+ spec.summary = '"Your permissions, freshly baked!" | A library to manage your database permissions for MySQL and Postgres'
14
+ spec.description = "A Ruby library to manage your database permissions for MySQL and PostgreSQL. Supports per-table, and per-column permissions for granular access and security."
15
+ spec.homepage = "https://github.com/blinkist/grantinee"
16
+ spec.license = "MIT"
17
+
18
+ # Specify which files should be added to the gem when it is released.
19
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
+ spec.files = Dir.chdir(File.dirname(__FILE__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ end
23
+ spec.bindir = "exe"
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.16"
28
+ spec.add_development_dependency "byebug"
29
+ spec.add_development_dependency "method_source"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec", "~> 3.0"
32
+ spec.add_development_dependency "rubocop"
33
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ # Grantinee module is where the magic at ;-)
6
+ module Grantinee
7
+ class << self
8
+ # Allow configuration using a block
9
+ def configure
10
+ yield configuration
11
+ end
12
+
13
+ # Returns configuration
14
+ def configuration
15
+ @configuration ||= Configuration.new
16
+ end
17
+
18
+ def logger
19
+ configuration.logger
20
+ end
21
+
22
+ def logger=(logger)
23
+ configuration.logger = logger
24
+ end
25
+
26
+ extend Gem::Deprecate
27
+ deprecate :logger=, "Please provide logger via configure block", 2018, 7
28
+ end
29
+ end
30
+
31
+ # Load internal stuffs
32
+ require 'grantinee/configuration'
33
+ require 'grantinee/engine'
34
+ require 'grantinee/cli'
35
+ require 'grantinee/dsl'
36
+ require 'grantinee/executor'
37
+ require 'grantinee/engine/abstract_engine'
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grantinee
4
+ class CLI
5
+ attr_accessor :options
6
+ attr_accessor :dsl
7
+ attr_accessor :engine
8
+
9
+ def initialize(args = ARGV, logger = ::Logger.new($stderr))
10
+ @args = args
11
+ @logger = logger
12
+
13
+ @options = {}
14
+ end
15
+
16
+ def run!
17
+ parse_command_line_parameters
18
+ process_command_line_parameters
19
+
20
+ @dsl = build_dsl
21
+ @engine = build_engine
22
+ @executor = build_executor
23
+ @executor.run!
24
+
25
+ [@dsl, @engine, @executor]
26
+ end
27
+
28
+ private
29
+
30
+ def parse_command_line_parameters # rubocop:disable Metrics/MethodLength
31
+ parser = OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
32
+ opts.banner = "Usage: grantinee [options]"
33
+
34
+ # Help
35
+ opts.on(
36
+ '-h', '--help',
37
+ "Displays help"
38
+ ) do
39
+ puts opts
40
+ exit
41
+ end
42
+
43
+ # Verbose mode
44
+ opts.on(
45
+ '-vLEVEL', '--verbosity=LEVEL',
46
+ "Set verbosity level to debug, info, warn, error, fatal, or unknown (default: warning)"
47
+ ) do |level|
48
+ @options[:verbose] = level || 'warning'
49
+ end
50
+
51
+ # App boot file
52
+ opts.on(
53
+ '-rFILE', '--require=FILE',
54
+ "Application boot file path (default: ./config/environment.rb)"
55
+ ) do |file_path|
56
+ @options[:require] = file_path
57
+ end
58
+
59
+ # Grantinee file
60
+ opts.on(
61
+ '-fFILE', '--file=FILE',
62
+ "Permission definitions file path (default: ./Grantinee)"
63
+ ) do |file_path|
64
+ @options[:file] = file_path
65
+ end
66
+
67
+ # Database configuration file
68
+ opts.on(
69
+ '-cFILE', '--config=FILE',
70
+ "Database configuration file path"
71
+ ) do |file_path|
72
+ @options[:config] = file_path
73
+ end
74
+ end
75
+
76
+ parser.parse! @args
77
+ end
78
+
79
+ # Process parsed parameters
80
+ def process_command_line_parameters
81
+ process_require_param
82
+ process_database_param
83
+ process_grantinee_param
84
+ process_verbosity_param
85
+ end
86
+
87
+ def build_dsl
88
+ Grantinee.configuration.logger = @logger
89
+ Grantinee::Dsl.eval(File.read(@options[:file]))
90
+ end
91
+
92
+ def build_engine
93
+ Grantinee::Engine.for Grantinee.configuration.engine
94
+ end
95
+
96
+ def build_executor
97
+ Grantinee::Executor.new(@dsl, @engine)
98
+ end
99
+
100
+ # Application boot file
101
+ def process_require_param
102
+ if @options[:require]
103
+ require @options[:require]
104
+ elsif defined?(Rails)
105
+ require './config/environment'
106
+ end
107
+ end
108
+
109
+ # Database configuration file
110
+ def process_database_param
111
+ require options[:config] if options[:config]
112
+
113
+ Grantinee::Engine.detect_active_record_connection! unless Grantinee.configuration.configured?
114
+ end
115
+
116
+ # Grantinee file
117
+ def process_grantinee_param
118
+ @options[:file] ||= "Grantinee"
119
+ end
120
+
121
+ # Explicit verbose mode, overrides configuration value
122
+ def process_verbosity_param
123
+ return unless @options[:verbose]
124
+ log_levels = %w[debug info warn error fatal unknown]
125
+ @logger.level = log_levels.index(@options[:verbose])
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Grantinee
6
+ class Configuration
7
+ # Which engine is used by the library?
8
+ attr_accessor :engine
9
+
10
+ attr_accessor :logger
11
+
12
+ # Connection parameters
13
+ attr_accessor :username
14
+ attr_accessor :password
15
+ attr_accessor :hostname
16
+ attr_accessor :port
17
+ attr_accessor :database
18
+ attr_reader :url
19
+
20
+ # Allow verbose mode
21
+ attr_accessor :verbose
22
+
23
+ def initialize
24
+ @logger = ::Logger.new(nil)
25
+ end
26
+
27
+ def configured?
28
+ username && password && hostname && port && database
29
+ end
30
+
31
+ # Handle url -> fields conversion
32
+ def url=(url)
33
+ uri = URI.parse url
34
+
35
+ case uri.scheme
36
+ when /^mysql/
37
+ default_port = 3306
38
+ @engine = :mysql
39
+ when /^postgres/
40
+ default_port = 5432
41
+ @engine = :postgres
42
+ end
43
+
44
+ @username = uri.user
45
+ @password = uri.password
46
+ @hostname = uri.host
47
+ @port = uri.port || default_port
48
+ @database = (uri.path || '').split('/').last
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grantinee
4
+ class Dsl
5
+ attr_accessor :permissions
6
+
7
+ # Allow evaluation of the code coming from the Grantinee file
8
+ def self.eval(commands)
9
+ new.tap do |x|
10
+ x.eval(commands)
11
+ end
12
+ end
13
+
14
+ # Initialize defaults
15
+ def initialize
16
+ @permissions = []
17
+ @data = {}
18
+ end
19
+
20
+ def eval(commands)
21
+ instance_eval(commands)
22
+ end
23
+
24
+ # Define database and mode
25
+ def on(database, &block)
26
+ logger.debug "Got database: #{database}"
27
+
28
+ @data[:database] = database
29
+
30
+ instance_eval(&block) if block_given?
31
+ end
32
+
33
+ # Define user and host
34
+ # Note: revokes all permissions for given user first
35
+ def user(user, &block)
36
+ logger.debug "Got user: #{user}"
37
+
38
+ @data[:user], @data[:host] = user.to_s.split '@'
39
+ @data[:host] ||= '%'
40
+
41
+ instance_eval(&block) if block_given?
42
+ end
43
+
44
+ # Define permission grants
45
+ Engine::WHITELISTED_KINDS.each do |kind|
46
+ define_method(kind.to_sym) do |table, fields = []|
47
+ logger.debug "Got table: #{table}, fields: #{fields}"
48
+
49
+ @permissions << @data.merge(
50
+ kind: kind,
51
+ table: table,
52
+ fields: fields
53
+ )
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def logger
60
+ Grantinee.logger
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grantinee
4
+ module Engine
5
+ SUPPORTED_ENGINES = %w[mysql postgresql].freeze
6
+
7
+ WHITELISTED_KINDS = %w[all usage select update insert].freeze
8
+
9
+ class << self
10
+ # Get appropriate engine class for the engine name
11
+ def for(engine)
12
+ logger.debug "Using engine: #{engine}"
13
+ unless SUPPORTED_ENGINES.include?(engine.to_s)
14
+ raise "Engine '#{engine}' is not supported, supported engines: #{SUPPORTED_ENGINES}"
15
+ end
16
+
17
+ case engine.to_s
18
+ when 'mysql'
19
+ require 'grantinee/engine/mysql'
20
+ Mysql.new
21
+ when 'postgresql'
22
+ require 'grantinee/engine/postgresql'
23
+ Postgresql.new
24
+ end
25
+ end
26
+
27
+ def detect_active_record_connection!
28
+ return unless defined?(ActiveRecord::Base)
29
+
30
+ configure_for_active_record(ActiveRecord::Base.connection_config)
31
+ end
32
+
33
+ private
34
+
35
+ def logger
36
+ Grantinee.logger
37
+ end
38
+
39
+ def configure_for_active_record(ar_config)
40
+ if ar_config[:url]
41
+ configure_for_active_record_url(ar_config)
42
+ else
43
+ configure_for_active_record_fields(ar_config)
44
+ end
45
+
46
+ Grantinee.configuration.engine = case ar_config[:adapter]
47
+ when 'mysql', 'mysql2'
48
+ :mysql
49
+ when 'postgresql', 'pg'
50
+ :postgresql
51
+ end
52
+ end
53
+
54
+ def configure_for_active_record_url(ar_config)
55
+ Grantinee.configuration.url = ar_config[:url]
56
+ end
57
+
58
+ def configure_for_active_record_fields(ar_config)
59
+ Grantinee.configuration.username = ar_config[:username]
60
+ Grantinee.configuration.password = ar_config[:password]
61
+ Grantinee.configuration.hostname = ar_config[:host]
62
+ Grantinee.configuration.port = ar_config[:port]
63
+ Grantinee.configuration.database = ar_config[:database]
64
+ end
65
+ end
66
+ end
67
+ end