rls_multi_tenant 0.3.0 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +15 -0
- data/CHANGELOG.md +32 -0
- data/lib/rls_multi_tenant/concerns/tenant_context.rb +12 -1
- data/lib/rls_multi_tenant/security_validator.rb +8 -18
- data/lib/rls_multi_tenant/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4e6d7f69028c0e956fc4d5caf7651dd885c4a9da43bed16a2d32e90c8177c7c9
|
|
4
|
+
data.tar.gz: 5c8d3042cf29d8a21aad002e16db1193d2d0e8b7fea7246e4766116e81d301f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c35648de09e3f93962337d09fa699edc696a822924e19b046c1a2d6ae47663f4759f8e4c0541f1001c71f11f3b975827a7048e685082838f184c6f2becd33d9c
|
|
7
|
+
data.tar.gz: f7148ffd7576d6cc20ca513cfa92c6d21c9890dbbcacd9fcd9c42b755deba2ee584d0de8c88e644d019370944451d4797e2cba72e1c09e89866b3b33a779ff6d
|
data/.rubocop.yml
CHANGED
|
@@ -36,6 +36,10 @@ Metrics/BlockLength:
|
|
|
36
36
|
Metrics/MethodLength:
|
|
37
37
|
Max: 25
|
|
38
38
|
|
|
39
|
+
Metrics/ModuleLength:
|
|
40
|
+
Exclude:
|
|
41
|
+
- 'spec/**/*'
|
|
42
|
+
|
|
39
43
|
RSpec/ExampleLength:
|
|
40
44
|
Max: 20
|
|
41
45
|
|
|
@@ -51,5 +55,16 @@ RSpec/DescribeClass:
|
|
|
51
55
|
RSpec/DescribeMethod:
|
|
52
56
|
Enabled: false
|
|
53
57
|
|
|
58
|
+
# The integration suite uses plain ActiveRecord models (no ApplicationRecord)
|
|
59
|
+
# and scenario-named spec files rather than one-file-per-class.
|
|
60
|
+
Rails/ApplicationRecord:
|
|
61
|
+
Exclude:
|
|
62
|
+
- 'spec/**/*'
|
|
63
|
+
|
|
64
|
+
RSpec/SpecFilePathFormat:
|
|
65
|
+
Exclude:
|
|
66
|
+
- 'spec/integration/**/*'
|
|
67
|
+
- 'spec/generators/**/*'
|
|
68
|
+
|
|
54
69
|
Gemspec/DevelopmentDependencies:
|
|
55
70
|
Enabled: false
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.1] - 2026-06-04
|
|
9
|
+
|
|
10
|
+
Corrects two bugs in the 0.3.0 security work, both uncovered by a new
|
|
11
|
+
real-PostgreSQL integration suite.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Security validator no longer rejects valid roles.** 0.3.0 used `pg_has_role`
|
|
16
|
+
to flag BYPASSRLS inherited through role membership, but PostgreSQL does not
|
|
17
|
+
inherit the SUPERUSER or BYPASSRLS role attributes through membership — only
|
|
18
|
+
privileges (GRANTs). A role that merely belongs to a BYPASSRLS group does not
|
|
19
|
+
bypass RLS unless it explicitly `SET ROLE`s to it. The validator now checks
|
|
20
|
+
only `current_user`'s own `rolsuper` / `rolbypassrls`, which is what actually
|
|
21
|
+
governs RLS enforcement. Verified empirically against PostgreSQL 16.
|
|
22
|
+
- **Tenant context restore no longer masks an aborted-transaction error.** When
|
|
23
|
+
a `switch` block hit a database error that aborts the transaction (e.g. a
|
|
24
|
+
WITH CHECK policy violation), the ensure clause ran `SET LOCAL` to restore the
|
|
25
|
+
previous tenant and raised `PG::InFailedSqlTransaction`, hiding the original
|
|
26
|
+
error. The restore now ignores `ActiveRecord::StatementInvalid`; the rollback
|
|
27
|
+
clears the `SET LOCAL` anyway.
|
|
28
|
+
|
|
29
|
+
### Internal
|
|
30
|
+
|
|
31
|
+
- Added a real-PostgreSQL integration suite (provisions privileged/unprivileged
|
|
32
|
+
roles, schema and the RLS policy) covering tenant isolation, WITH CHECK
|
|
33
|
+
enforcement, context lifecycle and the security validator; the suite
|
|
34
|
+
auto-skips when no database is reachable.
|
|
35
|
+
- Replaced mocked-only and tautological specs with behavior-driven unit specs,
|
|
36
|
+
real generator invocations, SimpleCov coverage, per-example configuration
|
|
37
|
+
isolation, randomized order, and a CI Ruby×Rails matrix with a Postgres
|
|
38
|
+
service.
|
|
39
|
+
|
|
8
40
|
## [0.3.0] - 2026-06-04
|
|
9
41
|
|
|
10
42
|
Security-focused release. Hardens tenant isolation and SQL handling.
|
|
@@ -42,7 +42,7 @@ module RlsMultiTenant
|
|
|
42
42
|
begin
|
|
43
43
|
yield
|
|
44
44
|
ensure
|
|
45
|
-
|
|
45
|
+
restore_tenant_context(previous_tenant_id)
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -81,6 +81,17 @@ module RlsMultiTenant
|
|
|
81
81
|
|
|
82
82
|
private
|
|
83
83
|
|
|
84
|
+
# Restore the previous context on the way out of a `switch` block.
|
|
85
|
+
# If the block aborted the transaction (e.g. a policy violation), any
|
|
86
|
+
# further statement raises until rollback; that rollback will clear the
|
|
87
|
+
# SET LOCAL anyway, so the failure is safe to ignore and we must not let
|
|
88
|
+
# it mask the original error escaping the block.
|
|
89
|
+
def restore_tenant_context(previous_tenant_id)
|
|
90
|
+
apply_tenant_context(previous_tenant_id, local: true)
|
|
91
|
+
rescue ActiveRecord::StatementInvalid
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
84
95
|
# Apply (or clear, when tenant_id is blank) the tenant context.
|
|
85
96
|
# local: true uses SET LOCAL (transaction-scoped); false uses session SET.
|
|
86
97
|
def apply_tenant_context(tenant_id, local:)
|
|
@@ -13,7 +13,7 @@ module RlsMultiTenant
|
|
|
13
13
|
username = role && role['username']
|
|
14
14
|
|
|
15
15
|
ensure_not_superuser!(role, username)
|
|
16
|
-
ensure_no_bypassrls!(username)
|
|
16
|
+
ensure_no_bypassrls!(role, username)
|
|
17
17
|
|
|
18
18
|
Rails.logger&.info "✅ RLS Multi-tenant security check passed: Using user '#{username}' " \
|
|
19
19
|
'without SUPERUSER or BYPASSRLS privileges'
|
|
@@ -25,8 +25,10 @@ module RlsMultiTenant
|
|
|
25
25
|
|
|
26
26
|
private
|
|
27
27
|
|
|
28
|
-
# Inspect the actual connected role (current_user)
|
|
29
|
-
#
|
|
28
|
+
# Inspect the actual connected role (current_user). Only this role's own
|
|
29
|
+
# attributes determine whether RLS is enforced: PostgreSQL does NOT
|
|
30
|
+
# inherit SUPERUSER or BYPASSRLS through role membership, so checking the
|
|
31
|
+
# current role is both correct and sufficient. Using current_user also
|
|
30
32
|
# avoids interpolating a config value into SQL.
|
|
31
33
|
def current_role
|
|
32
34
|
ActiveRecord::Base.connection.execute(<<~SQL.squish).first
|
|
@@ -45,26 +47,14 @@ module RlsMultiTenant
|
|
|
45
47
|
'non-privileged, non-superuser role for RLS Multi-tenant.'
|
|
46
48
|
end
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def ensure_no_bypassrls!(username)
|
|
51
|
-
return unless bypassrls_effective?
|
|
50
|
+
def ensure_no_bypassrls!(role, username)
|
|
51
|
+
return unless truthy?(role && role['rolbypassrls'])
|
|
52
52
|
|
|
53
|
-
raise SecurityError, "Database user '#{username}' has BYPASSRLS privilege " \
|
|
54
|
-
'(directly or through an inherited role). ' \
|
|
53
|
+
raise SecurityError, "Database user '#{username}' has BYPASSRLS privilege. " \
|
|
55
54
|
'In order to use RLS Multi-tenant, you must use a non-privileged user ' \
|
|
56
55
|
'without BYPASSRLS privilege.'
|
|
57
56
|
end
|
|
58
57
|
|
|
59
|
-
def bypassrls_effective?
|
|
60
|
-
result = ActiveRecord::Base.connection.execute(<<~SQL.squish).first
|
|
61
|
-
SELECT bool_or(rolbypassrls) AS bypass
|
|
62
|
-
FROM pg_roles WHERE pg_has_role(current_user, oid, 'USAGE')
|
|
63
|
-
SQL
|
|
64
|
-
|
|
65
|
-
truthy?(result && result['bypass'])
|
|
66
|
-
end
|
|
67
|
-
|
|
68
58
|
# PostgreSQL boolean columns may come back as true/false or 't'/'f'
|
|
69
59
|
# depending on the adapter/decoder configuration.
|
|
70
60
|
def truthy?(value)
|