Skip to content
This repository has been archived by the owner on Jun 27, 2023. It is now read-only.

Spring cleaning users #1006

Merged
merged 22 commits into from
May 31, 2018
Merged

Spring cleaning users #1006

merged 22 commits into from
May 31, 2018

Conversation

emcoding
Copy link
Member

@emcoding emcoding commented May 25, 2018

Related issue #991

On the way to separating authentication from authorisation:

  • Straighten out the authorisation in User: now we authorise all user actions, so that the abilities can take care what is shown to who (for example: we want a supervisor to be able to see email addresses of team members in the show view, even if the team member choose to hide their email from regular users).
  • Added a feature test for the sign in process for unconfirmed users.

Also:

  • UX: Added a client side validation for the city field, which already has a server side validation
  • Database: Added migration to remove users without a github_handle.
    Why? For accounts imported from GitHUb, the github_id is not nil. So if the id is nil, it is a fake
    account or an input error. Related issue Add rake task to strip down production database dump #979
    @klappradla I never used a migration for removing data before. I copied one of your examples, can you double double check please?
  • FIX: only confirmed users can read email addresses in users index and show

ALERT

  • In Dev env, running the migration will remove the seeded users from your local database. If you want some of them to persist, give them a fake github_id < 100.
  • Run rails db:migrate and rails db:seed

Background info:

  • Cancancan's load_and_authorize_resource will load the resource, unless it is already set. That means: we don't need the separate before_action: set_resource for authorised actions. (Note that my previous description about this behaviour where not 100% accurate 😇 )

Follow up:

  • Update the hint in the form, and tell the user that a supervisor and admin will be able to see email, whether marked hidden or not. Check first if this statement is and should be true.
  • Add message to slack dev channel that all user-seeds will be removed with the latest migration.

emcoding added 7 commits May 24, 2018 15:16
We added a rule that all users `can :read User`; we can choose to not authorize those actions, or check the abilities anyway.
City field has a presence validation, but that was not visible in the view.
#979)

For real github accounts, the github_id is not nil. So if the id is nil, it is a fake account or an input error.
Updated seeds with github_id
@emcoding
Copy link
Member Author

This is ready for review 🙇

Copy link
Member

@hola-soy-milk hola-soy-milk left a comment

Choose a reason for hiding this comment

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

Looks great to me. Thank you so much!

method = "#{resource}_params"
params[resource] &&= send(method) if respond_to?(method, true)
end

Copy link
Member

Choose a reason for hiding this comment

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

🙌

Copy link
Collaborator

@pgaspar pgaspar left a comment

Choose a reason for hiding this comment

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

I think this is heading in the right direction 🙌 🚀

Left some comments on things I think we should improve before merging 👍

@@ -31,8 +31,7 @@ table.users.table.table-striped.table-bordered.table-condensed.table-hover
td.image = avatar_url(user, size: 40)
td = link_to user.name_or_handle, user
td = user.interested_in.map { |key| User::INTERESTS[key].presence }.compact.join(', ')
- if signed_in?
td = mail_to user.email unless user.hide_email?
td = mail_to user.email if can?(:read, user.email)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think there's a bug here: this keeps the td even if the can? method returns false, which breaks the layout a bit. (Try it with a logged out or unconfirmed user.)

I think you need to change this back to be a 2-line conditional.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, as pointed out below, I think the check should be can?(:read_email, user).

Copy link
Member Author

Choose a reason for hiding this comment

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

Oiii, thanks for catching this. Turns out I was solving the wrong problem. Fixing the ability rules also fixed the layout and what I was trying to do with the new ability rule.

@@ -16,7 +16,7 @@ table.users.table.table-striped.table-bordered.table-condensed.table-hover
th
th = sortable :name
th = sortable :interested_in
- if signed_in?
- if current_user&.confirmed?
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should make this check be the same can? check as you have below. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

🤔To use the can? helper , I'd need access to the single user, as I do have below. I'd think this hack is better than alternatives, like adding an extra @user ivar? Or...?
Neat it is not. 😢

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, good point. I wonder what people usually do in these circumstances - we are kinda duplicating authorization knowledge here.

