Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

restore tenant_id enforcement in modern rails #3674

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

akostadinov
Copy link
Contributor

needs tests before merging and also we should consider whether this is needed at all at this point

fixes THREESCALE-10686

@jlledom
Copy link
Contributor

jlledom commented Jan 23, 2024

It's probably worth keeping it because we had related incidents in the past that could have been detected earlier if this check would have worked.

@github-actions github-actions bot added the Stale label Feb 7, 2024
@github-actions github-actions bot closed this Feb 15, 2024
@akostadinov akostadinov removed the Stale label Feb 15, 2024
@3scale 3scale deleted a comment from github-actions bot Feb 15, 2024
@akostadinov akostadinov reopened this Feb 15, 2024
@github-actions github-actions bot added the Stale label Mar 17, 2024
@3scale 3scale deleted a comment from github-actions bot Mar 17, 2024
@akostadinov akostadinov removed the Stale label Mar 17, 2024
@github-actions github-actions bot added the Stale label Apr 17, 2024
@3scale 3scale deleted a comment from github-actions bot Apr 17, 2024
@akostadinov akostadinov removed the Stale label Apr 17, 2024
@github-actions github-actions bot added the Stale label May 18, 2024
@3scale 3scale deleted a comment from github-actions bot May 20, 2024
@akostadinov akostadinov removed the Stale label May 20, 2024
Copy link
Contributor

@jlledom jlledom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to understand this, but I need some help:

  1. On startup, this module is initialized from
    config.middleware.use ThreeScale::Middleware::Multitenant, :tenant_id
  2. Then, the EnforceTenant module is included in all models (I think it would be more correct to use ApplicationRecord than ActiveRecord::Base)
    def initialize(app, attribute)
    @app = app
    @attribute = attribute
    ActiveRecord::Base.send(:include, EnforceTenant)
    end
  3. Every time a new model is created, :enforce_tenant! is called. I assume Thread.current[:multitenant] will always have the correct value here. Then it calls verify!
  4. The module name EnforceTenant makes me think the purpose is to ensure :tenant_id will always have a value, but from the code at verify! it seems to me it only checks the value doesn't change. So this assumes :tenant_id is set somewhere else and this only makes sure it never changes. Am I right?
  5. In this snippet,
    begin
    current = object.send(attribute)
    rescue ActiveRecord::MissingAttributeError
    # Multitenant.log("#{object} is missing #{attribute}. Reloading and trying again")
    fresh = object.class.send(:with_exclusive_scope) { object.class.find(object.id, :select => attribute) }
    current = fresh.send(attribute)
    end
    why don't we rescue ActiveRecord::MissingAttributeError when calling current = fresh.send(attribute)? Is it a bug?

If the purpose is to make sure :tenant_id doesn't change, couldn't this be done in a much easier way? e.g. I found this:

https://discuss.rubyonrails.org/t/validates-immutable-true/81479/2

lib/three_scale/middleware/multitenant.rb Show resolved Hide resolved
Comment on lines +88 to +89
extend ActiveSupport::Concern

included do
after_initialize :enforce_tenant!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better

@akostadinov
Copy link
Contributor Author

How did you conclude that code checkd tenant_id never changed? My limited understanding is that it should have same tenant on all objects. But I'm not yet at there. Just trying to restore functionality. And I'm stuck currently getting access to the session store.

@jlledom
Copy link
Contributor

jlledom commented May 31, 2024

How did you conclude that code checkd tenant_id never changed?

Maybe I'm wrong because I don't understand the code. I guessed that from this code:

return if current == original
# we still need to check if it's master before raising a tenant leak
@cookie_store ||= find_cookie_store(@app)
@session ||= @cookie_store.send(:load_session, @env)
@master ||= Account.find_by_sql(["SELECT * FROM accounts WHERE master = ?", true]).first
if (user_id = @session.last[:user_id].presence)
@users ||= {}
@users[user_id] ||= User.find_by_sql(["SELECT * FROM users WHERE id = ? AND account_id = ?", user_id, @master.id]).present?
return if @users[user_id]
end
return if @env["action_controller.request.query_parameters"]["provider_key"] == @master.api_key
raise TenantLeak.new(object, attribute, original)

If :tenant_id it's the same, return, otherwise raise

@akostadinov akostadinov changed the title [WIP] restore tenant_id enforcement in modern rails restore tenant_id enforcement in modern rails Jun 20, 2024
Comment on lines 64 to 76
if (user_id = @session.last[:user_id].presence)
@users ||= {}
@users[user_id] ||= User.find_by_sql(["SELECT * FROM users WHERE id = ? AND account_id = ?", user_id, @master.id]).present?
return if @users[user_id]
end
# this is supposed to match how we get the user_session in app/lib/authenticated_system/request.rb
# on API calls cookies are not present though, so we need to use safe navigation
@user_session ||= UserSession.authenticate(@env['action_dispatch.cookies']&.signed&.public_send(:[], :user_session))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting question here is why originally we kept user_id in a hash. As if we may observe multiple user_ids within the same request. Which doesn't make sense to me. Any ideas?

@akostadinov akostadinov requested a review from jlledom June 20, 2024 17:56
@akostadinov
Copy link
Contributor Author

  1. On startup, this module is initialized from
    config.middleware.use ThreeScale::Middleware::Multitenant, :tenant_id
  2. Then, the EnforceTenant module is included in all models (I think it would be more correct to use ApplicationRecord than ActiveRecord::Base)
    def initialize(app, attribute)
    @app = app
    @attribute = attribute
    ActiveRecord::Base.send(:include, EnforceTenant)
    end
  3. Every time a new model is created, :enforce_tenant! is called. I assume Thread.current[:multitenant] will always have the correct value here. Then it calls verify!

This is set once for each request here:

Thread.current[:multitenant] = TenantChecker.new(@attribute,@app,env)

  1. The module name EnforceTenant makes me think the purpose is to ensure :tenant_id will always have a value, but from the code at verify! it seems to me it only checks the value doesn't change. So this assumes :tenant_id is set somewhere else and this only makes sure it never changes. Am I right?

The purpose is to make sure that a user only sees objects from its own tenant. The implementation seems to be that first initialized object with a tenant_id would set the value. Then if any other object has a different tenant_id, the request will fail. I think this should be good enough because we should always initialize a User object so if any object has a different tenant_id, it will be detected.

  1. In this snippet,
    begin
    current = object.send(attribute)
    rescue ActiveRecord::MissingAttributeError
    # Multitenant.log("#{object} is missing #{attribute}. Reloading and trying again")
    fresh = object.class.send(:with_exclusive_scope) { object.class.find(object.id, :select => attribute) }
    current = fresh.send(attribute)
    end
    why don't we rescue ActiveRecord::MissingAttributeError when calling current = fresh.send(attribute)? Is it a bug?

I don't think it is a bug. Because we retrieve the necessary attribute when we obtain the object. That's the point of the rescue clause to begin with. Makes sense?

If the purpose is to make sure :tenant_id doesn't change, couldn't this be done in a much easier way? e.g. I found this:

https://discuss.rubyonrails.org/t/validates-immutable-true/81479/2

I need more elaboration to understand how this applies.

@akostadinov akostadinov marked this pull request as draft June 20, 2024 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants