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

add email_scope option for email uniqueness validator #5094

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

flynfish
Copy link

@flynfish flynfish commented Jul 9, 2019

This is a reboot of the closed PR #4793 with some tests. Issue #4767 has a lot of information about why this would be useful.

Description from #4793

@ACPK
Copy link

ACPK commented Aug 31, 2019

@flynfish @tegon - Do you know the status of this? I'd like to implement Device for a project that uses subdomains.

@flynfish
Copy link
Author

flynfish commented Sep 5, 2019

@ACPK I haven't heard anything else from @tegon yet. We are using my fork at the moment until this can hopefully be merged.

@tegon
Copy link
Member

tegon commented Sep 5, 2019

Sorry guys, I wasn't able to look at this yet, but I'll definitely take a look as soon as I can.

@christianrolle
Copy link

@tegon scoping the email makes totally sense in a multi tenant scenario.
We're gonna fork it with that important enhancement as well. Hope we're able to come back to trunk soon.

christianrolle added a commit to christianrolle/devise that referenced this pull request Sep 13, 2019
For multi tenancy the email validation scoping is
crucial. This enhancement is actually based on the
currently open PR:
heartcombo#5094
As soon as the feature is merged, one should use the
Devise trunk again.
@bananatron
Copy link

Scoping email validations is HUGE - thanks so much for this!

@@ -7,7 +7,7 @@ module SharedUser
devise :database_authenticatable, :confirmable, :lockable, :recoverable,
:registerable, :rememberable, :timeoutable,
:trackable, :validatable, :omniauthable, password_length: 7..72,
reconfirmable: false
reconfirmable: false, email_scope: [:username]
Copy link
Member

Choose a reason for hiding this comment

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

I would rather create another test model with this option so that we can keep both behaviors being tested. Something similar to https://github.com/plataformatec/devise/blob/master/test/rails_app/lib/shared_user_without_email.rb

@@ -124,7 +124,9 @@ def user_sign_up
# https://github.com/mongoid/mongoid/issues/756
(pending "Fails on Mongoid < 2.1"; break) if defined?(Mongoid) && Mongoid::VERSION.to_f < 2.1

create_user
user = create_user
user.update_attribute(:username, nil)
Copy link
Member

@tegon tegon Nov 26, 2019

Choose a reason for hiding this comment

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

Would you mind to explain why this change was needed?

Copy link
Author

Choose a reason for hiding this comment

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

I don't remember now...I'll pick this back up in the next few days along with the other comments you made.

@@ -26,6 +26,18 @@ class ValidatableTest < ActiveSupport::TestCase
assert user.valid?
end

test 'should allow duplicate email when email_scope attribute does not match' do
Copy link
Member

Choose a reason for hiding this comment

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

I believe something like this should be enough:

existing_user = create_user
user = new_user(email: existing_user.email, username: "New username")

assert user.valid?

@christianrolle
Copy link

@flynfish Did you have the chance to push this change forward? We certainly should get it done.

@flynfish
Copy link
Author

flynfish commented Apr 1, 2020

@flynfish Did you have the chance to push this change forward? We certainly should get it done.

I didn't have a chance, but thanks for the reminder. I'm gonna set a goal to get the comments addressed in the next week.

@donny-nyc
Copy link

@flynfish we're looking to enable this in our project. Would you recommend forking for now?

@Unnumbered
Copy link

any news?

@xhocquet
Copy link

Hey @tegon , I'm stepping in to try and push this over the finish line for my colleague @flynfish

I've split the test case out and reverted some unnecessary changes caused by conflating the two test users. Could you take another look and let me know what you think? Thanks! 🙏

@xhocquet xhocquet force-pushed the 4767-add-email-scope branch from 11ab701 to bd4580f Compare August 12, 2020 19:49
@flynfish flynfish requested a review from tegon August 12, 2020 19:57
@xhocquet
Copy link

xhocquet commented Sep 1, 2020

Hey @carlosantoniodasilva , I noticed you were actively merging PRs, could you help us get this one across the finish line? I think the community would appreciate this feature!

@carlosantoniodasilva
Copy link
Member

carlosantoniodasilva commented Sep 2, 2020

@xhocquet hey, yes! Appreciate the ping. I've recently started to catch up on some of the issues/PRs here, just want to wrap up a few things on the other projects and then Devise is next. I'm putting this one high in the list to follow-up on first, please stay put for a little longer, I should get to it soon.

@danzanzini
Copy link

danzanzini commented Sep 29, 2020

Hello @carlosantoniodasilva, I was taking a look at this PR and I haven't found where these changes impact the migration generated to create users.

As far as I know, the migration generated by Devise adds a database index to ensure email uniqueness. Without changing this index, this feature will not work since the new user creation will be blocked at the database layer.

Another thing to take into consideration is that the recoverable module may not work well because of the duplicated e-mails.
As described in this wiki post:

it may be the case that users can have multiple accounts with the same e-mail address. This will cause the “recoverable” module not to work well, as it will only include a link in the password reset e-mail to the first account for which it finds a matching e-mail.

I'm not so sure about all of this, but can you take a look when reviewing this PR?
If you find that these points are blockers for this PR, I will be glad to help to find possible solutions and work on them.

Thank you

@xhocquet
Copy link

xhocquet commented Oct 1, 2020

Thanks for the feedback @danzanzini , some thoughts -

By following the referenced wiki post, the migration created by Devise generators would already be insufficient since they would not contain the 'username' for example. One option could be to add a commented out row with the scoped index in the migration to guide developers to the right usage?

Given the migration gotcha and the incompatibility with the recoverable module, maybe it makes sense to create a new wiki for scoped emails outlining the caveats?

@danzanzini
Copy link

@xhocquet creating a wiki article is a good idea and should solve all these problems, I think it will also remove the need for the commented line in the migration since it would be noise for most users.

I'll work on a first version and then bring it here for discussion.

Thank you :)

@danzanzini
Copy link

danzanzini commented Oct 5, 2020

@xhocquet Here is the first version of the wiki page wiki page, feel free to make any adjustments you find necessary.

Since this feature is not merged, I haven't added it to the wiki index.

@xhocquet
Copy link

xhocquet commented Oct 6, 2020

@danzanzini Small typo fix but otherwise the wiki looks great! I'm not sure if there's other considerations for this change, but the existing page seems to reflect the two you mentioned succinctly.

Edit: Realized I changed the title and therefore the link above became invalid. New wiki link

@vickodin
Copy link

On the other hand, we have next 2 options in config/initializers/devise.rb

  # ==> Configuration for any authentication mechanism
  # Configure which keys are used when authenticating a user. The default is
  # just :email. You can configure it to use [:username, :subdomain], so for
  # authenticating a user, both parameters are required. Remember that those
  # parameters are used only when authenticating and not when retrieving from
  # session. If you need permissions, you should implement that in a before filter.
  # You can also supply a hash where the value is a boolean determining whether
  # or not authentication should be aborted when the value is not present.
  # config.authentication_keys = [:email]

  # Configure parameters from the request object used for authentication. Each entry
  # given should be a request method and it will automatically be passed to the
  # find_for_authentication method and considered in your model lookup. For instance,
  # if you set :request_keys to [:subdomain], :subdomain will be used on authentication.
  # The same considerations mentioned for authentication_keys also apply to request_keys.
  # config.request_keys = []

I'm not yet understand why we need request_keys, but we can use:
config.authentication_keys = [:account_id, :email] instead of email_scope

What do you think?

@xhocquet
Copy link

@vickodin I think your suggestion is not directly related to the problem this PR is trying to solve. The problem we are trying to solve is that multiple users with the same email cannot be created because of Devise validations.

The configs you point to could be used to lookup these users after they are created. Personally we would not use this and would instead use domain-based scoping to load our user records.

@vickodin
Copy link

@vickodin I think your suggestion is not directly related to the problem this PR is trying to solve. The problem we are trying to solve is that multiple users with the same email cannot be created because of Devise validations.

The configs you point to could be used to lookup these users after they are created. Personally we would not use this and would instead use domain-based scoping to load our user records.

@xhocquet Thank you for explanations!

Then when this PR will be merged?

Thanks in advance!

Copy link

@vickodin vickodin left a comment

Choose a reason for hiding this comment

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

Looks good!

@xhocquet
Copy link

xhocquet commented Jan 5, 2021

Hey @carlosantoniodasilva, happy new year! Hoping to bump this in your notifications 😄

@carlosantoniodasilva
Copy link
Member

@xhocquet happy new year! Consider it bumped 😉

There may be more to it than just the validation so I need to look into it a bit more carefully in the coming weeks.

@nexxus-vi
Copy link

+1, we need this please :)

@poetry-like-code
Copy link

poetry-like-code commented Mar 10, 2021

Although this request seems to be dragging for morethan 3 years now, i don't see anyone venting out any frustration, kudos to everyone for that. :)
And +1, we need this too :) kindly fast-track.

@stephenpassero
Copy link

@carlosantoniodasilva I noticed @poetry-like-code brought up this PR recently, but didn't see any changes. Is there any update on getting this merged?

Thanks!

@carlosantoniodasilva
Copy link
Member

@stephenpassero appreciate the ping, unfortunately no updates at this time yet.

This is still where it stands:

There may be more to it than just the validation so I need to look into it a bit more carefully in the coming weeks.

