-
Notifications
You must be signed in to change notification settings - Fork 21
Description
Environment:
- Rails: edge (8.2.0.alpha)
- activerecord-tenanted: 0.5.0
- Devise: 5.0.0
- Architecture: Database-per-tenant with subdomain-based tenancy (one primary db, and many secondary (tenanted DBs)
Context:
I'm using this gem to tenant my application and I use devise to authenticate. I have a rack middleware that I insert after ActiveRecord::Tenanted::TenantSelector that I use to set Current.organization
config/initializer.organization_resolver.rb
Rails.application.config.middleware.insert_after ActiveRecord::Tenanted::TenantSelector, OrganizationResolverlib/organization_resolver.rb
# frozen_string_literal: true
class OrganizationResolver
def initialize(app)
@app = app
end
def call(env)
tenant = ApplicationRecord.current_tenant
if tenant.present?
organization = Organization.find_by(subdomain: tenant)
Current.organization = organization if organization
end
@app.call(env)
end
endProblem:
When I sign in on my tenanted app, everything works as expected. However, if my sign in fails (incorrect password, email,etc) . ActiveRecord::Tenanted::NoTenantError is raised because both current_tenant and Current.organization is nil
Here is how you can reproduce this issue:
- Setup Devise and activerecord-tenanted
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
primary:
<<: *default
database: storage/dummy_development.sqlite3
secondary:
<<: *default
database: "storage/tenants/%{tenant}/main.sqlite3"
tenanted: true
cache:
<<: *default
database: storage/dummy_development_cache.sqlite3
migrations_paths: db/cache_migrate
queue:
<<: *default
database: storage/dummy_development_queue.sqlite3
migrations_paths: db/queue_migrate
cable:
<<: *default
database: storage/dummy_development_cable.sqlite3
migrations_paths: db/cable_migrate- Setup authentication
- Submit wrong password
- Controller tries to render → any query triggers NoTenantError
I think the reason why this happens is because Devise::FailureApp is a Rack app that bypasses the middleware stack:
module Devise
class FailureApp < ActionController::Metal
def self.call(env)
@respond ||= action(:respond)
@respond.call(env) # Direct Rack call - TenantSelector never runs!
end
def respond
# Tries to render/redirect WITHOUT tenant context
end
end
endSo the flow becomes like this:
- User submits wrong password
- Warden invokes the failure app directly as a Rack endpoint
- The Rails middleware stack (
TenantSelector→OrganizationResolver) is not executed - Both
ApplicationRecord.current_tenantandCurrent.organizationremain nil - Any database query in the sign-in view triggers the error
I have fixed this solution by patching DeviseFailureApp by patching the FailureApp
# frozen_string_literal: true
class CustomDeviseFailureApp < Devise::FailureApp
def respond
subdomain = request.subdomain
if subdomain.present? && !admin_subdomain?
ApplicationRecord.with_tenant(subdomain) do
organization = Organization.find_by(subdomain: subdomain)
Current.organization = organization if organization
super
end
else
super
end
end
private
def admin_subdomain?
request.subdomain == "admin"
end
endMy Questions:
- Is this a known behaviour? Should users of activerecord-tenanted + Devise always create a custom
FailureApplike this? - Is there a better pattern? Am I missing a capability that would handle this more elegantly?
- If this is the expected approach, documenting it in the gem's README would save users significant debugging time, especially since Devise is extremely common in Rails apps.
- We may need documentation on handling Rack middleware that bypasses the Rails stack