blob: 6a0bbab87fe5c315291bbfecd8c30fd843d30805 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <string>
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/run_loop.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/arc/arc_support_host.h"
#include "chrome/browser/chromeos/arc/auth/arc_active_directory_enrollment_token_fetcher.h"
#include "chrome/browser/chromeos/arc/extensions/fake_arc_support.h"
#include "chrome/browser/chromeos/policy/browser_policy_connector_chromeos.h"
#include "chrome/browser/chromeos/policy/dm_token_storage.h"
#include "chrome/browser/chromeos/profiles/profile_helper.h"
#include "chrome/browser/policy/cloud/test_request_interceptor.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/fake_cryptohome_client.h"
#include "components/arc/arc_util.h"
#include "components/policy/core/common/cloud/device_management_service.h"
#include "components/policy/core/common/policy_switches.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/browser_thread.h"
#include "net/base/network_delegate.h"
#include "net/base/upload_bytes_element_reader.h"
#include "net/base/upload_data_stream.h"
#include "net/base/url_util.h"
#include "net/http/http_request_headers.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_test_job.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace em = enterprise_management;
namespace arc {
namespace {
constexpr char kFakeDmToken[] = "fake-dm-token";
constexpr char kFakeEnrollmentToken[] = "fake-enrollment-token";
constexpr char kFakeUserId[] = "fake-user-id";
constexpr char kFakeAuthSessionId[] = "fake-auth-session-id";
constexpr char kFakeAdfsServerUrl[] = "http://example.com/adfs/ls/awesome.aspx";
constexpr char kNotYetFetched[] = "NOT-YET-FETCHED";
constexpr char kBadRequestHeader[] = "HTTP/1.1 400 Bad Request";
using Status = ArcActiveDirectoryEnrollmentTokenFetcher::Status;
std::string GetDmServerUrl() {
policy ::BrowserPolicyConnectorChromeOS* const connector =
g_browser_process->platform_part()->browser_policy_connector_chromeos();
return connector->device_management_service()->GetServerUrl();
}
// Observer for FakeArcSupport.
class FakeArcSupportObserverBase : public FakeArcSupport::Observer {
public:
explicit FakeArcSupportObserverBase(FakeArcSupport* fake_arc_support)
: fake_arc_support_(fake_arc_support) {}
// Called when the Active Directory auth page is shown.
virtual void OnAuthPageShown() = 0;
void OnPageChanged(ArcSupportHost::UIPage page) override {
if (page != ArcSupportHost::UIPage::ACTIVE_DIRECTORY_AUTH)
return;
EXPECT_EQ(kFakeAdfsServerUrl,
fake_arc_support_->active_directory_auth_federation_url());
EXPECT_EQ(GetDmServerUrl(),
fake_arc_support_
->active_directory_auth_device_management_url_prefix());
OnAuthPageShown();
}
protected:
~FakeArcSupportObserverBase() override = default;
FakeArcSupport* const fake_arc_support_; // Not owned.
};
// Simulates SAML authentication success.
class SimulateAuthSucceedsObserver : public FakeArcSupportObserverBase {
public:
explicit SimulateAuthSucceedsObserver(FakeArcSupport* fake_arc_support)
: FakeArcSupportObserverBase(fake_arc_support) {}
void OnAuthPageShown() override { fake_arc_support_->EmulateAuthSuccess(); }
};
// Simulates pressing the Cancel button or closing the window.
class SimulateAuthCancelledObserver : public FakeArcSupportObserverBase {
public:
SimulateAuthCancelledObserver(FakeArcSupport* fake_arc_support,
base::RunLoop* run_loop)
: FakeArcSupportObserverBase(fake_arc_support), run_loop_(run_loop) {}
void OnAuthPageShown() override {
fake_arc_support_->Close();
// Since ArcActiveDirectoryEnrollmentTokenFetcher won't call the
// FetchCallback in case of an error, we break the runloop manually.
run_loop_->Quit();
}
private:
base::RunLoop* const run_loop_; // Not owned.
};
// Simulates SAML authentication failure.
class SimulateAuthFailsObserver : public FakeArcSupportObserverBase {
public:
SimulateAuthFailsObserver(FakeArcSupport* fake_arc_support,
base::RunLoop* run_loop)
: FakeArcSupportObserverBase(fake_arc_support), run_loop_(run_loop) {}
void OnAuthPageShown() override {
fake_arc_support_->EmulateAuthFailure("error");
// Since ArcActiveDirectoryEnrollmentTokenFetcher won't call the
// FetchCallback in case of an error, we break the runloop manually.
run_loop_->Quit();
}
private:
base::RunLoop* const run_loop_; // Not owned.
};
// Simulates SAML authentication retry.
class SimulateAuthRetryObserver : public FakeArcSupportObserverBase {
public:
SimulateAuthRetryObserver(FakeArcSupport* fake_arc_support,
base::RunLoop* run_loop)
: FakeArcSupportObserverBase(fake_arc_support), run_loop_(run_loop) {}
void OnAuthPageShown() override {
saml_auth_count_++;
if (saml_auth_count_ == 1) {
// First saml auth attempt, trigger error and retry.
fake_arc_support_->EmulateAuthFailure("error");
EXPECT_EQ(ArcSupportHost::UIPage::ERROR, fake_arc_support_->ui_page());
fake_arc_support_->ClickRetryButton();
} else if (saml_auth_count_ == 2) {
// Second saml auth attempt, trigger success.
fake_arc_support_->EmulateAuthSuccess();
} else {
ADD_FAILURE() << "Auth page should only be shown twice";
run_loop_->Quit();
}
}
private:
base::RunLoop* const run_loop_; // Not owned.
int saml_auth_count_ = 0;
};
} // namespace
// Checks whether |request| is a valid request to enroll a play user and returns
// the corresponding protobuf.
em::ActiveDirectoryEnrollPlayUserRequest CheckRequestAndGetEnrollRequest(
net::URLRequest* request) {
// Check the operation.
std::string request_type;
EXPECT_TRUE(
net::GetValueForKeyInQuery(request->url(), "request", &request_type));
EXPECT_EQ("active_directory_enroll_play_user", request_type);
// Check content of request.
const net::UploadDataStream* upload = request->get_upload();
EXPECT_TRUE(upload);
EXPECT_TRUE(upload->GetElementReaders());
EXPECT_EQ(1u, upload->GetElementReaders()->size());
EXPECT_TRUE((*upload->GetElementReaders())[0]->AsBytesReader());
const net::UploadBytesElementReader* bytes_reader =
(*upload->GetElementReaders())[0]->AsBytesReader();
// Check the DMToken.
net::HttpRequestHeaders request_headers = request->extra_request_headers();
std::string value;
EXPECT_TRUE(request_headers.GetHeader("Authorization", &value));
EXPECT_EQ("GoogleDMToken token=" + std::string(kFakeDmToken), value);
// Extract the actual request proto.
em::DeviceManagementRequest parsed_request;
EXPECT_TRUE(parsed_request.ParseFromArray(bytes_reader->bytes(),
bytes_reader->length()));
EXPECT_TRUE(parsed_request.has_active_directory_enroll_play_user_request());
return parsed_request.active_directory_enroll_play_user_request();
}
net::URLRequestJob* SendResponse(net::URLRequest* request,
net::NetworkDelegate* network_delegate,
const em::DeviceManagementResponse& response) {
std::string response_data;
EXPECT_TRUE(response.SerializeToString(&response_data));
return new net::URLRequestTestJob(request, network_delegate,
net::URLRequestTestJob::test_headers(),
response_data, true);
}
// If this gets called, the test will fail.
net::URLRequestJob* NotReachedResponseJob(
net::URLRequest* request,
net::NetworkDelegate* network_delegate) {
ADD_FAILURE() << "DMServer called when not expected";
return nullptr;
}
// JobCallback for the interceptor to test non-SAML flow.
net::URLRequestJob* NonSamlResponseJob(net::URLRequest* request,
net::NetworkDelegate* network_delegate) {
CheckRequestAndGetEnrollRequest(request);
// Response contains the enrollment token and user id.
em::DeviceManagementResponse response;
em::ActiveDirectoryEnrollPlayUserResponse* enroll_response =
response.mutable_active_directory_enroll_play_user_response();
enroll_response->set_enrollment_token(kFakeEnrollmentToken);
enroll_response->set_user_id(kFakeUserId);
return SendResponse(request, network_delegate, response);
}
// JobCallback for the interceptor to start the SAML flow.
net::URLRequestJob* InitiateSamlResponseJob(
net::URLRequest* request,
net::NetworkDelegate* network_delegate) {
em::ActiveDirectoryEnrollPlayUserRequest enroll_request =
CheckRequestAndGetEnrollRequest(request);
EXPECT_FALSE(enroll_request.has_auth_session_id());
// Response contains only SAML parameters to initialize the SAML flow.
em::DeviceManagementResponse response;
em::ActiveDirectoryEnrollPlayUserResponse* enroll_response =
response.mutable_active_directory_enroll_play_user_response();
em::SamlParametersProto* saml_parameters =
enroll_response->mutable_saml_parameters();
saml_parameters->set_auth_session_id(kFakeAuthSessionId);
saml_parameters->set_auth_redirect_url(kFakeAdfsServerUrl);
return SendResponse(request, network_delegate, response);
}
// JobCallback returns a 400 Bad Request.
net::URLRequestJob* BadRequestResponseJob(
net::URLRequest* request,
net::NetworkDelegate* network_delegate) {
return new net::URLRequestTestJob(request, network_delegate,
kBadRequestHeader, std::string(), true);
}
// JobCallback for the interceptor to end the SAML flow.
net::URLRequestJob* FinishSamlResponseJob(
net::URLRequest* request,
net::NetworkDelegate* network_delegate) {
DCHECK(request);
em::ActiveDirectoryEnrollPlayUserRequest enroll_request =
CheckRequestAndGetEnrollRequest(request);
EXPECT_TRUE(enroll_request.has_auth_session_id());
EXPECT_EQ(kFakeAuthSessionId, enroll_request.auth_session_id());
// Response contains the enrollment token and user id.
em::DeviceManagementResponse response;
em::ActiveDirectoryEnrollPlayUserResponse* enroll_response =
response.mutable_active_directory_enroll_play_user_response();
enroll_response->set_enrollment_token(kFakeEnrollmentToken);
enroll_response->set_user_id(kFakeUserId);
return SendResponse(request, network_delegate, response);
}
class ArcActiveDirectoryEnrollmentTokenFetcherBrowserTest
: public InProcessBrowserTest,
public ArcSupportHost::ErrorDelegate {
protected:
ArcActiveDirectoryEnrollmentTokenFetcherBrowserTest() = default;
~ArcActiveDirectoryEnrollmentTokenFetcherBrowserTest() override = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII(policy::switches::kDeviceManagementUrl,
"http://localhost");
SetArcAvailableCommandLineForTesting(command_line);
}
void SetUpInProcessBrowserTestFixture() override {
// Set fake cryptohome, because we want to fail DMToken retrieval
auto cryptohome_client = std::make_unique<chromeos::FakeCryptohomeClient>();
fake_cryptohome_client_ = cryptohome_client.get();
chromeos::DBusThreadManager::GetSetterForTesting()->SetCryptohomeClient(
std::move(cryptohome_client));
}
void SetUpOnMainThread() override {
interceptor_ = std::make_unique<policy::TestRequestInterceptor>(
"localhost", content::BrowserThread::GetTaskRunnerForThread(
content::BrowserThread::IO));
support_host_ = std::make_unique<ArcSupportHost>(browser()->profile());
support_host_->SetErrorDelegate(this);
fake_arc_support_ = std::make_unique<FakeArcSupport>(support_host_.get());
token_fetcher_ = std::make_unique<ArcActiveDirectoryEnrollmentTokenFetcher>(
support_host_.get());
}
void TearDownOnMainThread() override {
token_fetcher_.reset();
fake_arc_support_.reset();
support_host_->SetErrorDelegate(nullptr);
support_host_.reset();
interceptor_.reset();
}
// Stores a correct (fake) DM token.
// ArcActiveDirectoryEnrollmentTokenFetcher will succeed to fetch the DM
// token.
void StoreCorrectDmToken() {
fake_cryptohome_client_->set_system_salt(
chromeos::FakeCryptohomeClient::GetStubSystemSalt());
fake_cryptohome_client_->SetServiceIsAvailable(true);
// Store a fake DM token.
base::RunLoop run_loop;
auto dm_token_storage = std::make_unique<policy::DMTokenStorage>(
g_browser_process->local_state());
dm_token_storage->StoreDMToken(
kFakeDmToken, base::BindOnce(
[](base::RunLoop* run_loop, bool success) {
EXPECT_TRUE(success);
run_loop->Quit();
},
&run_loop));
// Because the StoreDMToken() operation interacts with the I/O thread,
// RunUntilIdle() won't work here. Instead, use Run() and Quit()
// explicitly in the callback.
run_loop.Run();
}
// Does not store a correct DM token.
// ArcActiveDirectoryEnrollmentTokenFetcher will fail to fetch the DM token.
void FailDmToken() {
fake_cryptohome_client_->set_system_salt(std::vector<uint8_t>());
fake_cryptohome_client_->SetServiceIsAvailable(true);
}
void FetchEnrollmentToken(base::RunLoop* run_loop,
Status* out_fetch_status,
std::string* out_enrollment_token,
std::string* out_user_id) {
token_fetcher_->Fetch(base::BindOnce(
[](base::RunLoop* run_loop, Status* out_fetch_status,
std::string* out_enrollment_token, std::string* out_user_id,
Status fetch_status, const std::string& enrollment_token,
const std::string& user_id) {
*out_fetch_status = fetch_status;
*out_enrollment_token = enrollment_token;
*out_user_id = user_id;
run_loop->Quit();
},
run_loop, out_fetch_status, out_enrollment_token, out_user_id));
// Because the Fetch() operation needs to interact with other threads,
// RunUntilIdle() won't work here. Instead, use Run() and Quit()
// explicitly in the callback.
run_loop->Run();
}
void FetchEnrollmentTokenAndExpectCallbackNotReached(
base::RunLoop* run_loop) {
token_fetcher_->Fetch(base::BindOnce(
[](Status fetch_status, const std::string& enrollment_token,
const std::string& user_id) { FAIL() << "Should not be called"; }));
run_loop->Run();
}
void ExpectEnrollmentTokenFetchSucceeds(base::RunLoop* run_loop) {
Status fetch_status = Status::FAILURE;
std::string enrollment_token;
std::string user_id;
FetchEnrollmentToken(run_loop, &fetch_status, &enrollment_token, &user_id);
// Verify expectations.
EXPECT_EQ(Status::SUCCESS, fetch_status);
EXPECT_EQ(kFakeEnrollmentToken, enrollment_token);
EXPECT_EQ(kFakeUserId, user_id);
}
void ExpectEnrollmentTokenFetchFails(base::RunLoop* run_loop,
Status expected_status) {
EXPECT_NE(expected_status, Status::SUCCESS);
Status fetch_status = Status::SUCCESS;
// The expected strings are empty, o initialize with non-empty value.
std::string enrollment_token = kNotYetFetched;
std::string user_id = kNotYetFetched;
FetchEnrollmentToken(run_loop, &fetch_status, &enrollment_token, &user_id);
// Verify expectations.
EXPECT_EQ(expected_status, fetch_status);
EXPECT_TRUE(enrollment_token.empty());
EXPECT_TRUE(user_id.empty());
}
std::unique_ptr<policy::TestRequestInterceptor> interceptor_;
std::unique_ptr<FakeArcSupport> fake_arc_support_;
std::unique_ptr<ArcActiveDirectoryEnrollmentTokenFetcher> token_fetcher_;
private:
ArcSupportHost::AuthDelegate* GetAuthDelegate() {
return static_cast<ArcSupportHost::AuthDelegate*>(token_fetcher_.get());
}
// ArcSupportHost:::ErrorDelegate:
// Settings these prevents some DCHECK failures.
void OnWindowClosed() override {}
void OnRetryClicked() override {}
void OnSendFeedbackClicked() override {}
std::unique_ptr<ArcSupportHost> support_host_;
// DBusThreadManager owns this.
chromeos::FakeCryptohomeClient* fake_cryptohome_client_;
DISALLOW_COPY_AND_ASSIGN(ArcActiveDirectoryEnrollmentTokenFetcherBrowserTest);
};
// Non-SAML flow fetches valid enrollment token and user id.
IN_PROC_BROWSER_TEST_F(ArcActiveDirectoryEnrollmentTokenFetcherBrowserTest,
RequestAccountInfoSuccess) {
StoreCorrectDmToken();
interceptor_->PushJobCallback(base::Bind(&NonSamlResponseJob));
base::RunLoop run_loop;
ExpectEnrollmentTokenFetchSucceeds(&run_loop);
}
// Failure to fetch DM token leads to token fetch failure.
IN_PROC_BROWSER_TEST_F(ArcActiveDirectoryEnrollmentTokenFetcherBrowserTest,
DmTokenRetrievalFailed) {
FailDmToken();
interceptor_->PushJobCallback(base::Bind(NotReachedResponseJob));
base::RunLoop run_loop;
ExpectEnrollmentTokenFetchFails(&run_loop, Status::FAILURE);
}
// Server responds with bad request and fails token fetch.
IN_PROC_BROWSER_TEST_F(ArcActiveDirectoryEnrollmentTokenFetcherBrowserTest,
RequestAccountInfoError) {
StoreCorrectDmToken();
interceptor_->PushJobCallback(
policy::TestRequestInterceptor::BadRequestJob());
base::RunLoop run_loop;
ExpectEnrollmentTokenFetchFails(&run_loop, Status::FAILURE);
}
// ARC disabled leads to failed token fetch.
IN_PROC_BROWSER_TEST_F(ArcActiveDirectoryEnrollmentTokenFetcherBrowserTest,
ArcDisabled) {
StoreCorrectDmToken();
interceptor_->PushJobCallback(
policy::TestRequestInterceptor::HttpErrorJob("904 ARC Disabled"));
base::RunLoop run_loop;
ExpectEnrollmentTokenFetchFails(&run_loop, Status::ARC_DISABLED);
}
// Successful enrollment token fetch including SAML authentication.
// SAML flow works as follows:
// 1) |token_fetcher_| sends a request to device management (DM) server to
// fetch the token.
// 2) DM server responds with an auth session id and a redirect URL to the
// ADFS server. This is emulated in InitiateSamlResponseJob.
// 3) |token_fetcher_| sets the redirect URL in |fake_arc_support_| and
// opens the Active Directory auth page, which triggers
// SimulateAuthSucceedsObserver's OnAuthPageShown() handler.
// 4) SimulateAuthSucceedsObserver triggers the onAuthSucceeded event, which
// causes |arc_support_host_| to call into |token_fetcher_|'s
// OnAuthSucceeded.
// 5) |token_fetcher_| sends another request to DM server to fetch the token,
// this time with the auth session id from 2).
// 6) DM server responds with the token. This is emulated in
// FinishSamlResponseJob.
IN_PROC_BROWSER_TEST_F(ArcActiveDirectoryEnrollmentTokenFetcherBrowserTest,
SamlFlowSuccess) {
StoreCorrectDmToken();
interceptor_->PushJobCallback(base::Bind(&InitiateSamlResponseJob));
interceptor_->PushJobCallback(base::Bind(&FinishSamlResponseJob));
base::RunLoop run_loop;
SimulateAuthSucceedsObserver observer(fake_arc_support_.get());
fake_arc_support_->AddObserver(&observer);
ExpectEnrollmentTokenFetchSucceeds(&run_loop);
fake_arc_support_->RemoveObserver(&observer);
}
// SAML flow fails since the user closed the window or clicked the Cancel
// button.
IN_PROC_BROWSER_TEST_F(ArcActiveDirectoryEnrollmentTokenFetcherBrowserTest,
SamlFlowFailsUserCancelled) {
StoreCorrectDmToken();
interceptor_->PushJobCallback(base::Bind(&InitiateSamlResponseJob));
base::RunLoop run_loop;
SimulateAuthCancelledObserver observer(fake_arc_support_.get(), &run_loop);
fake_arc_support_->AddObserver(&observer);
// On user cancel, the FetchCallback should not be called (in fact, the
// window closes silently).
ASSERT_NO_FATAL_FAILURE(
FetchEnrollmentTokenAndExpectCallbackNotReached(&run_loop));
fake_arc_support_->RemoveObserver(&observer);
}
// SAML flow fails because of an error.
IN_PROC_BROWSER_TEST_F(ArcActiveDirectoryEnrollmentTokenFetcherBrowserTest,
SamlFlowFailsError) {
StoreCorrectDmToken();
interceptor_->PushJobCallback(base::Bind(&InitiateSamlResponseJob));
base::RunLoop run_loop;
SimulateAuthFailsObserver observer(fake_arc_support_.get(), &run_loop);
fake_arc_support_->AddObserver(&observer);
// Similar to user cancel, the callback to Fetch should never be called.
// Instead, we should end up on the error page.
ASSERT_NO_FATAL_FAILURE(
FetchEnrollmentTokenAndExpectCallbackNotReached(&run_loop));
EXPECT_EQ(ArcSupportHost::UIPage::ERROR, fake_arc_support_->ui_page());
fake_arc_support_->RemoveObserver(&observer);
}
// SAML flow fails first during initial DM server request, but the retry
// works.
IN_PROC_BROWSER_TEST_F(ArcActiveDirectoryEnrollmentTokenFetcherBrowserTest,
SamlFlowSucceedsWithDmRetry) {
StoreCorrectDmToken();
interceptor_->PushJobCallback(base::Bind(&BadRequestResponseJob));
base::RunLoop failure_run_loop;
ExpectEnrollmentTokenFetchFails(&failure_run_loop, Status::FAILURE);
interceptor_->PushJobCallback(base::Bind(&InitiateSamlResponseJob));
interceptor_->PushJobCallback(base::Bind(&FinishSamlResponseJob));
base::RunLoop success_run_loop;
SimulateAuthSucceedsObserver observer(fake_arc_support_.get());
fake_arc_support_->AddObserver(&observer);
ExpectEnrollmentTokenFetchSucceeds(&success_run_loop);
fake_arc_support_->RemoveObserver(&observer);
}
// SAML flow fails first during SAML auth, but the retry works.
IN_PROC_BROWSER_TEST_F(ArcActiveDirectoryEnrollmentTokenFetcherBrowserTest,
SamlFlowSucceedsWithAuthRetry) {
StoreCorrectDmToken();
interceptor_->PushJobCallback(base::Bind(&InitiateSamlResponseJob));
interceptor_->PushJobCallback(base::Bind(&InitiateSamlResponseJob));
interceptor_->PushJobCallback(base::Bind(&FinishSamlResponseJob));
base::RunLoop run_loop;
SimulateAuthRetryObserver observer(fake_arc_support_.get(), &run_loop);
fake_arc_support_->AddObserver(&observer);
ExpectEnrollmentTokenFetchSucceeds(&run_loop);
EXPECT_EQ(0u, interceptor_->GetPendingSize());
fake_arc_support_->RemoveObserver(&observer);
}
} // namespace arc