Unfortunately 3+ months and I haven't stopped to look into this carefully yet, but I'm bumping it again in my list here. First I have to get a release out there. 😅

@tegon tegon removed their request for review May 5, 2021 14:02
@akshaysmurthy
Copy link

akshaysmurthy commented Sep 6, 2021

Until this is merged, what is the recommendation for people like me who wants to implement this (email uniqueness on a scope) in our app today? 😄

@danzanzini
Copy link

danzanzini commented Sep 6, 2021

Hi @akshaysmurthy, I did these steps to implement it in my app:

  1. Changed the unique index of the email column:
def up
  remove_index :users, :email
  add_index :users, [:email, :tenant_id], unique: true
end
  1. Overwrote the will_save_change_to_email? method so Devise skips its built-in validations. This change may impact other parts of your application if this method is used anywhere else.
# user.rb
def will_save_change_to_email?
  false
end
  1. Created my own email validations:
# user.rb

validates :email, uniqueness: { scope: :tenant_id }
validates :email, format: /\A[^@\s]+@[^@\s]+\z/

Hope it helps

@mrabetr
Copy link

mrabetr commented Sep 24, 2021

when is this due to be merged please? thanks

@flynfish
Copy link
Author

@tegon @vickodin @carlosantoniodasilva it would be really helpful to the community if we could get this merged or if you could provide any insight into what we can do to help out further

@carlosantoniodasilva
Copy link
Member

@danzanzini thanks for the detailed steps there.

@akshaysmurthy the simplest thing you can do if you need any further customization: don't use validatable, write your own email/password validations that include the uniqueness scope.

@mrabetr there's no due date for merging PRs. ;)

@flynfish at this point I cannot provide any insights, without actually stopping to look into the problem and think about it more carefully, my apologies. Like I said above, multi tenancy might be more than just the validation so I want to consider the problem better, it's just not been high in my priority list, all things considered. In any case, validatable is composed of just a few validations that you can easily add to your model to customize things further for now.

I am unfortunately not in a position to provide any timeline for when I can work on this. Again, I apologize to everyone for not having given this proper attention yet, but I want to reassure that I'm keeping an eye on each and every comment.

❤️💜💛💚💙

@bvogel
Copy link

bvogel commented May 23, 2023

Yeah, another year and a tad have passed, making this probably the longest outstanding PR in this project with the most interest. Any update? (i.e. bump!)

@flamontagne
Copy link

Hi @akshaysmurthy, I did these steps to implement it in my app:

  1. Changed the unique index of the email column:
def up
  remove_index :users, :email
  add_index :users, [:email, :tenant_id], unique: true
end
  1. Overwrote the will_save_change_to_email? method so Devise skips its built-in validations. This change may impact other parts of your application if this method is used anywhere else.
# user.rb
def will_save_change_to_email?
  false
end
  1. Created my own email validations:
# user.rb

validates :email, uniqueness: { scope: :tenant_id }
validates :email, format: /\A[^@\s]+@[^@\s]+\z/

Hope it helps

This is a great workaround, however redifining will_save_change_to_email? will also mess with the Confirmable plugin.

Instead of redefining will_save_change_to_email? I think it's better to disable the validatable plugin. Just make sure you add all the validators you need (for email and password columns) in the User model.

@bvogel
Copy link

bvogel commented Aug 22, 2023

@carlosantoniodasilva if you aren't willing to implement this, please be honest enough to at least give an explanation and close this PR.

@flamontagne
Copy link

A word of caution: I just found out that the workaround mentionned in this thread will also mess badly with the password reset function. If the same email address is associated with two or more tenants, the password reset link in the email may point to the WRONG tenant. (e.g. wrongsubdomain.myapp.com/reset-password?token=blablabla...)

@bvogel
Copy link

bvogel commented Jan 24, 2024

As this just hit us again, @carlosantoniodasilva - any news and/or decisions on how to move forward with this?

@justwiebe
Copy link

@flamontagne In my case, I just needed to support soft-deleted users, so I was able to fix the password reset link by hacking send_reset_password_instructions

class User < ApplicationRecord
  def self.send_reset_password_instructions(attributes = {})
    recoverable = where(deleted_at: nil).find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
    recoverable.send_reset_password_instructions if recoverable.persisted?
    recoverable
  end
end

@fa11enangel
Copy link

fa11enangel commented Mar 26, 2024

@justwiebe thank you for the hint. I've updated the wiki subdomain tutorial to add a password reset instruction:

see: https://github.com/heartcombo/devise/wiki/How-to:-Scope-login-to-subdomain

@flamontagne you can have a look at it. It should help you to find a solution.

Maybe it is a good idea, to fill up attributes for the method based on request_keys: [:subdomain] given in User model as it is used for authentication.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.