Also this seems to be slightly different from what we have specified in the Ability class: from what I understand, confirmed users can: can :read_email, User, hide_email: false, can't they? (I may be missing something here! I'm not too familiar with CanCan).

Copy link
Member Author

Choose a reason for hiding this comment

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

I hope we'll have a nicer solution at the end of the Overhaul. And I'll keep looking for a better solution.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Saw your recent change to Ability 👍 One thing that seems to work is - if can?(:read_email, nil).

Copy link
Member Author

Choose a reason for hiding this comment

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

That didn't work for me, not as a twoliner, and not as a oneliner :-(
See next commits.

@@ -0,0 +1,5 @@
class DestroyNonExistingGitHubUsers < ActiveRecord::Migration[5.1]
def up
execute 'DELETE FROM users WHERE github_id IS NULL'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Some considerations 🤔:

First, I think we may want to prevent this from running on development to avoid the issue you pointed out on the PR description. I worry this may cause unexpected behavior for existing developers who update their local copy of the app.

Wrapping the execute statement on a unless Rails.env.development? should do it, I think 👍

Second point: do these users have any relationships with other models in the database? If they do, maybe we should consider doing this using Ruby so the destroy callbacks run. User.where(github_id: nil).destroy_all. I don't have access to production data to see if this is the case or not.

Copy link
Member Author

Choose a reason for hiding this comment

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

  1. I like the ruby destroy better anyway.
  2. Are you sure about not running the migration in dev env? I'd think that we want the user objects in the local database not to be different from the test and production databases?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Then maybe we could generate github_ids for users without one in development to make them be compatible without deleting them?

Copy link
Member Author

Choose a reason for hiding this comment

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

I intended to add a 'how-to' to the readme or troubleshooting guide. Or should I add it to the migration? Add a part to add gh_ids if env == dev?

Sorry, I missed this comment yesterday.

expect(response.body).not_to include user_opted_out.email
# expect(response.body).to include user.email
# This expectation ^ fails, but should this be tested in a controller test in
# the first place?
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm confused 😕 What changed in this PR to make this expectation start failing?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, I think this really is an issue with your changes:

# Shouldn't this:
can?(:read, user.email)

# Be this:
can?(:read_email, user)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it should! See other comment. 💃

@@ -15,6 +15,8 @@

it_behaves_like 'has access to public features'

it { expect(subject).not_to be_able_to(:read, other_user.email) }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Heads up I believe this should be be_able_to(:read_email, other_user). Same below.

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought this is testing the desired outcome of the read_email rule? No?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not experienced with CanCan, but looking at how the rule is defined, using read_email instead of read, I would assume you need to use read_email here as well. Also, the rule definition does not receive an email, right? It receives a User.

But it should be testable. I made these changes and the spec fails:

screen shot 2018-05-28 at 13 24 58

Using this makes it pass:

it { expect(subject).to be_able_to(:read_email, other_user) }

Copy link
Member Author

Choose a reason for hiding this comment

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

You are right. I remember what I did here. The thing is, the read_email rule is not yet renewed yet. So it is hard to test it (and use it in the community view) the right way.
The one thing I wanted to guard is that a guest user can not read emails. I am 98% sure that the test is testing the right thing here. 😇
See my general comment for a proposal on how to handle this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Still think this is a false positive @F3PiX 😟 I wasn't able to make be_able_to(:read, other_user.email) return true even for cases where it should return true, as I showed above.

This is the same error you did initially, where you were testing things in the view as can?(:read, user.email) and it didn't work and we changed it to can?(:read_email, user). It makes sense that the same logic would apply here in the expectation.

Copy link
Member Author

Choose a reason for hiding this comment

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

O yes. So sorry. You are right.

@emcoding
Copy link
Member Author

@pgaspar Back to you!
Heads up: if you pulled the branch and ran the migration, you may want to rollback that migration first.

Copy link
Collaborator

@pgaspar pgaspar left a comment

Choose a reason for hiding this comment

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

Thanks for your changes @F3PiX!

Added some more comments. Thanks for bearing with me 😅

@@ -16,7 +16,7 @@ table.users.table.table-striped.table-bordered.table-condensed.table-hover
th
th = sortable :name
th = sortable :interested_in
- if signed_in?
- if current_user&.confirmed?
Copy link
Collaborator

