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.
- checksums.yaml +4 -4
- data/.rubocop.yml +55 -17
- data/.ruby-version +1 -0
- data/CHANGELOG.md +19 -2
- data/CODE_OF_CONDUCT.md +77 -29
- data/Guardfile +44 -0
- data/README.md +247 -83
- data/Rakefile +5 -12
- data/Steepfile +29 -0
- data/UPGRADE.md +106 -0
- data/app/models/pg_rls/admin.rb +24 -0
- data/app/models/pg_rls/current.rb +48 -0
- data/app/models/pg_rls/record.rb +13 -0
- data/app/models/pg_rls/tenant/searchable.rb +60 -0
- data/app/models/pg_rls/tenant/securable.rb +67 -0
- data/app/models/pg_rls/tenant/switchable.rb +40 -0
- data/app/models/pg_rls/tenant.rb +9 -0
- data/assets/logo.svg +8 -0
- data/docker-compose.yml +14 -0
- data/lib/generators/pg_rls/active_record/active_record_generator.rb +62 -65
- data/lib/generators/pg_rls/install/install_generator.rb +38 -0
- data/lib/generators/pg_rls/pg_rls_generator.rb +2 -1
- data/lib/generators/pg_rls/templates/USAGE +28 -0
- data/lib/generators/pg_rls/templates/app/models/abstract_base_class.rb.tt +7 -0
- data/lib/generators/pg_rls/templates/app/models/model.rb.tt +22 -0
- data/lib/generators/pg_rls/templates/config/initializers/pg_rls.rb.tt +58 -0
- data/lib/generators/pg_rls/templates/db/migrate/backport_pg_rls_table.rb.tt +14 -0
- data/lib/generators/pg_rls/templates/db/migrate/convert_to_pg_rls_table.rb.tt +5 -0
- data/lib/generators/pg_rls/templates/db/migrate/convert_to_pg_rls_tenant_table.rb.tt +5 -0
- data/lib/generators/pg_rls/templates/db/migrate/create_pg_rls_table.rb.tt +29 -0
- data/lib/generators/pg_rls/templates/db/migrate/create_pg_rls_tenant_table.rb.tt +29 -0
- data/lib/pg_rls/active_record/connection_adapters/connection_pool.rb +31 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/check_rls_user_privileges.rb +207 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/errors.rb +17 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/grant_rls_user_privileges.rb +167 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_functions.rb +91 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_policies.rb +56 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_triggers.rb +95 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_user_statements.rb +127 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/schema_dumper.rb +71 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/schema_statements.rb +120 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/sql_helper_method.rb +30 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql.rb +36 -0
- data/lib/pg_rls/active_record/connection_adapters.rb +12 -0
- data/lib/pg_rls/active_record/database_shards.rb +74 -0
- data/lib/pg_rls/active_record/migration/command_recorder.rb +28 -0
- data/lib/pg_rls/active_record/migration.rb +11 -0
- data/lib/pg_rls/active_record/test_databases.rb +19 -0
- data/lib/pg_rls/active_record.rb +11 -0
- data/lib/pg_rls/active_support/string_ext.rb +17 -0
- data/lib/pg_rls/active_support.rb +9 -0
- data/lib/pg_rls/connection_config.rb +61 -0
- data/lib/pg_rls/deprecation.rb +14 -0
- data/lib/pg_rls/engine.rb +8 -0
- data/lib/pg_rls/error.rb +10 -0
- data/lib/pg_rls/generators/.keep +0 -0
- data/lib/pg_rls/railtie.rb +1 -11
- data/lib/pg_rls/tasks/.keep +0 -0
- data/lib/pg_rls/version.rb +3 -1
- data/lib/pg_rls.rb +67 -151
- data/rbs_collection.lock.yaml +132 -0
- data/rbs_collection.yaml +127 -0
- data/review_code.sh +33 -0
- data/sig/generators/pg_rls/active_record/active_record_generator.rbs +43 -0
- data/sig/generators/pg_rls/install/install_generator.rbs +20 -0
- data/sig/generators/pg_rls/pg_rls_generator.rbs +9 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/check_rls_user_privileges.rbs +53 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/errors.rbs +24 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/grant_rls_user_privileges.rbs +55 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_functions.rbs +31 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_policies.rbs +28 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_triggers.rbs +35 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_user_statements.rbs +48 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/schema_dumper.rbs +38 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/schema_statements.rbs +67 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/sql_helper_method.rbs +21 -0
- data/sig/pg_rls/active_record/connection_adapters/postgresql.rbs +10 -0
- data/sig/pg_rls/active_record/connection_adapters.rbs +50 -0
- data/sig/pg_rls/active_record/database_shards.rbs +34 -0
- data/sig/pg_rls/active_record/migration/command_recorder.rbs +14 -0
- data/sig/pg_rls/active_record/migration.rbs +8 -0
- data/sig/pg_rls/active_record.rbs +7 -0
- data/sig/pg_rls/active_support/hash_ext.rbs +11 -0
- data/sig/pg_rls/active_support/string_ext.rbs +27 -0
- data/sig/pg_rls/active_support.rbs +7 -0
- data/sig/pg_rls/app/models/pg_rls/record.rbs +4 -0
- data/sig/pg_rls/connection_config.rbs +16 -0
- data/sig/pg_rls/deprecation.rbs +9 -0
- data/sig/pg_rls/engine.rbs +7 -0
- data/sig/pg_rls/errors.rbs +14 -0
- data/sig/pg_rls/railtie.rbs +6 -0
- data/sig/pg_rls/tenant_test_helper.rbs +14 -0
- data/sig/pg_rls.rbs +60 -0
- data/sig/support/active_record.rbs +86 -0
- data/sig/support/active_support.rbs +7 -0
- data/sig/support/fowardable.rbs +2 -0
- data/sig/support/pg.rbs +12 -0
- data/sig/support/rails.rbs +38 -0
- data/start.sh +30 -0
- metadata +167 -12
- data/Gemfile +0 -21
- data/Gemfile.lock +0 -300
- data/bin/console +0 -15
- 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,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,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
|
data/lib/pg_rls/error.rb
ADDED
File without changes
|