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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8fab0710fc915514adcd0f8ba409b24557b5cafe0f1f0bdc4084b24d25b2fba5
4
- data.tar.gz: 6359b5afdeb5493f4e56482682d6e82ff4a9e6aafd36adbfdf8998630b715525
3
+ metadata.gz: 4e6d7f69028c0e956fc4d5caf7651dd885c4a9da43bed16a2d32e90c8177c7c9
4
+ data.tar.gz: 5c8d3042cf29d8a21aad002e16db1193d2d0e8b7fea7246e4766116e81d301f9
5
5
  SHA512:
6
- metadata.gz: a5079b1e2b6d73d30197ffd65042468301bc80afb36a573f2f6edcbf9663a320dca71354828f839bdbcac1d35ee72fcc70bd060ef9ee379ffe028740f99ac2f7
7
- data.tar.gz: 89af2331b8b0f91f844e2a994e40f7d5162ea739a744d41a0e3d02675b5ca89a5c3d1f53f79b6ab0f8ab07e17d229893b937bb4fb2c2c2d4e2499ee504788dd5
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
- apply_tenant_context(previous_tenant_id, local: true)
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), not the configured
29
- # username, so the check reflects what PostgreSQL really enforces and
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
- # BYPASSRLS can also be inherited through role membership, so check the
49
- # effective privilege across every role the user is a member of.
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RlsMultiTenant
4
- VERSION = '0.3.0'
4
+ VERSION = '0.3.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rls_multi_tenant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Coding Ways