Choose a reason for hiding this comment

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

Saw your recent change to Ability 👍 One thing that seems to work is - if can?(:read_email, nil).

@@ -31,8 +31,7 @@ table.users.table.table-striped.table-bordered.table-condensed.table-hover
td.image = avatar_url(user, size: 40)
td = link_to user.name_or_handle, user
td = user.interested_in.map { |key| User::INTERESTS[key].presence }.compact.join(', ')
- if signed_in?
td = mail_to user.email unless user.hide_email?
td = mail_to user.email if can?(:read_email, user)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This still needs to change to this form:

- if can?(:read_email, user)
  td = mail_to user.email

Otherwise, this happens when you're logged out (or when the can? check returns false):

screen shot 2018-05-28 at 12 33 01

(There's a ghost column under the Github table header).

Copy link
Member Author

Choose a reason for hiding this comment

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

🙈 I thought I had it right. Sorry.
I didn't miss your suggestion, I just found that it gave problems when a user as their email hidden. Look:

two_line_if_one_hidden_email
(first user hides email).

I will look again!!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, that's a great point - I'm sorry I missed that!

I haven't tested this, but maybe something like this would work:

- if can?(:read_email, nil)
  td = mail_to user.email if can?(:read_email, user)

The idea being that the first check will be false for unconfirmed / logged out users, while being true for other users. It's not the prettiest thing every, but it may solve the issue without us needing to explicitly check if the user is confirmed (which duplicates knowledge that unconfirmed/logged out users should not see emails).

def up
unless ENV['RAILS_ENV'] == 'development'
User.find_each do |user|
if user.github_id == nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm curious why you're doing this like that instead of filtering users upfront.

User.where(github_id: nil).find_each do |user|
  puts "removing user #{user.id} from database"
  user.destroy
end

It would avoid needing to go through all the users in the database.

User.find_each do |user|
if user.github_id == nil
puts "removing user #{user.id} from database"
user.destroy
Copy link
Collaborator

Choose a reason for hiding this comment

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

This may not matter much, depending on the number of records we have, but I think this version leads to better performance:

User.where(github_id: nil).destroy_all.each do |user|
  puts "removed user #{user.id} from database"
end

destroy_all returns the records that were removed, so we can use those to print stuff.

PS: did a quick one-run test and for 5000 users this method ran in ~23 seconds while the inline user.destroy method ran in ~41 seconds. That being said, this probably doesn't matter at all since we don't have a lot of records being destroyed! 😅

@@ -36,7 +36,8 @@
user_opted_out = create(:user, hide_email: true)
get :index
expect(response.body).to include user.email
expect(response.body).not_to include user_opted_out.email
# Should this be tested in a controller test?
expect(response.body).not_to include user_opted_out.email # false negative; also passes with hide_email: false
Copy link
Collaborator

Choose a reason for hiding this comment

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

@F3PiX I may be missing something here, but I think this may not be a false negative. Changing user_opted_out to have hide_email: false will make the test fail.

let!(:activity) { create(:status_update, :published, team: team1) }
let!(:other_user) { create(:user) }
let!(:activity) { create(:status_update, :published, team: team1) }
let(:other_user) { create(:user, hide_email: false) }
Copy link
Collaborator

@pgaspar pgaspar May 28, 2018

Choose a reason for hiding this comment

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

Isn't this change unnecessary? Removing it does not make the spec fail (I assume hide_email defaults to false).

Are you doing it for clarity? Looks good if you are 👍


it 'nothing happens when clicking on resend link' do
find_link('Click here to resend the email.').click
# todo: expect_to happen: something visible
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe check that an email was enqueued here or something?

Copy link
Member Author

Choose a reason for hiding this comment

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

I mean there is no visual feedback to the user.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yep, I understood that part 😅 I was just saying that maybe we should also check that an email was enqueued, because that's what happens, right? Or maybe it isn't? I'm a bit confused by the spec name nothing happens when clicking on resend link.

@@ -15,6 +15,8 @@

it_behaves_like 'has access to public features'

it { expect(subject).not_to be_able_to(:read, other_user.email) }
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not experienced with CanCan, but looking at how the rule is defined, using read_email instead of read, I would assume you need to use read_email here as well. Also, the rule definition does not receive an email, right? It receives a User.

But it should be testable. I made these changes and the spec fails:

screen shot 2018-05-28 at 13 24 58

Using this makes it pass:

it { expect(subject).to be_able_to(:read_email, other_user) }

@emcoding
Copy link
Member Author

emcoding commented May 28, 2018

@pgaspar Sorry for keeping you busy 🙏
What I did: I made it so that the column title is hiding now in sync with the email's visibility. That means, we have a stray mini column for guests and unconfirmed users.
I rather left it as is for now, because both the authentication is not finished, and the ability rule :read_email is not renewed. The thing is: I couldn't leave it alone, because the email addresses got exposed to guest users. My priority was to prevent that.
We already have an issue for the :read_email rule (I'll update that one with our acquired knowledge and questions).
Is that acceptable? All the changes are interrelated, so it is hard to not change everything at once, and impossible to change one thing at a time :-) I am very open to suggestions on how to handle this situation better!

@emcoding
Copy link
Member Author

For confirmed users it looks like this now:
schermafbeelding 2018-05-28 om 20 32 22

For unconfirmed and guests like this 💩
schermafbeelding 2018-05-28 om 20 31 48

@pgaspar
Copy link
Collaborator

pgaspar commented May 28, 2018

Thank you @F3PiX 👍 And thanks for your patience.

If you're willing to try one last solution, this should get rid of the empty column for guests:

- if current_user&.confirmed?
  th Email

/ ...

- if current_user&.confirmed?
  td = mail_to user.email if can?(:read_email, user)

Did a quick test and it seemed to work (and it should work, looking at the code).

@emcoding emcoding force-pushed the spring-cleaning-users branch from e6bd629 to d48ea6b Compare May 29, 2018 09:32
@emcoding
Copy link
Member Author

@pgaspar I updated the specs; now they do test the read_email rule. Thanks.
One thing left to do:
[ ] add github_ids to users in dev env
Should I add it to the migration? Add a separate migration? WDYT?

@emcoding emcoding closed this May 29, 2018
@emcoding emcoding reopened this May 29, 2018
@pgaspar
Copy link
Collaborator

pgaspar commented May 29, 2018

Great work @F3PiX 👍

As for the github_ids, I think both options are fine, so whatever you prefer 😸 There is probably a way to do this with SQL directly using Random() or something, but you can do it with Ruby as well...

@emcoding emcoding force-pushed the spring-cleaning-users branch from d10b94c to 62b49c4 Compare May 30, 2018 16:51
users = User.all.select { |user| user.github_id.nil? }
users.each_with_index do |user, index|
user.update!(github_id: index + 10)
end
Copy link
Collaborator

Choose a reason for hiding this comment

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

@F3PiX select is doing this in ruby which is slow 😟 users = User.where(github_id: nil) does the same thing but in the database, which is fast!

users.each_with_index is not great for memory because it loads every record without a github id at the same time into memory.

Here's an alternative:

User.where(github_id: nil).find_each(batch_size: 200).with_index do |user, index|
  user.update!(github_id: index + 10)
end

You can find a similar example in the docs here: http://api.rubyonrails.org/classes/ActiveRecord/Batches.html#method-i-find_each

@emcoding
Copy link
Member Author

@pgaspar I got distracted before I submitted my comment.
I was going to say:

  • I put it in the same migration, so that the nil records don't get deleted first :-)
  • It will never be more than ca 30 records, so I didn't care about performance.

Do you want me to fix it anyway? I'll do that tomorrow then! Either way, I will stick to the where's in the future.

Copy link
Collaborator

@pgaspar pgaspar left a comment

Choose a reason for hiding this comment

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

@F3PiX no, that’s fine 🙌 thank you for your patience! ❤️

@emcoding
Copy link
Member Author

Thanks so much @pgaspar for having my back! 🙇 Much appreciated!

@emcoding emcoding merged commit 0d50ff4 into master May 31, 2018
@klappradla klappradla deleted the spring-cleaning-users branch January 25, 2019 10:36
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants