forty 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 97efc5c8cb7262d1dfb61406f68497e31d49d90d
4
- data.tar.gz: 393e67307c31c8273709d61bff8b6428186925ce
3
+ metadata.gz: 1c69cf32507f1a087a044f2571606170c3c28459
4
+ data.tar.gz: 7d13e1d39898bc33077e627463abff4040f0bb97
5
5
  SHA512:
6
- metadata.gz: de46378f836e770d1d02f9e7319691ffc08dd59165255fd09440b86cba41f23e786674d2b84f18d2aa007ccdafaada6410b867bcb4bb7e41f3aeb011b29529cd
7
- data.tar.gz: f866784cdcbce84f15f36943e0d188eaf731bdea969f0043ea2839040431c39fa518781399a24ca493bb18f4330af4f326d55101243e974f6c0f565813f9a489
6
+ metadata.gz: a77cdb8ce81e28740ece0fd5d6fc02809cb9c36d855b638224480476f07f18b63b4437882453e036d496fb31d63ddfd9c1d68d4b5beb69b7cf3bd9a5e40cc0e7
7
+ data.tar.gz: 7d5afa71ef584d53e72ae07b3a72741028fdabec327246381a8eb7d2792dd64d40a82f74c950822528d61eb73fcfd55566cdad1406836e10cda82831633a92c4
@@ -1,3 +1,5 @@
1
+ require 'logger'
2
+
1
3
  module Forty
2
4
  class Configuration
3
5
  attr_accessor :logger
@@ -5,6 +7,14 @@ module Forty
5
7
  attr_accessor :schemas
6
8
  attr_accessor :acl_file
7
9
  attr_accessor :generate_passwords
10
+
11
+ def initialize
12
+ @logger = ::Logger.new(STDOUT)
13
+ @logger.level = ::Logger::INFO
14
+ @logger.formatter = proc do |severity, _, _, message|
15
+ "[Forty] [#{severity}] #{message}\n"
16
+ end
17
+ end
8
18
  end
9
19
 
10
20
  class Database
@@ -19,7 +19,5 @@ module Forty
19
19
 
20
20
  @db.exec(statement)
21
21
  end
22
-
23
- alias_method :dwh, :execute
24
22
  end
25
23
  end
@@ -0,0 +1,69 @@
1
+ module Forty
2
+ module Privilege
3
+ class Base
4
+ PRIVILEGES = self.constants.map { |const| self.const_get(const) }
5
+
6
+ def self.get_privilege_name_by_acronym(acronym)
7
+ privilege = self.constants.select do |constant|
8
+ self.const_get(constant).eql?(acronym)
9
+ end[0]
10
+ privilege.nil? ? nil : privilege.to_s.downcase
11
+ end
12
+
13
+ def self.parse_privileges_from_string(privileges_string)
14
+ privileges = []
15
+ self.constants.each do |constant|
16
+ acronym = self.const_get(constant)
17
+ unless privileges_string.slice!(acronym).nil?
18
+ privileges << self.get_privilege_name_by_acronym(acronym)
19
+ end
20
+ break if privileges_string.empty?
21
+ end
22
+ privileges
23
+ end
24
+ end
25
+
26
+ # https://www.postgresql.org/docs/9.6/static/sql-grant.html
27
+ # r -- SELECT ("read")
28
+ # w -- UPDATE ("write")
29
+ # a -- INSERT ("append")
30
+ # d -- DELETE
31
+ # D -- TRUNCATE
32
+ # x -- REFERENCES
33
+ # t -- TRIGGER
34
+ # X -- EXECUTE
35
+ # U -- USAGE
36
+ # C -- CREATE
37
+ # c -- CONNECT
38
+ # T -- TEMPORARY
39
+ # arwdDxt -- ALL PRIVILEGES (for tables, varies for other objects)
40
+ # * -- grant option for preceding privilege
41
+ #
42
+ # /yyyy -- role that granted this privilege
43
+
44
+ class Table < Base
45
+ ALL = 'arwdDxt'
46
+ SELECT = 'r'
47
+ UPDATE = 'W'
48
+ INSERT = 'a'
49
+ DELETE = 'd'
50
+ TRUNCATE = 'D'
51
+ REFERENCES = 'x'
52
+ TRIGGER = 't'
53
+ EXECUTE = 'X'
54
+ end
55
+
56
+ class Schema < Base
57
+ ALL = 'UC'
58
+ USAGE = 'U'
59
+ CREATE = 'C'
60
+ end
61
+
62
+ class Database < Base
63
+ ALL = 'CTc'
64
+ CREATE = 'C'
65
+ CONNECT = 'c'
66
+ TEMPORARY = 'T'
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,23 @@
1
+ require 'rake'
2
+ require_relative '../../forty'
3
+
4
+ module Forty
5
+ module Rake
6
+ class Task
7
+ include ::Rake::DSL if defined? ::Rake::DSL
8
+
9
+ def install_tasks
10
+ namespace :acl do
11
+ namespace :sync do
12
+ desc 'syncs entire acl config with database'
13
+ task :all, [:disable_dry_run] do
14
+ Forty.sync
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ Forty::Rake::Task.new.install_tasks
data/lib/forty/sync.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # require_relative 'configuration'
2
2
 
