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

chore(playwrighttesting): removed fallback authentication logic #46667

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ internal class Constants
internal static readonly string s_invalid_mpt_pat_error = "The Access Token provided in the environment variable is invalid.";
internal static readonly string s_expired_mpt_pat_error = "The Access Token you are using is expired. Create a new token.";
internal static readonly string s_invalid_os_error = "Invalid operating system, supported values are 'linux' and 'windows'.";
internal static readonly string s_workspace_mismatch_error = "The provided access token does not match the specified workspace URL. Please verify that both values are correct.";
internal static readonly string s_invalid_service_endpoint_error_message = "The service endpoint provided is invalid. Please verify the endpoint URL and try again.";

internal static readonly string s_playwright_service_disable_scalable_execution_environment_variable = "PLAYWRIGHT_SERVICE_DISABLE_SCALABLE_EXECUTION";
internal static readonly string s_playwright_service_reporting_url_environment_variable = "PLAYWRIGHT_SERVICE_REPORTING_URL";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public EntraLifecycle(TokenCredential? tokenCredential = null, JsonWebTokenHandl
SetEntraIdAccessTokenFromEnvironment();
}

internal async Task<bool> FetchEntraIdAccessTokenAsync(CancellationToken cancellationToken = default)
internal async Task FetchEntraIdAccessTokenAsync(CancellationToken cancellationToken = default)
{
try
{
Expand All @@ -33,12 +33,12 @@ internal async Task<bool> FetchEntraIdAccessTokenAsync(CancellationToken cancell
_entraIdAccessToken = accessToken.Token;
_entraIdAccessTokenExpiry = accessToken.ExpiresOn.ToUnixTimeSeconds();
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceAccessToken, _entraIdAccessToken);
return true;
return;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return false;
throw new Exception(Constants.s_no_auth_error);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.IdentityModel.JsonWebTokens;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
Expand Down Expand Up @@ -109,7 +110,7 @@ internal PlaywrightService(OSPlatform? os = null, string? runId = null, string?
// 2. Not close to expiry
if (!string.IsNullOrEmpty(_entraLifecycle!._entraIdAccessToken) && _entraLifecycle!.DoesEntraIdAccessTokenRequireRotation())
{
_ = await _entraLifecycle.FetchEntraIdAccessTokenAsync(cancellationToken).ConfigureAwait(false);
await _entraLifecycle.FetchEntraIdAccessTokenAsync(cancellationToken).ConfigureAwait(false);
}
if (string.IsNullOrEmpty(GetAuthToken()))
{
Expand Down Expand Up @@ -146,24 +147,15 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default)
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceUri, null);
}
// If default auth mechanism is Access token and token is available in the environment variable, no need to setup rotation handler
if (ServiceAuth == ServiceAuthType.AccessToken && !string.IsNullOrEmpty(GetAuthToken()))
if (ServiceAuth == ServiceAuthType.AccessToken)
{
ValidateMptPAT();
return;
}
var operationStatus = await _entraLifecycle!.FetchEntraIdAccessTokenAsync(cancellationToken).ConfigureAwait(false);
if (!operationStatus)
{
if (!string.IsNullOrEmpty(GetAuthToken()))
{
ValidateMptPAT(); // throws exception if token is invalid
}
return; // no need to setup rotation handler. If token is not available, it will fallback to local browser launch
await _entraLifecycle!.FetchEntraIdAccessTokenAsync(cancellationToken).ConfigureAwait(false);
RotationTimer = new Timer(RotationHandlerAsync, null, TimeSpan.FromMinutes(Constants.s_entra_access_token_rotation_interval_period_in_minutes), TimeSpan.FromMinutes(Constants.s_entra_access_token_rotation_interval_period_in_minutes));
}

RotationTimer = new Timer(RotationHandlerAsync, null, TimeSpan.FromMinutes(Constants.s_entra_access_token_rotation_interval_period_in_minutes), TimeSpan.FromMinutes(Constants.s_entra_access_token_rotation_interval_period_in_minutes));
}

