Repository navigation
Lock the reviewer's email address so revocation can't be bypassed #3239
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
Merged
+290
−7
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
21 changes: 21 additions & 0 deletions
21
saas/app/controllers/concerns/users/reviewer_email_address_lock.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Refuses an email change for the directory reviewer's users, before any | ||
| # confirmation is sent (see User::ReviewerEmailAddressLock). | ||
| module Users::ReviewerEmailAddressLock | ||
| extend ActiveSupport::Concern | ||
|
|
||
| included do | ||
| before_action :refuse_reviewer_email_address_change, only: %i[ new create ] | ||
| end | ||
|
|
||
| private | ||
| def refuse_reviewer_email_address_change | ||
| if @user.email_address_locked? | ||
| error = "This email address can't be changed" | ||
|
|
||
| respond_to do |format| | ||
| format.html { redirect_to edit_user_path(@user), alert: error } | ||
| format.json { render json: { error: error }, status: :forbidden } | ||
| end | ||
| end | ||
| end | ||
| end |
25 changes: 25 additions & 0 deletions
25
saas/app/controllers/concerns/users/reviewer_membership_lock.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Refuses to deactivate the directory reviewer's users from the app, whoever | ||
| # asks. Deactivating a user clears its identity, so the account would drop out | ||
| # of the reviewer's accounts, and `saas:reviewer_sign_in:sign_out` would then | ||
| # neither reset that account's join code nor list its members for the | ||
| # after-review check. Operators who mean to remove the reviewer do it from a | ||
| # console. | ||
| module Users::ReviewerMembershipLock | ||
| extend ActiveSupport::Concern | ||
|
|
||
| included do | ||
| before_action :refuse_reviewer_deactivation, only: %i[ destroy ] | ||
| end | ||
|
|
||
| private | ||
| def refuse_reviewer_deactivation | ||
| if Identity::ReviewerSignIn.reviewer?(@user.identity) | ||
| error = "This person can't be removed" | ||
|
|
||
| respond_to do |format| | ||
| format.html { redirect_to account_settings_path, alert: error } | ||
| format.json { render json: { error: error }, status: :forbidden } | ||
| end | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # The directory reviewer's users keep the reviewer's email address. An email | ||
| # change moves a user to another identity, so a holder of the reviewer code | ||
| # could otherwise move the reviewer's membership to an address they control, | ||
| # where rotating the code and `saas:reviewer_sign_in:sign_out` can't reach it. | ||
| # The reviewer is a comped internal identity, so nobody needs to change it. | ||
| module User::ReviewerEmailAddressLock | ||
| def send_email_address_change_confirmation(new_email_address) | ||
| super unless email_address_locked? | ||
| end | ||
|
|
||
| def change_email_address(new_email_address) | ||
| if email_address_locked? | ||
| false | ||
| else | ||
| super | ||
| end | ||
| end | ||
|
|
||
| def email_address_locked? | ||
| Identity::ReviewerSignIn.reviewer?(identity) | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
saas/test/controllers/users/reviewer_email_address_lock_test.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| require "test_helper" | ||
|
|
||
| class Users::ReviewerEmailAddressLockTest < ActionDispatch::IntegrationTest | ||
| include ActionMailer::TestHelper | ||
|
|
||
| setup do | ||
| @reviewer = identities(:kevin) | ||
| @user = users(:kevin) | ||
| configure email_address: @reviewer.email_address, code_digest: Identity::ReviewerSignIn.digest(Identity::ReviewerSignIn.generate_code) | ||
| sign_in_as :kevin | ||
| end | ||
|
|
||
| teardown { configure email_address: nil, code_digest: nil } | ||
|
|
||
| test "the reviewer can't request an email change" do | ||
| assert_no_enqueued_emails do | ||
| post user_email_addresses_path(@user, script_name: @user.account.slug), params: { email_address: "attacker@example.com" } | ||
| end | ||
|
|
||
| assert_redirected_to edit_user_path(@user, script_name: @user.account.slug) | ||
|
|
||
| post user_email_addresses_path(@user, script_name: @user.account.slug), params: { email_address: "attacker@example.com" }, as: :json | ||
| assert_response :forbidden | ||
| end | ||
|
|
||
| test "a confirmation link minted for the reviewer doesn't move it" do | ||
| token = @user.send(:generate_email_address_change_token, to: "attacker@example.com") | ||
|
|
||
| post user_email_address_confirmation_path(user_id: @user.id, email_address_token: token, script_name: @user.account.slug), as: :json | ||
|
|
||
| assert_response :unprocessable_entity | ||
| assert_equal @reviewer, @user.reload.identity | ||
| end | ||
|
|
||
| private | ||
| def configure(email_address:, code_digest:) | ||
| Rails.application.config.x.reviewer_sign_in.email_address = email_address | ||
| Rails.application.config.x.reviewer_sign_in.code_digest = code_digest | ||
| end | ||
| end |
51 changes: 51 additions & 0 deletions
51
saas/test/controllers/users/reviewer_membership_lock_test.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| require "test_helper" | ||
|
|
||
| class Users::ReviewerMembershipLockTest < ActionDispatch::IntegrationTest | ||
| setup do | ||
| @reviewer = identities(:kevin) | ||
| @user = users(:kevin) | ||
| configure email_address: @reviewer.email_address, code_digest: Identity::ReviewerSignIn.digest(Identity::ReviewerSignIn.generate_code) | ||
| end | ||
|
|
||
| teardown { configure email_address: nil, code_digest: nil } | ||
|
|
||
| test "the reviewer can't deactivate itself" do | ||
| sign_in_as :kevin | ||
|
|
||
| delete user_path(@user, script_name: @user.account.slug), as: :json | ||
|
|
||
| assert_response :forbidden | ||
| assert @user.reload.active? | ||
| assert_equal @reviewer, @user.identity | ||
| assert_includes @reviewer.accounts.reload, @user.account | ||
| end | ||
|
|
||
| test "another admin can't deactivate the reviewer either" do | ||
| admin = @user.account.users.where.not(id: @user.id).find { |user| user.identity && user.can_change?(@user) } || \ | ||
| flunk("fixture needs another admin in the reviewer's account") | ||
| sign_in_as admin.identity | ||
|
|
||
| delete user_path(@user, script_name: @user.account.slug) | ||
|
|
||
| assert_redirected_to account_settings_path(script_name: @user.account.slug) | ||
| assert @user.reload.active? | ||
| end | ||
|
|
||
| test "other users can still be deactivated" do | ||
| configure email_address: identities(:jason).email_address, code_digest: nil | ||
| sign_in_as :kevin | ||
| other = @user.account.users.where.not(id: @user.id).find { |user| user.identity && @user.can_change?(user) && !user.owner? } || \ | ||
| flunk("fixture needs a user kevin can change") | ||
|
|
||
| delete user_path(other, script_name: other.account.slug), as: :json | ||
|
|
||
| assert_response :no_content | ||
| assert_not other.reload.active? | ||
| end | ||
|
|
||
| private | ||
| def configure(email_address:, code_digest:) | ||
| Rails.application.config.x.reviewer_sign_in.email_address = email_address | ||
| Rails.application.config.x.reviewer_sign_in.code_digest = code_digest | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| require "test_helper" | ||
|
|
||
| class User::ReviewerEmailAddressLockTest < ActiveSupport::TestCase | ||
| include ActionMailer::TestHelper | ||
|
|
||
| setup do | ||
| @code = Identity::ReviewerSignIn.generate_code | ||
| @reviewer = identities(:kevin) | ||
| @user = users(:kevin) | ||
| configure email_address: @reviewer.email_address, code_digest: Identity::ReviewerSignIn.digest(@code) | ||
| end | ||
|
|
||
| teardown { configure email_address: nil, code_digest: nil } | ||
|
|
||
| test "the reviewer can't move its user to an address a code holder controls, so sign-out still reaches it" do | ||
| token = @user.send(:generate_email_address_change_token, to: "attacker@example.com") | ||
| @reviewer.sessions.create! | ||
|
|
||
| assert_not @user.change_email_address_using_token(token) | ||
| assert_equal @reviewer, @user.reload.identity | ||
| assert_nil Identity.find_by(email_address: "attacker@example.com") | ||
|
|
||
| assert_operator Identity::ReviewerSignIn.sign_out_everywhere, :>, 0 | ||
| assert_empty @reviewer.sessions.reload | ||
| assert_nil Identity::ReviewerSignIn.authenticate(email_address: "attacker@example.com", code: @code) | ||
| end | ||
|
|
||
| test "the reviewer gets no email change confirmation" do | ||
| assert_no_enqueued_emails do | ||
| @user.send_email_address_change_confirmation("attacker@example.com") | ||
| end | ||
| end | ||
|
|
||
| test "everyone else can still change their email address" do | ||
| user = users(:david) | ||
|
|
||
| assert user.change_email_address("david.new@example.com") | ||
| assert_equal "david.new@example.com", user.reload.identity.email_address | ||
| end | ||
|
|
||
| test "nobody is locked when the reviewer isn't configured" do | ||
| configure email_address: nil, code_digest: nil | ||
|
|
||
| assert_not @user.email_address_locked? | ||
| end | ||
|
|
||
| private | ||
| def configure(email_address:, code_digest:) | ||
| Rails.application.config.x.reviewer_sign_in.email_address = email_address | ||
| Rails.application.config.x.reviewer_sign_in.code_digest = code_digest | ||
| end | ||
| end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.