pg_rls 0.2.5 → 1.0.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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +55 -17
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +19 -2
  5. data/CODE_OF_CONDUCT.md +77 -29
  6. data/Guardfile +44 -0
  7. data/README.md +247 -83
  8. data/Rakefile +5 -12
  9. data/Steepfile +29 -0
  10. data/UPGRADE.md +106 -0
  11. data/app/models/pg_rls/admin.rb +24 -0
  12. data/app/models/pg_rls/current.rb +48 -0
  13. data/app/models/pg_rls/record.rb +13 -0
  14. data/app/models/pg_rls/tenant/searchable.rb +60 -0
  15. data/app/models/pg_rls/tenant/securable.rb +67 -0
  16. data/app/models/pg_rls/tenant/switchable.rb +40 -0
  17. data/app/models/pg_rls/tenant.rb +9 -0
  18. data/assets/logo.svg +8 -0
  19. data/docker-compose.yml +14 -0
  20. data/lib/generators/pg_rls/active_record/active_record_generator.rb +62 -65
  21. data/lib/generators/pg_rls/install/install_generator.rb +38 -0
  22. data/lib/generators/pg_rls/pg_rls_generator.rb +2 -1
  23. data/lib/generators/pg_rls/templates/USAGE +28 -0
  24. data/lib/generators/pg_rls/templates/app/models/abstract_base_class.rb.tt +7 -0
  25. data/lib/generators/pg_rls/templates/app/models/model.rb.tt +22 -0
  26. data/lib/generators/pg_rls/templates/config/initializers/pg_rls.rb.tt +58 -0
  27. data/lib/generators/pg_rls/templates/db/migrate/backport_pg_rls_table.rb.tt +14 -0
  28. data/lib/generators/pg_rls/templates/db/migrate/convert_to_pg_rls_table.rb.tt +5 -0
  29. data/lib/generators/pg_rls/templates/db/migrate/convert_to_pg_rls_tenant_table.rb.tt +5 -0
  30. data/lib/generators/pg_rls/templates/db/migrate/create_pg_rls_table.rb.tt +29 -0
  31. data/lib/generators/pg_rls/templates/db/migrate/create_pg_rls_tenant_table.rb.tt +29 -0
  32. data/lib/pg_rls/active_record/connection_adapters/connection_pool.rb +31 -0
  33. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/check_rls_user_privileges.rb +207 -0
  34. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/errors.rb +17 -0
  35. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/grant_rls_user_privileges.rb +167 -0
  36. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_functions.rb +91 -0
  37. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_policies.rb +56 -0
  38. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_triggers.rb +95 -0
  39. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_user_statements.rb +127 -0
  40. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/schema_dumper.rb +71 -0
  41. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/schema_statements.rb +120 -0
  42. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/sql_helper_method.rb +30 -0
  43. data/lib/pg_rls/active_record/connection_adapters/postgre_sql.rb +36 -0
  44. data/lib/pg_rls/active_record/connection_adapters.rb +12 -0
  45. data/lib/pg_rls/active_record/database_shards.rb +74 -0
  46. data/lib/pg_rls/active_record/migration/command_recorder.rb +28 -0
  47. data/lib/pg_rls/active_record/migration.rb +11 -0
  48. data/lib/pg_rls/active_record/test_databases.rb +19 -0
  49. data/lib/pg_rls/active_record.rb +11 -0
  50. data/lib/pg_rls/active_support/string_ext.rb +17 -0
  51. data/lib/pg_rls/active_support.rb +9 -0
  52. data/lib/pg_rls/connection_config.rb +61 -0
  53. data/lib/pg_rls/deprecation.rb +14 -0
  54. data/lib/pg_rls/engine.rb +8 -0
  55. data/lib/pg_rls/error.rb +10 -0
  56. data/lib/pg_rls/generators/.keep +0 -0
  57. data/lib/pg_rls/railtie.rb +1 -11
  58. data/lib/pg_rls/tasks/.keep +0 -0
  59. data/lib/pg_rls/version.rb +3 -1
  60. data/lib/pg_rls.rb +67 -151
  61. data/rbs_collection.lock.yaml +132 -0
  62. data/rbs_collection.yaml +127 -0
  63. data/review_code.sh +33 -0
  64. data/sig/generators/pg_rls/active_record/active_record_generator.rbs +43 -0
  65. data/sig/generators/pg_rls/install/install_generator.rbs +20 -0
  66. data/sig/generators/pg_rls/pg_rls_generator.rbs +9 -0
  67. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/check_rls_user_privileges.rbs +53 -0
  68. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/errors.rbs +24 -0
  69. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/grant_rls_user_privileges.rbs +55 -0
  70. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_functions.rbs +31 -0
  71. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_policies.rbs +28 -0
  72. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_triggers.rbs +35 -0
  73. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_user_statements.rbs +48 -0
  74. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/schema_dumper.rbs +38 -0
  75. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/schema_statements.rbs +67 -0
  76. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/sql_helper_method.rbs +21 -0
  77. data/sig/pg_rls/active_record/connection_adapters/postgresql.rbs +10 -0
  78. data/sig/pg_rls/active_record/connection_adapters.rbs +50 -0
  79. data/sig/pg_rls/active_record/database_shards.rbs +34 -0
  80. data/sig/pg_rls/active_record/migration/command_recorder.rbs +14 -0
  81. data/sig/pg_rls/active_record/migration.rbs +8 -0
  82. data/sig/pg_rls/active_record.rbs +7 -0
  83. data/sig/pg_rls/active_support/hash_ext.rbs +11 -0
  84. data/sig/pg_rls/active_support/string_ext.rbs +27 -0
  85. data/sig/pg_rls/active_support.rbs +7 -0
  86. data/sig/pg_rls/app/models/pg_rls/record.rbs +4 -0
  87. data/sig/pg_rls/connection_config.rbs +16 -0
  88. data/sig/pg_rls/deprecation.rbs +9 -0
  89. data/sig/pg_rls/engine.rbs +7 -0
  90. data/sig/pg_rls/errors.rbs +14 -0
  91. data/sig/pg_rls/railtie.rbs +6 -0
  92. data/sig/pg_rls/tenant_test_helper.rbs +14 -0
  93. data/sig/pg_rls.rbs +60 -0
  94. data/sig/support/active_record.rbs +86 -0
  95. data/sig/support/active_support.rbs +7 -0
  96. data/sig/support/fowardable.rbs +2 -0
  97. data/sig/support/pg.rbs +12 -0
  98. data/sig/support/rails.rbs +38 -0
  99. data/start.sh +30 -0
  100. metadata +167 -12
  101. data/Gemfile +0 -21
  102. data/Gemfile.lock +0 -300
  103. data/bin/console +0 -15
  104. data/bin/setup +0 -8
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ module ActiveRecord
5
+ module ConnectionAdapters
6
+ module PostgreSQL
7
+ # This module contains the logic to grant user privileges
8
+ module RlsUserStatements
9
+ include SqlHelperMethod
10
+
11
+ def create_rls_role(name, password)
12
+ create_rls_user(name, password)
13
+ assign_user_to_group(name)
14
+ end
15
+
16
+ def drop_rls_role(name)
17
+ remove_user_from_group(name)
18
+ drop_rls_user(name)
19
+ end
20
+
21
+ def user_exists?(name)
22
+ execute_sql!(user_exists_sql(name)).first.present?
23
+ end
24
+
25
+ def drop_rls_user(name)
26
+ execute_sql!(drop_rls_user_sql(name))
27
+ end
28
+
29
+ def create_rls_user(name, password)
30
+ execute_sql!(create_rls_user_sql(name, password))
31
+ end
32
+
33
+ def create_rls_group(name = PgRls.rls_role_group)
34
+ execute_sql!(create_rls_group_sql(name))
35
+ end
36
+
37
+ def drop_rls_group(name = PgRls.rls_role_group)
38
+ execute_sql!(drop_rls_group_sql(name))
39
+ end
40
+
41
+ def assign_user_to_group(name)
42
+ execute_sql!(assign_user_to_group_sql(name))
43
+ end
44
+
45
+ def remove_user_from_group(name)
46
+ execute_sql!(remove_user_from_group_sql(name))
47
+ end
48
+
49
+ private
50
+
51
+ def user_exists_sql(name)
52
+ <<~SQL
53
+ SELECT 1
54
+ FROM pg_catalog.pg_roles
55
+ WHERE rolname = '#{name}';
56
+ SQL
57
+ end
58
+
59
+ def drop_rls_user_sql(name)
60
+ <<~SQL
61
+ DO $do$ BEGIN
62
+ IF EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '#{name}') THEN
63
+ DROP ROLE #{name};
64
+ END IF; END $do$;
65
+ SQL
66
+ end
67
+
68
+ def create_rls_user_sql(name, password)
69
+ <<~SQL
70
+ DO $do$ BEGIN
71
+ IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '#{name}') THEN
72
+ CREATE ROLE #{name} LOGIN PASSWORD '#{password}';
73
+ END IF;
74
+ END $do$;
75
+ SQL
76
+ end
77
+
78
+ def create_rls_group_sql(role_name)
79
+ <<~SQL
80
+ DO $do$ BEGIN
81
+ IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '#{role_name}') THEN
82
+ CREATE ROLE #{role_name} NOLOGIN;
83
+ END IF;
84
+ END $do$;
85
+ SQL
86
+ end
87
+
88
+ def drop_rls_group_sql(role_name)
89
+ <<~SQL
90
+ DO $do$ BEGIN
91
+ IF EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '#{role_name}') THEN
92
+ DROP ROLE #{role_name};
93
+ END IF;
94
+ END $do$;
95
+ SQL
96
+ end
97
+
98
+ def assign_user_to_group_sql(name)
99
+ <<~SQL
100
+ DO $$ BEGIN IF NOT EXISTS (
101
+ SELECT FROM pg_catalog.pg_auth_members
102
+ JOIN pg_roles AS rls_group ON rls_group.oid = pg_auth_members.roleid
103
+ JOIN pg_roles AS rls_member ON rls_member.oid = pg_auth_members.member
104
+ WHERE rls_group.rolname = 'rls_group' AND rls_member.rolname = '#{name}'
105
+ ) THEN
106
+ GRANT rls_group TO #{name};
107
+ END IF; END $$;
108
+ SQL
109
+ end
110
+
111
+ def remove_user_from_group_sql(name)
112
+ <<~SQL
113
+ DO $$ BEGIN IF EXISTS (
114
+ SELECT FROM pg_catalog.pg_auth_members
115
+ JOIN pg_roles AS rls_group ON rls_group.oid = pg_auth_members.roleid
116
+ JOIN pg_roles AS rls_member ON rls_member.oid = pg_auth_members.member
117
+ WHERE rls_group.rolname = 'rls_group' AND rls_member.rolname = '#{name}'
118
+ ) THEN
119
+ REVOKE rls_group FROM #{name};
120
+ END IF; END $$;
121
+ SQL
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ module ActiveRecord
5
+ module ConnectionAdapters
6
+ module PostgreSQL
7
+ # This module is responsible for changing the `create_table` method to `create_rls_table`
8
+ # when the table is a RLS table.
9
+ module SchemaDumper
10
+ def tables(stream)
11
+ tmp_stream = StringIO.new
12
+ super(tmp_stream)
13
+
14
+ stream_content = tmp_stream.string
15
+
16
+ return stream.puts stream_content if @rls_tenant_table.nil?
17
+
18
+ stream.puts @rls_tenant_table
19
+ stream.puts unless stream_content.nil?
20
+ stream.puts stream_content.chomp
21
+ end
22
+
23
+ def table(table_name, stream)
24
+ original_table_method = method(:table).super_method
25
+ stream_content = dump_table_to_string(original_table_method, table_name)
26
+
27
+ if rls_tenant_table?(table_name)
28
+ @rls_tenant_table = stream_content.gsub!("create_table", "create_rls_tenant_table").to_s
29
+ return
30
+ elsif rls_table?(table_name)
31
+ stream_content.gsub!("create_table", "create_rls_table")
32
+ end
33
+
34
+ stream.print stream_content
35
+ end
36
+
37
+ private
38
+
39
+ def dump_table_to_string(original_table_method, table_name)
40
+ temp_stream = StringIO.new
41
+ original_table_method.call(table_name, temp_stream)
42
+ temp_stream.string
43
+ end
44
+
45
+ def rls_table?(table_name)
46
+ rls_table_array.include?(table_name)
47
+ end
48
+
49
+ def rls_tenant_table?(table_name)
50
+ PgRls.table_name.to_s == table_name.to_s
51
+ end
52
+
53
+ def rls_table_array
54
+ @rls_table_array ||= fetch_rls_tables
55
+ end
56
+
57
+ def fetch_rls_tables
58
+ statement = <<-SQL
59
+ SELECT tablename FROM pg_policies WHERE schemaname = '#{PgRls.schema}';
60
+ SQL
61
+ @connection.execute(statement).to_a.map { |table| table["tablename"] }
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaDumper.prepend(
70
+ PgRls::ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaDumper
71
+ )
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ module ActiveRecord
5
+ module ConnectionAdapters
6
+ module PostgreSQL
7
+ # This module contains the logic to create rls policies tables and functions
8
+ module SchemaStatements
9
+ def create_rls_tenant_table(table_name, ...)
10
+ create_rls_initialize_setup
11
+ create_table(table_name, ...)
12
+ create_rls_tenant_table_setup(table_name)
13
+ end
14
+
15
+ def convert_to_rls_tenant_table(table_name)
16
+ create_rls_initialize_setup
17
+ create_rls_tenant_table_setup(table_name)
18
+ end
19
+
20
+ def create_rls_table(table_name, ...)
21
+ create_table(table_name, ...)
22
+ create_rls_table_setup(table_name) if check_rls_user_privileges!(PgRls.username, PgRls.schema)
23
+ end
24
+
25
+ def convert_to_rls_table(table_name)
26
+ create_rls_table_setup(table_name) if check_rls_user_privileges!(PgRls.username, PgRls.schema)
27
+ end
28
+
29
+ def drop_rls_tenant_table(table_name, ...)
30
+ drop_rls_initialize_setup(table_name)
31
+ drop_table(table_name, ...)
32
+ end
33
+
34
+ def revert_from_rls_tenant_table(table_name)
35
+ drop_rls_initialize_setup(table_name)
36
+ remove_column(table_name, :tenant_id)
37
+ end
38
+
39
+ def drop_rls_table(table_name, ...)
40
+ drop_rls_table_setup(table_name)
41
+ drop_table(table_name, ...)
42
+ end
43
+
44
+ def revert_from_rls_table(table_name)
45
+ drop_rls_table_setup(table_name)
46
+ end
47
+
48
+ def create_rls_index(table_name, columns, **options)
49
+ add_index(table_name, rls_index_columns(columns), **options)
50
+ end
51
+
52
+ def drop_rls_index(table_name, columns, **options)
53
+ remove_index(table_name, rls_index_columns(columns), **options)
54
+ end
55
+
56
+ {
57
+ create_rls_tenant_table: :drop_rls_tenant_table,
58
+ convert_to_rls_tenant_table: :revert_from_rls_tenant_table,
59
+ create_rls_table: :drop_rls_table,
60
+ convert_to_rls_table: :revert_from_rls_table,
61
+ create_rls_index: :drop_rls_index
62
+ }.each do |cmd, inv|
63
+ [[inv, cmd], [cmd, inv]].uniq.each do |method, inverse|
64
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
65
+ def invert_#{method}(args, &block) # def invert_create_table(args, &block)
66
+ [:#{inverse}, args, block] # [:drop_table, args, block]
67
+ end # end
68
+ RUBY
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def create_rls_table_setup(table_name)
75
+ add_column(table_name, :tenant_id, :uuid, null: false) unless column_exists?(table_name, :tenant_id)
76
+ enable_table_rls(table_name, PgRls.username)
77
+ append_rls_table_triggers(table_name)
78
+ end
79
+
80
+ def create_rls_tenant_table_setup(table_name)
81
+ unless column_exists?(table_name, :tenant_id)
82
+ add_column(
83
+ table_name, :tenant_id, :uuid, default: "gen_random_uuid()", null: false
84
+ )
85
+ end
86
+ add_index(table_name, :tenant_id, unique: true) unless index_exists?(table_name, :tenant_id, unique: true)
87
+ append_tenant_table_triggers(table_name)
88
+ end
89
+
90
+ def create_rls_initialize_setup
91
+ create_rls_group
92
+ create_rls_role(PgRls.username, PgRls.password)
93
+ grant_rls_user_privileges(PgRls.schema)
94
+ create_rls_functions
95
+ end
96
+
97
+ def drop_rls_table_setup(table_name)
98
+ drop_rls_table_triggers(table_name)
99
+ disable_table_rls(table_name, PgRls.username)
100
+ remove_column(table_name, :tenant_id, if_exists: true) if table_exists?(table_name)
101
+ end
102
+
103
+ def drop_rls_initialize_setup(table_name)
104
+ drop_tenant_table_triggers(table_name)
105
+ drop_rls_functions
106
+ revoke_rls_user_privileges(PgRls.schema)
107
+ drop_rls_role(PgRls.username)
108
+ drop_rls_group
109
+ end
110
+
111
+ def rls_index_columns(columns)
112
+ cols = Array(columns).map(&:to_sym)
113
+ cols << :tenant_id unless cols.include?(:tenant_id)
114
+ cols
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ module ActiveRecord
5
+ module ConnectionAdapters
6
+ module PostgreSQL
7
+ # This module contains the logic to validate user privileges
8
+ module SqlHelperMethod
9
+ private
10
+
11
+ def execute_sql!(statement)
12
+ ::ActiveRecord::Base.transaction(requires_new: true) do
13
+ execute(statement.sanitize_sql)
14
+ rescue StandardError => e
15
+ raise e unless rescue_sql_error?(e)
16
+
17
+ ::ActiveRecord::Base.connection.rollback_db_transaction
18
+ retry
19
+ end
20
+ end
21
+
22
+ def rescue_sql_error?(error)
23
+ error.message.include?("PG::InFailedSqlTransaction") ||
24
+ error.message.include?("PG::TRDeadlockDetected")
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/connection_adapters/postgresql_adapter"
4
+ require_relative "postgre_sql/errors"
5
+ require_relative "postgre_sql/sql_helper_method"
6
+ require_relative "postgre_sql/rls_functions"
7
+ require_relative "postgre_sql/rls_triggers"
8
+ require_relative "postgre_sql/rls_user_statements"
9
+ require_relative "postgre_sql/check_rls_user_privileges"
10
+ require_relative "postgre_sql/grant_rls_user_privileges"
11
+ require_relative "postgre_sql/rls_policies"
12
+ require_relative "postgre_sql/schema_statements"
13
+ require_relative "postgre_sql/schema_dumper"
14
+
15
+ module PgRls
16
+ module ActiveRecord
17
+ module ConnectionAdapters
18
+ # ActiveRecord PostgreSQL Connection Adapter Extension
19
+ module PostgreSQL
20
+ def self.included(base)
21
+ # Dynamically include all modules into the adapter
22
+ constants.each do |const_name|
23
+ next if const_name == :SchemaDumper
24
+
25
+ mod = const_get(const_name)
26
+ base.include(mod) if mod.is_a?(Module) && !mod.is_a?(Class)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements.include(
35
+ PgRls::ActiveRecord::ConnectionAdapters::PostgreSQL
36
+ )
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "connection_adapters/postgre_sql"
4
+ require_relative "connection_adapters/connection_pool"
5
+
6
+ module PgRls
7
+ module ActiveRecord
8
+ # ActiveRecord Connection Adapter Extension
9
+ module ConnectionAdapters
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ module ActiveRecord
5
+ # Overwrite the configurations method to add the RLS configurations
6
+ module DatabaseShards
7
+ REQUIRED_CONFIGURATION_KEYS = %w[adapter host database username password rls_mode].freeze
8
+
9
+ def add_rls_configurations(config, new_config = {})
10
+ current_config = config[Rails.env]
11
+
12
+ if rls_shard_config?(current_config)
13
+ add_primary_and_rls_config(current_config, new_config)
14
+ else
15
+ current_config.each do |key, value|
16
+ add_primary_and_rls_config(value, new_config, key)
17
+ end
18
+ end
19
+
20
+ { Rails.env => new_config }
21
+ end
22
+
23
+ def configurations=(config)
24
+ new_config = add_rls_configurations(config)
25
+ super(new_config)
26
+ end
27
+
28
+ private
29
+
30
+ def add_primary_and_rls_config(config, new_config, key = "primary")
31
+ new_config[key] = config
32
+
33
+ return new_config unless rls_shard_config?(config)
34
+
35
+ configuration = adapter_configurations(config, new_config, key)
36
+
37
+ if configuration.nil?
38
+ raise ArgumentError,
39
+ "Invalid RLS mode: #{config["rls_mode"]}. valid options are: dual, single, none"
40
+ end
41
+
42
+ new_config
43
+ end
44
+
45
+ def adapter_configurations(config, new_config, key)
46
+ case config["rls_mode"]
47
+ when "dual"
48
+ new_config["rls_#{key}"] = config.merge(rls_configuration)
49
+ when "single"
50
+ new_config[key] = config.merge(rls_configuration)
51
+ when "none"
52
+ new_config[key] = config
53
+ end
54
+ end
55
+
56
+ def rls_configuration
57
+ {
58
+ "username" => PgRls.username.to_s,
59
+ "password" => PgRls.password.to_s,
60
+ "database_tasks" => false,
61
+ "rls" => true
62
+ }
63
+ end
64
+
65
+ def rls_shard_config?(config)
66
+ return false unless config.is_a?(Hash)
67
+
68
+ REQUIRED_CONFIGURATION_KEYS.all? { |key| config.key?(key) }
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ ActiveRecord::Base.singleton_class.prepend(PgRls::ActiveRecord::DatabaseShards)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ module ActiveRecord
5
+ module Migration
6
+ # ActiveRecord Migration Command Recorder Extension
7
+ module CommandRecorder
8
+ REVERSIBLE_AND_IRREVERSIBLE_METHODS = %i[
9
+ create_rls_tenant_table convert_to_rls_tenant_table drop_rls_tenant_table
10
+ create_rls_table convert_to_rls_table drop_rls_table
11
+ ].freeze
12
+
13
+ def self.included(base)
14
+ REVERSIBLE_AND_IRREVERSIBLE_METHODS.each do |method|
15
+ base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
16
+ def #{method}(*args, &block) # def create_table(*args, &block)
17
+ record(:"#{method}", args, &block) # record(:create_table, args, &block)
18
+ end # end
19
+ RUBY
20
+ base.send(:ruby2_keywords, method)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ ActiveRecord::Migration::CommandRecorder.include(PgRls::ActiveRecord::Migration::CommandRecorder)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "migration/command_recorder"
4
+
5
+ module PgRls
6
+ module ActiveRecord
7
+ # ActiveRecord Migration Extension
8
+ module Migration
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ module ActiveRecord
5
+ module TestDatabases # :nodoc:
6
+ def create_and_load_schema(i, env_name:)
7
+ super
8
+
9
+ PgRls::Record.configurations.configs_for(env_name: env_name, include_hidden: true).each do |db_config|
10
+ next if db_config.name == "primary"
11
+
12
+ db_config._database = "#{db_config.database}-#{i}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ ActiveRecord::TestDatabases.singleton_class.prepend(PgRls::ActiveRecord::TestDatabases)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "active_record/migration"
4
+ require_relative "active_record/connection_adapters"
5
+ require_relative "active_record/database_shards"
6
+
7
+ module PgRls
8
+ # ActiveRecord Extension
9
+ module ActiveRecord
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ module ActiveSupport
5
+ # Extensions to the String class
6
+ module StringExt
7
+ def sanitize_sql
8
+ str = dup
9
+ str.gsub!(/[[:space:]]+/, " ")
10
+ str.strip!
11
+ str.to_s
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ String.include(PgRls::ActiveSupport::StringExt)
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "active_support/string_ext"
4
+
5
+ module PgRls
6
+ # Active Support Extension
7
+ module ActiveSupport
8
+ end
9
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ # The ConnectionConfig class provides methods to configure and manage
5
+ # the connection settings for Row Level Security (RLS) in PostgreSQL.
6
+ # It includes methods to look up and validate the connection configuration,
7
+ # as well as helper methods to build the configuration hash based on different modes.
8
+ class ConnectionConfig
9
+ def initialize(db_config = ::ActiveRecord::Base.connection_db_config)
10
+ @db_config = db_config
11
+ @connection_name = db_config.name
12
+ end
13
+
14
+ def look_up_connection_config
15
+ config_hash = build_config_hash(@db_config, @connection_name)
16
+
17
+ return invalid_connection_config unless config_hash
18
+
19
+ PgRls.connects_to = config_hash.deep_transform_values(&:to_sym)
20
+ end
21
+
22
+ def connection_config?
23
+ PgRls.connects_to&.key?(:database) || false
24
+ end
25
+
26
+ def invalid_connection_config
27
+ raise PgRls::Error::InvalidConnectionConfig,
28
+ "you must edit your database.yml file to include the RLS configuration, " \
29
+ "or set the RLS configuration manually in the PgRls initializer"
30
+ end
31
+
32
+ private
33
+
34
+ def build_config_hash(db_config, connection_name)
35
+ case db_config.configuration_hash[:rls_mode]
36
+ when "dual"
37
+ build_dual_mode_config(connection_name)
38
+ when "single", "none"
39
+ build_single_mode_config(connection_name)
40
+ end
41
+ end
42
+
43
+ def build_dual_mode_config(connection_name)
44
+ {
45
+ shards: {
46
+ rls: { writing: "rls_#{connection_name}", reading: "rls_#{connection_name}" },
47
+ admin: { writing: connection_name, reading: connection_name }
48
+ }
49
+ }
50
+ end
51
+
52
+ def build_single_mode_config(connection_name)
53
+ {
54
+ database: {
55
+ writing: connection_name,
56
+ reading: connection_name
57
+ }
58
+ }
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ # Deprecator Module
5
+ module Deprecation
6
+ def self.warn(message)
7
+ logger.warn(message)
8
+ end
9
+
10
+ def self.logger
11
+ @logger ||= ::ActiveSupport::Deprecation.new(PgRls::VERSION, "PgRls")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ # Engine
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace PgRls
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ # Main PgRls Error Class
5
+ class Error < StandardError
6
+ class InvalidConnectionConfig < Error; end
7
+ class InvalidSearchInput < Error; end
8
+ class TenantNotFound < Error; end
9
+ end
10
+ end
File without changes