/// <summary>
/// Cleans up the resources used to setup entra id authentication.
/// </summary>
Expand Down Expand Up @@ -251,18 +243,20 @@ internal static void SetReportingUrlAndWorkspaceId()

private void ValidateMptPAT()
{
try
{
string authToken = GetAuthToken()!;
if (string.IsNullOrEmpty(authToken))
throw new Exception(Constants.s_no_auth_error);
JsonWebToken jsonWebToken = _jsonWebTokenHandler!.ReadJsonWebToken(authToken) ?? throw new Exception(Constants.s_invalid_mpt_pat_error);
var tokenaWorkspaceId = jsonWebToken.Claims.FirstOrDefault(c => c.Type == "aid")?.Value;
kashish2508 marked this conversation as resolved.
Show resolved Hide resolved
Match match = Regex.Match(ServiceEndpoint, @"wss://(?<region>[\w-]+)\.api\.(?<domain>playwright(?:-test|-int)?\.io|playwright\.microsoft\.com)/accounts/(?<workspaceId>[\w-]+)/");
if (!match.Success)
throw new Exception(Constants.s_invalid_service_endpoint_error_message);
var serviceEndpointWorkspaceId = match.Groups["workspaceId"].Value;
if (tokenaWorkspaceId != serviceEndpointWorkspaceId)
throw new Exception(Constants.s_workspace_mismatch_error);
var expiry = (long)(jsonWebToken.ValidTo - new DateTime(1970, 1, 1)).TotalSeconds;
if (expiry <= DateTimeOffset.UtcNow.ToUnixTimeSeconds())
throw new Exception(Constants.s_expired_mpt_pat_error);
}
catch (Exception)
{
throw new Exception(Constants.s_invalid_mpt_pat_error);
}
}

private string? getServiceCompatibleOs(OSPlatform? oSPlatform)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Runtime.InteropServices;
using Azure.Core;
using Azure.Identity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public async Task FetchEntraIdAccessTokenAsync_WhenTokenIsFetched_SetsTokenAndEx
}
kashish2508 marked this conversation as resolved.
Show resolved Hide resolved

[Test]
public async Task FetchEntraIdAccessTokenAsync_WhenTokenIsFetched_ReturnsTrue()
public async Task FetchEntraIdAccessTokenAsync_WhenTokenIsFetched_ReturnVoid()
{
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
var token = "valid_token";
Expand All @@ -141,20 +141,23 @@ public async Task FetchEntraIdAccessTokenAsync_WhenTokenIsFetched_ReturnsTrue()
.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AccessToken(token, expiry));
EntraLifecycle entraLifecycle = new(defaultAzureCredentialMock.Object);
Assert.That(await entraLifecycle.FetchEntraIdAccessTokenAsync(), Is.True);
await entraLifecycle.FetchEntraIdAccessTokenAsync();

Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceAccessToken, null);
}

[Test]
public async Task FetchEntraIdAccessTokenAsync_WhenThrowsError_ReturnsFalse()
public void FetchEntraIdAccessTokenAsync_WhenThrowsError()
{
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
defaultAzureCredentialMock
.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception("sample exception"));
EntraLifecycle entraLifecycle = new(defaultAzureCredentialMock.Object);
Assert.That(await entraLifecycle.FetchEntraIdAccessTokenAsync(), Is.False);
Exception? ex = Assert.ThrowsAsync<Exception>(async () =>
await entraLifecycle.FetchEntraIdAccessTokenAsync());

Assert.That(ex!.Message, Is.EqualTo(Constants.s_no_auth_error));
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ public void Initialize_WhenFetchesEntraIdAccessToken_SetsUpRotationHandler()
}

