InstAccess Tokens should prefer shard-local users
closes FOO-2368 TEST PLAN: 1) have a user-merge occur x-shard 2) visit an api-gateway mediated pathway like account notifications 3) correct shard-local user should always be loaded as auth context Change-Id: I2dfb86ec35499e9a00ebb8498c4eab9c6c95297e Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/273451 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Rob Orton <rob@instructure.com> QA-Review: Ethan Vizitei <evizitei@instructure.com> Product-Review: Ethan Vizitei <evizitei@instructure.com>
This commit is contained in:
parent
8303761f3f
commit
9aaf1106fc
|
@ -42,34 +42,19 @@ module AuthenticationMethods
|
|||
end
|
||||
|
||||
def load_pseudonym_from_inst_access_token(token_string)
|
||||
return false unless InstAccess::Token.is_token?(token_string)
|
||||
token = ::AuthenticationMethods::InstAccessToken.parse(token_string)
|
||||
return false unless token
|
||||
|
||||
begin
|
||||
token = InstAccess::Token.from_token_string(token_string)
|
||||
rescue InstAccess::InvalidToken, # token didn't pass signature verification
|
||||
InstAccess::TokenExpired # token passed signature verification, but is expired
|
||||
raise AccessTokenError
|
||||
rescue InstAccess::ConfigError => exception
|
||||
# InstAccess isn't configured. A human should fix that, but this method
|
||||
# should recover gracefully.
|
||||
Canvas::Errors.capture_exception(:inst_access, exception, :warn)
|
||||
return true
|
||||
end
|
||||
auth_context = ::AuthenticationMethods::InstAccessToken.load_user_and_pseudonym_context(token, @domain_root_account)
|
||||
|
||||
@current_user = User.find_by(uuid: token.user_uuid)
|
||||
@current_pseudonym = SisPseudonym.for(
|
||||
@current_user, @domain_root_account, type: :implicit, require_sis: false
|
||||
)
|
||||
@current_user = auth_context[:current_user]
|
||||
@current_pseudonym = auth_context[:current_pseudonym]
|
||||
raise AccessTokenError unless @current_user && @current_pseudonym
|
||||
|
||||
if token.masquerading_user_uuid && token.masquerading_user_shard_id
|
||||
Shard.lookup(token.masquerading_user_shard_id).activate do
|
||||
@real_current_user = User.find_by!(uuid: token.masquerading_user_uuid)
|
||||
@real_current_pseudonym = SisPseudonym.for(
|
||||
@real_current_user, @domain_root_account, type: :implicit, require_sis: false
|
||||
)
|
||||
logger.warn "[AUTH] #{@real_current_user.name}(#{@real_current_user.id}) impersonating #{@current_user.name} on page #{request.url}"
|
||||
end
|
||||
if auth_context[:real_current_user]
|
||||
@real_current_user = auth_context[:real_current_user]
|
||||
@real_current_pseudonym = auth_context[:real_current_pseudonym]
|
||||
logger.warn "[AUTH] #{@real_current_user.name}(#{@real_current_user.id}) impersonating #{@current_user.name} on page #{request.url}"
|
||||
end
|
||||
@authenticated_with_jwt = @authenticated_with_inst_access_token = true
|
||||
end
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# Copyright (C) 2021 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
module AuthenticationMethods
|
||||
# this module bridges the gap between the token
|
||||
# defined in the canvas_security gem and the
|
||||
# canvas domain itself (users, pseudonyms, accounts, etc)
|
||||
module InstAccessToken
|
||||
|
||||
# given a POTENTIAL token string, this will validate
|
||||
# it as being an InstAccess token and return
|
||||
# the token ruby object.
|
||||
#
|
||||
# A 'false' indicates this is not a token at all and we can skip
|
||||
# any other attempts at token processing.
|
||||
#
|
||||
# an error (AccessTokenError) means that this IS an inst access token, but
|
||||
# not a valid one (expired or bad signature) and processing should only
|
||||
# continue on the assumption that this is an invalid request.
|
||||
def self.parse(token_string)
|
||||
return false unless InstAccess::Token.is_token?(token_string)
|
||||
|
||||
begin
|
||||
token = InstAccess::Token.from_token_string(token_string)
|
||||
return token
|
||||
rescue InstAccess::InvalidToken, # token didn't pass signature verification
|
||||
InstAccess::TokenExpired # token passed signature verification, but is expired
|
||||
raise AccessTokenError
|
||||
rescue InstAccess::ConfigError => exception
|
||||
# InstAccess isn't configured. A human should fix that, but this method
|
||||
# should recover gracefully.
|
||||
Canvas::Errors.capture_exception(:inst_access, exception, :warn)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
# functionally encapsulates mapping an InstAccess token and a domain root account
|
||||
# to a user/pseudonym. This is out on it's own because there are some db-state
|
||||
# edge cases (like multiple users with the same UUID due to user merges, etc)
|
||||
# that are convenient to test close to the implementation.
|
||||
#
|
||||
# the hash this method returns is defined up front with the intention
|
||||
# that the masquerading keys will only have their values populated if the token contains these values
|
||||
def self.load_user_and_pseudonym_context(token, domain_root_account)
|
||||
auth_context = {
|
||||
current_user: nil,
|
||||
current_pseudonym: nil,
|
||||
real_current_user: nil,
|
||||
real_current_pseudonym: nil
|
||||
}
|
||||
auth_context[:current_user] = find_user_by_uuid_prefer_local(token.user_uuid)
|
||||
auth_context[:current_pseudonym] = SisPseudonym.for(
|
||||
auth_context[:current_user], domain_root_account, type: :implicit, require_sis: false
|
||||
)
|
||||
return auth_context unless auth_context[:current_user] && auth_context[:current_pseudonym]
|
||||
|
||||
if token.masquerading_user_uuid && token.masquerading_user_shard_id
|
||||
Shard.lookup(token.masquerading_user_shard_id).activate do
|
||||
real_user = find_user_by_uuid_prefer_local(token.masquerading_user_uuid)
|
||||
raise AccessTokenError, "masquerading user not found" unless real_user
|
||||
auth_context[:real_current_user] = real_user
|
||||
auth_context[:real_current_pseudonym] = SisPseudonym.for(
|
||||
real_user, domain_root_account, type: :implicit, require_sis: false
|
||||
)
|
||||
end
|
||||
end
|
||||
return auth_context
|
||||
end
|
||||
|
||||
# generally users should not share uuids.
|
||||
# this is just to make sure that when a shadow
|
||||
# user or similar exists, the local user
|
||||
# gets preferred.
|
||||
def self.find_user_by_uuid_prefer_local(uuid)
|
||||
User.where(uuid: uuid).order(:id).first
|
||||
end
|
||||
private_class_method :find_user_by_uuid_prefer_local
|
||||
end
|
||||
end
|
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# Copyright (C) 2021 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
require_relative '../../sharding_spec_helper'
|
||||
|
||||
describe AuthenticationMethods::InstAccessToken do
|
||||
let(:signing_keypair) { OpenSSL::PKey::RSA.new(2048) }
|
||||
let(:encryption_keypair) { OpenSSL::PKey::RSA.new(2048) }
|
||||
let(:signing_priv_key) { signing_keypair.to_s }
|
||||
let(:signing_pub_key) { signing_keypair.public_key.to_s }
|
||||
let(:encryption_priv_key) { encryption_keypair.to_s }
|
||||
let(:encryption_pub_key) { encryption_keypair.public_key.to_s }
|
||||
|
||||
around(:each) do |example|
|
||||
InstAccess.with_config(signing_key: signing_priv_key) do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
describe ".parse" do
|
||||
it "is false for bad tokens" do
|
||||
result = ::AuthenticationMethods::InstAccessToken.parse("not-a-token")
|
||||
expect(result).to be_falsey
|
||||
end
|
||||
|
||||
it "returns a token object for good tokens" do
|
||||
token_obj = ::InstAccess::Token.for_user(user_uuid: 'fake-user-uuid', account_uuid: 'fake-acct-uuid')
|
||||
result = ::AuthenticationMethods::InstAccessToken.parse(token_obj.to_unencrypted_token_string)
|
||||
expect(result.user_uuid).to eq('fake-user-uuid')
|
||||
end
|
||||
end
|
||||
|
||||
describe ".load_user_and_pseudonym_context" do
|
||||
it "finds the user who created the token" do
|
||||
account = Account.default
|
||||
user_with_pseudonym(:active_all => true)
|
||||
token_obj = ::InstAccess::Token.for_user(user_uuid: @user.uuid, account_uuid: account.uuid)
|
||||
ctx = ::AuthenticationMethods::InstAccessToken.load_user_and_pseudonym_context(token_obj, account)
|
||||
expect(ctx[:current_user]).to eq(@user)
|
||||
expect(ctx[:current_pseudonym]).to eq(@pseudonym)
|
||||
end
|
||||
|
||||
it "chooses the local user when a local and shadow user share the same UUID" do
|
||||
user_model(id: (10_000_000_000_000 + (rand*10000000).to_i), uuid: 'a-shared-uuid-between-users')
|
||||
account = Account.default
|
||||
user_with_pseudonym(:active_all => true)
|
||||
@user.uuid = 'a-shared-uuid-between-users'
|
||||
@user.save!
|
||||
token_obj = ::InstAccess::Token.for_user(user_uuid: 'a-shared-uuid-between-users', account_uuid: account.uuid)
|
||||
ctx = ::AuthenticationMethods::InstAccessToken.load_user_and_pseudonym_context(token_obj, account)
|
||||
expect(ctx[:current_user]).to eq(@user)
|
||||
expect(ctx[:current_pseudonym]).to eq(@pseudonym)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue