Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## next / unreleased

### Added

- Add `config.active_record_tenanted.allow_untenanted_active_storage` configuration option to enable ActiveStorage without a tenant context.
When enabled, ActiveStorage will fallback to default (non-tenanted) behavior when there is no current tenant,
allowing models in the primary database to use Active Storage alongside tenanted models.
Defaults to `false` for backward compatibility. @keshavbiswa

### Fixed

- `.current_tenant = nil` now clears the tenant context, properly setting the shard to `UNTENANTED_SENTINEL` instead of `""` @flavorjones
Expand Down
12 changes: 12 additions & 0 deletions lib/active_record/tenanted/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ class Railtie < ::Rails::Railtie
# Defaults to "development-tenant" in development and "test-tenant" in test environments.
config.active_record_tenanted.default_tenant = Rails.env.local? ? "#{Rails.env}-tenant" : nil

# Set this to true to allow Active Storage to work without a tenant context.
#
# When enabled, Active Storage will fall back to default behavior (non-tenanted storage)
# when there is no current tenant. This allows models in the primary database to use
# Active Storage alongside tenanted models.
#
# When disabled (default), Active Storage will raise NoTenantError if accessed without
# a tenant context, maintaining strict tenant isolation.
#
# Defaults to `false`.
config.active_record_tenanted.allow_untenanted_active_storage = false

config.before_configuration do
ActiveSupport.on_load(:active_record_database_configurations) do
ActiveRecord::Tenanted::DatabaseConfigurations.register_db_config_handler
Expand Down
26 changes: 18 additions & 8 deletions lib/active_record/tenanted/storage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ module Storage # :nodoc:
module DiskService
def root
if klass = ActiveRecord::Tenanted.connection_class
unless tenant = klass.current_tenant
tenant = klass.current_tenant
allow_untenanted = Rails.application.config.active_record_tenanted.allow_untenanted_active_storage

if tenant.nil?
return super if allow_untenanted

raise NoTenantError, "Cannot access Active Storage Disk service without a tenant"
end

Expand All @@ -18,12 +23,12 @@ def root

def path_for(key)
if ActiveRecord::Tenanted.connection_class
# TODO: this is brittle if the key isn't tenanted ... errors in folder_for:
#
# NoMethodError undefined method '[]' for nil (NoMethodError) [ key[0..1], key[2..3] ].join("/")
#
tenant, key = key.split("/", 2)
File.join(root, tenant, folder_for(key), key)
if key.include?("/")
tenant, key = key.split("/", 2)
File.join(root, tenant, folder_for(key), key)
else
super
end
else
super
end
Expand All @@ -33,7 +38,12 @@ def path_for(key)
module Blob
def key
self[:key] ||= if klass = ActiveRecord::Tenanted.connection_class
unless tenant = klass.current_tenant
tenant = klass.current_tenant
allow_untenanted = Rails.application.config.active_record_tenanted.allow_untenanted_active_storage

if tenant.nil?
return super if allow_untenanted

raise NoTenantError, "Cannot generate a Blob key without a tenant"
end

Expand Down
86 changes: 86 additions & 0 deletions test/unit/storage_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@

describe ActiveRecord::Tenanted::Storage do
describe "DiskService" do
let(:allow_untenanted_active_storage) { false }

setup do
@old_allow_untenanted = Rails.application.config.active_record_tenanted.allow_untenanted_active_storage
Rails.application.config.active_record_tenanted.allow_untenanted_active_storage = allow_untenanted_active_storage
end

teardown do
Rails.application.config.active_record_tenanted.allow_untenanted_active_storage = @old_allow_untenanted
end

describe ".root" do
with_active_storage do
let(:service) { ActiveStorage::Service::DiskService.new(root: root_path) }
Expand All @@ -26,6 +37,24 @@
end
end
end

describe "with allow_untenanted_active_storage enabled" do
let(:allow_untenanted_active_storage) { true }

test "allows access without tenant" do
ActiveRecord::Tenanted.stub(:connection_class, TenantedApplicationRecord) do
assert_equal("/path/to/%{tenant}/storage", service.root)
end
end

test "uses tenant when available" do
ActiveRecord::Tenanted.stub(:connection_class, TenantedApplicationRecord) do
TenantedApplicationRecord.create_tenant("foo") do
assert_equal("/path/to/foo/storage", service.root)
end
end
end
end
end

describe "with a non-tenanted root path" do
Expand All @@ -47,6 +76,55 @@
end
end
end

describe "with allow_untenanted_active_storage enabled" do
let(:allow_untenanted_active_storage) { true }
let(:root_path) { "/path/to/storage" }

test "allows access while untenanted" do
ActiveRecord::Tenanted.stub(:connection_class, TenantedApplicationRecord) do
assert_equal("/path/to/storage", service.root)
end
end

test "uses current tenant while tenanted" do
ActiveRecord::Tenanted.stub(:connection_class, TenantedApplicationRecord) do
TenantedApplicationRecord.create_tenant("foo") do
assert_equal("/path/to/storage", service.root)
end
end
end
end
end
end

describe ".path_for" do
with_active_storage do
let(:service) { ActiveStorage::Service::DiskService.new(root: "/path/to/storage") }

describe "with allow_untenanted_active_storage enabled" do
let(:allow_untenanted_active_storage) { true }

test "handles non-tenanted keys" do
ActiveRecord::Tenanted.stub(:connection_class, TenantedApplicationRecord) do
non_tenanted_key = "abc123"

expected_path = "/path/to/storage/ab/c1/#{non_tenanted_key}"
assert_equal expected_path, service.path_for(non_tenanted_key)
end
end
end

test "handles tenanted keys" do
ActiveRecord::Tenanted.stub(:connection_class, TenantedApplicationRecord) do
TenantedApplicationRecord.create_tenant("foo") do
tenanted_key = "foo/abc123"

expected_path = "/path/to/storage/foo/ab/c1/abc123"
assert_equal expected_path, service.path_for(tenanted_key)
end
end
end
end
end
end
Expand Down Expand Up @@ -85,6 +163,14 @@
end
end
end

test "raises exception without tenant when flag is disabled" do
ActiveRecord::Tenanted.stub(:connection_class, TenantedApplicationRecord) do
assert_raises(ActiveRecord::Tenanted::NoTenantError) do
ActiveStorage::Blob.new(filename: "foo.jpg", byte_size: 100, checksum: "abc123", service_name: service_name).key
end
end
end
end
end
end