[Test]
public void Initialize_WhenFailsToFetchEntraIdAccessToken_DoesNotSetUpRotationHandler()
public void Initialize_WhenFailsToFetchEntraIdAccessToken_ThrowsException()
{
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
var jsonWebTokenHandlerMock = new Mock<JsonWebTokenHandler>();
Expand All @@ -238,13 +238,12 @@ public void Initialize_WhenFailsToFetchEntraIdAccessToken_DoesNotSetUpRotationHa
.ThrowsAsync(new Exception());
var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, jsonWebTokenHandlerMock.Object);
PlaywrightService service = new(entraLifecycle: entraLifecycleMock.Object);
service.InitializeAsync().Wait();
defaultAzureCredentialMock.Verify(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()), Times.Once);
Assert.That(service.RotationTimer, Is.Null);
Exception? ex = Assert.ThrowsAsync<Exception>(async () => await service.InitializeAsync());
Assert.That(ex!.Message, Is.EqualTo(Constants.s_no_auth_error));
}

[Test]
public void Initialize_WhenEntraIdAccessTokenFailsAndMptPatIsSet_DoesNotSetUpRotationHandler()
public void Initialize_WhenEntraIdAccessTokenFailsAndMptPatIsSet_ThrowsException()
{
var token = GetToken(new Dictionary<string, object>
{
Expand All @@ -257,12 +256,12 @@ public void Initialize_WhenEntraIdAccessTokenFailsAndMptPatIsSet_DoesNotSetUpRot
.ThrowsAsync(new Exception());
var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, new JsonWebTokenHandler());
PlaywrightService service = new(entraLifecycle: entraLifecycleMock.Object, jsonWebTokenHandler: new JsonWebTokenHandler());
service.InitializeAsync().Wait();
Assert.That(service.RotationTimer, Is.Null);
Exception? ex = Assert.ThrowsAsync<Exception>(async () => await service.InitializeAsync());
Assert.That(ex!.Message, Is.EqualTo(Constants.s_no_auth_error));
}

[Test]
public void Initialize_WhenEntraIdAccessTokenFailsAndMptPatIsNotSet_DoesNotSetUpRotationHandler()
public void Initialize_WhenEntraIdAccessTokenFailsAndMptPatIsNotSet_ThrowsException()
{
var token = GetToken(new Dictionary<string, object>
{
Expand All @@ -274,8 +273,8 @@ public void Initialize_WhenEntraIdAccessTokenFailsAndMptPatIsNotSet_DoesNotSetUp
.ThrowsAsync(new Exception());
var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, new JsonWebTokenHandler());
PlaywrightService service = new(entraLifecycle: entraLifecycleMock.Object, jsonWebTokenHandler: new JsonWebTokenHandler());
service.InitializeAsync().Wait();
Assert.That(service.RotationTimer, Is.Null);
Exception? ex = Assert.ThrowsAsync<Exception>(async () => await service.InitializeAsync());
Assert.That(ex!.Message, Is.EqualTo(Constants.s_no_auth_error));
}

[Test]
Expand Down Expand Up @@ -335,9 +334,17 @@ public void Initialize_WhenDefaultAuthIsMptPATAndPATIsSet_DoesNotSetUpRotationHa
{
var token = GetToken(new Dictionary<string, object>
{
{"aid", "account-id-guid"},
{"aid", "eastus_bd830e63-6120-40cb-8cd7-f0739502d888"},
});
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceAccessToken, token);
var testRubric = new Dictionary<string, string>
{
{ "url", "wss://eastus.api.playwright.microsoft.com/accounts/eastus_bd830e63-6120-40cb-8cd7-f0739502d888/browsers" },
{ "workspaceId", "eastus_bd830e63-6120-40cb-8cd7-f0739502d888" },
{ "region", "eastus" },
{ "domain", "playwright.microsoft.com" }
};
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceUri, $"{testRubric["url"]}");
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, new JsonWebTokenHandler());
PlaywrightService service = new(entraLifecycle: entraLifecycleMock.Object, jsonWebTokenHandler: new JsonWebTokenHandler(), serviceAuth: ServiceAuthType.AccessToken);
Expand Down