3
3
  module Forty
4
+
4
5
  def self.sync
5
6
  Forty::Sync.new(
6
7
  Forty.configuration.logger,
@@ -19,6 +20,8 @@ module Forty
19
20
  @logger = logger or raise Error, 'No logger provided'
20
21
  @master_username = master_username or raise Error, 'No master username provided'
21
22
  @production_schemas = production_schemas or raise Error, 'No production schemas provided'
23
+ @system_groups = ["pg_signal_backend"]
24
+ @system_users = ["postgres"]
22
25
  @acl_config = acl_config or raise Error, 'No acl config provided'
23
26
  @acl_config['users'] ||= {}
24
27
  @acl_config['groups'] ||= {}
@@ -30,6 +33,7 @@ module Forty
30
33
  end
31
34
 
32
35
  def run
36
+ banner()
33
37
  sync_users()
34
38
  sync_groups()
35
39
  sync_user_groups()
@@ -37,13 +41,39 @@ module Forty
37
41
  sync_acl()
38
42
  end
39
43
 
44
+ def banner
45
+ @logger.info(<<-BANNER)
46
+ Starting sync...
47
+ ____ __
48
+ / __/___ _____/ /___ __
49
+ / /_/ __ \\/ ___/ __/ / / /
50
+ / __/ /_/ / / / /_/ /_/ /
51
+ /_/ \\____/_/ \\__/\\__, / Database ACL Sync
52
+ /____/ v0.2.0
53
+
54
+ ===============================================================================
55
+
56
+ Running in #{@dry_run ? 'DRY-MODE (not enforcing state)' : 'PRODUCTION-MODE (enforcing state)'}
57
+
58
+ Configuration:
59
+ Master user: #{@master_username}
60
+ Synced schemas: #{@production_schemas.join(', ')}
61
+ System users: #{@system_users.join(', ')}
62
+ System groups: #{@system_groups.join(', ')}
63
+
64
+ ===============================================================================
65
+ BANNER
66
+ end
67
+
40
68
  def sync_users
41
69
  current_users = _get_current_dwh_users.keys
42
70
  defined_users = @acl_config['users'].keys
43
71
 
44
- undefined_users = (current_users - defined_users).uniq.compact
72
+ undefined_users = (current_users - defined_users - @system_users).uniq.compact
45
73
  missing_users = (defined_users - current_users).uniq.compact
46
74
 
75
+ @logger.debug("Undefined users: #{undefined_users}")
76
+ @logger.debug("Missing users: #{missing_users}")
47
77
  undefined_users.each { |user| _delete_user(user) }
48
78
 
49
79
  missing_users.each do |user|
@@ -61,7 +91,7 @@ module Forty
61
91
  current_groups = _get_current_dwh_groups().keys
62
92
  defined_groups = @acl_config['groups'].keys
63
93
 
64
- undefined_groups = (current_groups - defined_groups).uniq.compact
94
+ undefined_groups = (current_groups - defined_groups - @system_groups).uniq.compact
65
95
  missing_groups = (defined_groups - current_groups).uniq.compact
66
96
 
67
97
  undefined_groups.each { |group| _delete_group(group) }
@@ -108,8 +138,8 @@ module Forty
108
138
  unless schemas_owned_by_user.empty?
109
139
  tables_owned_by_user = _get_currently_owned_tables(user)
110
140
  schemas_owned_by_user.each do |schema|
111
- @executor.dwh("set search_path=#{schema}")
112
- tables = @executor.dwh("select tablename from pg_tables where schemaname='#{schema}'").map { |row| "#{schema}.#{row['tablename']}" }
141
+ @executor.execute("set search_path=#{schema}")
142
+ tables = @executor.execute("select tablename from pg_tables where schemaname='#{schema}'").map { |row| "#{schema}.#{row['tablename']}" }
113
143
  nonowned_tables_by_user = tables.uniq - tables_owned_by_user
114
144
  nonowned_tables_by_user.each { |table| _execute_statement("alter table #{table} owner to #{user};") }
115
145
  end
@@ -126,7 +156,7 @@ module Forty
126
156
  diverged = 0
127
157
 
128
158
  users.each do |user|
129
- next if user.eql?(@master_username)
159
+ next if user.eql?(@master_username) or @system_users.include?(user)
130
160
 
131
161
  raise Error, "Users are not in sync #{user}" if current_user_roles[user].nil? or defined_user_roles[user].nil?
132
162
 
@@ -206,7 +236,7 @@ module Forty
206
236
  end
207
237
 
208
238
  def _get_current_user_roles
209
- Hash[@executor.dwh(<<-SQL).map { |row| [row['usename'], row['user_roles'].split(',').select { |e| not e.empty? }.compact] }]
239
+ Hash[@executor.execute(<<-SQL).map { |row| [row['usename'], row['user_roles'].split(',').select { |e| not e.empty? }.compact] }]
210
240
  select
211
241
  usename
212
242
  , case when usecreatedb is true then 'createdb' else '' end
@@ -221,11 +251,13 @@ module Forty
221
251
  end
222
252
 
223
253
  def _check_group_unknown(current_groups, defined_groups)
224
- raise Error, 'Groups are out of sync!' if _mismatch?(current_groups, defined_groups)
254
+ @logger.debug("Check whether groups are in sync. Current: #{current_groups}; Defined: #{defined_groups}; System: #{@system_groups}")
255
+ raise Error, 'Groups are out of sync!' if _mismatch?(current_groups - @system_groups, defined_groups)
225
256
  end
226
257
 
227
258
  def _check_user_unknown(current_users, defined_users)
228
- raise Error, 'Users are out of sync!' if _mismatch?(current_users, defined_users)
259
+ @logger.debug("Check whether users are in sync. Current: #{current_users}; Defined: #{defined_users}; System: #{@system_users}")
260
+ raise Error, 'Users are out of sync!' if _mismatch?(current_users - @system_users, defined_users)
229
261
  end
230
262
 
231
263
  def _mismatch?(current, defined)
@@ -243,7 +275,7 @@ module Forty
243
275
  @logger.info("Retrying to execute statement in #{attempts*10} seconds...") if attempts > 0
244
276
  sleep (attempts*10)
245
277
  attempts += 1
246
- @executor.dwh(statement)
278
+ @executor.execute(statement)
247
279
  rescue PG::UndefinedTable => e
248
280
  @logger.error("#{e.class}: #{e.message}" )
249
281
  retry unless attempts > 3
@@ -353,7 +385,7 @@ module Forty
353
385
  ;
354
386
  SQL
355
387
 
356
- raw_dwh_users = @executor.dwh(query)
388
+ raw_dwh_users = @executor.execute(query)
357
389
 
358
390
  Hash[raw_dwh_users.map do |row|
359
391
  name = row['name']
@@ -375,7 +407,7 @@ module Forty
375
407
  from pg_group
376
408
  ;
377
409
  SQL
378
- raw_dwh_groups = @executor.dwh(query)
410
+ raw_dwh_groups = @executor.execute(query)
379
411
 
380
412
  Hash[raw_dwh_groups.map do |row|
381
413
  name = row['name']
@@ -398,7 +430,7 @@ module Forty
398
430
  ;
399
431
  SQL
400
432
 
401
- raw_schema_acl = @executor.dwh(query)
433
+ raw_schema_acl = @executor.execute(query)
402
434
  _parse_current_acl('schema', raw_schema_acl)
403
435
  end
404
436
 
@@ -414,7 +446,7 @@ module Forty
414
446
  ;
415
447
  SQL
416
448
 
417
- raw_database_acl = @executor.dwh(query)
449
+ raw_database_acl = @executor.execute(query)
418
450
  _parse_current_acl('database', raw_database_acl)
419
451
  end
420
452
 
@@ -437,7 +469,7 @@ module Forty
437
469
  ;
438
470
  SQL
439
471
 
440
- raw_table_acl = @executor.dwh(query)
472
+ raw_table_acl = @executor.execute(query)
441
473
  _parse_current_acl('table', raw_table_acl)
442
474
  end
443
475
 
@@ -459,7 +491,7 @@ module Forty
459
491
  .compact
460
492
 
461
493
  if grantees.any? { |grantee| !known_grantees.include?(grantee) }
462
- raise Error, 'Users or groups not in sync!'
494
+ raise Error, "Users or groups not in sync! Could not find #{grantees.select { |grantee| !known_grantees.include?(grantee) }.join(', ')}"
463
495
  end
464
496
 
465
497
  grantees.each do |grantee|
@@ -613,14 +645,14 @@ module Forty
613
645
 
614
646
  if schema.eql?('*')
615
647
  @production_schemas.each do |prod_schema|
616
- tables = @executor.dwh(<<-SQL).map { |row| "#{prod_schema}.#{row['tablename']}" }
648
+ tables = @executor.execute(<<-SQL).map { |row| "#{prod_schema}.#{row['tablename']}" }
617
649
  select tablename from pg_tables where schemaname='#{prod_schema}'
618
650
  SQL
619
651
 
620
652
  tables_to_grant_privileges_on.concat(tables)
621
653
  end
622
654
  else
623
- tables_to_grant_privileges_on = @executor.dwh(<<-SQL).map { |row| "#{schema}.#{row['tablename']}" }
655
+ tables_to_grant_privileges_on = @executor.execute(<<-SQL).map { |row| "#{schema}.#{row['tablename']}" }
624
656
  select tablename from pg_tables where schemaname='#{schema}'
625
657
  SQL
626
658
  end
@@ -651,9 +683,14 @@ module Forty
651
683
  parsed_acls = {}
652
684
  raw_acl.each do |row|
653
685
  name = row['name']
686
+ @logger.debug("Current ACL: [#{identifier_type}] '#{name}': #{row['acls']}")
654
687
  parsed_acl = _parse_current_permissions(identifier_type, row['acls'])
655
688
  parsed_acl.each do |grantee, privileges|
656
689
  unless grantee.empty?
690
+ if _get_current_dwh_groups().keys.include?(grantee)
691
+ @logger.debug("Grantee '#{grantee}' has been identified as a group")
692
+ grantee = "group #{grantee}"
693
+ end
657
694
  parsed_acls[grantee] ||= {}
658
695
  parsed_acls[grantee][name] ||= []
659
696
  parsed_acls[grantee][name].concat(privileges)
@@ -667,7 +704,7 @@ module Forty
667
704
  # http://www.postgresql.org/docs/8.1/static/sql-grant.html
668
705
  # Typical ACL string:
669
706
  #
670
- # admin=arwdRxt/admin,jimdo=r/admin,"group selfservice=r/admin"
707
+ # admin=arwdRxt/admin,someone=r/admin,"group selfservice=r/admin"
671
708
  #
672
709
  # =xxxx -- privileges granted to PUBLIC
673
710
  # uname=xxxx -- privileges granted to a user
@@ -708,7 +745,7 @@ module Forty
708
745
  where pg_user.usename = '#{user}'
709
746
  ;
710
747
  SQL
711
- @executor.dwh(query).map { |row| row['schemaname'] }
748
+ @executor.execute(query).map { |row| row['schemaname'] }
712
749
  end
713
750
 
714
751
  def _get_currently_owned_tables(user)
@@ -718,7 +755,7 @@ module Forty
718
755
  where tableowner = '#{user}'
719
756
  ;
720
757
  SQL
721
- @executor.dwh(query).map { |row| row['tablename'] }
758
+ @executor.execute(query).map { |row| row['tablename'] }
722
759
  end
723
760
  end
724
761
  end
metadata CHANGED
@@ -1,15 +1,115 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefanie Grunwald
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-07-25 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2017-04-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pg
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.16'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '1.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '0.16'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: cucumber
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '3.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '2.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '3.0'
53
+ - !ruby/object:Gem::Dependency
54
+ name: rake
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '10.1'
60
+ - - "<"
61
+ - !ruby/object:Gem::Version
62
+ version: '12.0'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '10.1'
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: '12.0'
73
+ - !ruby/object:Gem::Dependency
74
+ name: rspec
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '3.1'
80
+ - - "<"
81
+ - !ruby/object:Gem::Version
82
+ version: '4.0'
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '3.1'
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: '4.0'
93
+ - !ruby/object:Gem::Dependency
94
+ name: rspec-collection_matchers
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 1.1.2
100
+ - - "<"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 1.1.2
110
+ - - "<"
111
+ - !ruby/object:Gem::Version
112
+ version: '2.0'
13
113
  description:
14
114
  email: steffi@physics.org
15
115
  executables: []
@@ -20,8 +120,8 @@ files:
20
120
  - lib/forty/acl.rb
21
121
  - lib/forty/configuration.rb
22
122
  - lib/forty/database.rb
23
- - lib/forty/dbms.rb
24
- - lib/forty/redshift/privilege.rb
123
+ - lib/forty/privilege.rb
124
+ - lib/forty/rake/task.rb
25
125
  - lib/forty/sync.rb
26
126
  homepage: https://github.com/moertel/forty
27
127
  licenses:
data/lib/forty/dbms.rb DELETED
@@ -1 +0,0 @@
1
- require_relative 'redshift/privilege'
@@ -1,49 +0,0 @@
1
- module Forty
2
- module Redshift
3
- module Privilege
4
- class Base
5
- PRIVILEGES = self.constants.map { |const| self.const_get(const) }
6
-
7
- def self.get_privilege_name_by_acronym(acronym)
8
- privilege = self.constants.select do |constant|
9
- self.const_get(constant).eql?(acronym)
10
- end[0]
11
- privilege.nil? ? nil : privilege.to_s.downcase
12
- end
13
-
14
- def self.parse_privileges_from_string(privileges_string)
15
- privileges = []
16
- self.constants.each do |constant|
17
- acronym = self.const_get(constant)
18
- unless privileges_string.slice!(acronym).nil?
19
- privileges << self.get_privilege_name_by_acronym(acronym)
20
- end
21
- break if privileges_string.empty?
22
- end
23
- privileges
24
- end
25
- end
26
-
27
- class Table < Base
28
- ALL = 'arwdRxt'
29
- SELECT = 'r'
30
- UPDATE = 'w'
31
- INSERT = 'a'
32
- DELETE = 'd'
33
- REFERENCES = 'x'
34
- end
35
-
36
- class Schema < Base
37
- ALL = 'UC'
38
- USAGE = 'U'
39
- CREATE = 'C'
40
- end
41
-
42
- class Database < Base
43
- ALL = 'CT'
44
- CREATE = 'C'
45
- TEMPORARY = 'T'
46
- end
47
- end
48
- end
49
- end