From 7eb5035d94ed67927d3f638ebd34d89003507441 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 12 Sep 2025 10:53:11 -0400 Subject: [PATCH] [PM-22740] Update current context to jive with Send Access Tokens (#6307) * feat: modify current context to not include user information * fix: circular dependency for feature check in current context. Successfully tested client isn't affected with feature flag off. * test: whole bunch of tests for current context --- src/Core/Context/CurrentContext.cs | 39 +- test/Core.Test/Context/CurrentContextTests.cs | 733 ++++++++++++++++++ 2 files changed, 754 insertions(+), 18 deletions(-) create mode 100644 test/Core.Test/Context/CurrentContextTests.cs diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index e824a30a0e..5d9b5a1759 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -18,10 +18,10 @@ using Microsoft.AspNetCore.Http; namespace Bit.Core.Context; -public class CurrentContext : ICurrentContext +public class CurrentContext( + IProviderOrganizationRepository _providerOrganizationRepository, + IProviderUserRepository _providerUserRepository) : ICurrentContext { - private readonly IProviderOrganizationRepository _providerOrganizationRepository; - private readonly IProviderUserRepository _providerUserRepository; private bool _builtHttpContext; private bool _builtClaimsPrincipal; private IEnumerable _providerOrganizationProviderDetails; @@ -48,14 +48,6 @@ public class CurrentContext : ICurrentContext public virtual IdentityClientType IdentityClientType { get; set; } public virtual Guid? ServiceAccountOrganizationId { get; set; } - public CurrentContext( - IProviderOrganizationRepository providerOrganizationRepository, - IProviderUserRepository providerUserRepository) - { - _providerOrganizationRepository = providerOrganizationRepository; - _providerUserRepository = providerUserRepository; - } - public async virtual Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings) { if (_builtHttpContext) @@ -137,6 +129,24 @@ public class CurrentContext : ICurrentContext var claimsDict = user.Claims.GroupBy(c => c.Type).ToDictionary(c => c.Key, c => c.Select(v => v)); + ClientId = GetClaimValue(claimsDict, "client_id"); + + var clientType = GetClaimValue(claimsDict, Claims.Type); + if (clientType != null) + { + if (Enum.TryParse(clientType, out IdentityClientType c)) + { + IdentityClientType = c; + } + } + + if (IdentityClientType == IdentityClientType.Send) + { + // For the Send client, we don't need to set any User specific properties on the context + // so just short circuit and return here. + return Task.FromResult(0); + } + var subject = GetClaimValue(claimsDict, "sub"); if (Guid.TryParse(subject, out var subIdGuid)) { @@ -165,13 +175,6 @@ public class CurrentContext : ICurrentContext } } - var clientType = GetClaimValue(claimsDict, Claims.Type); - if (clientType != null) - { - Enum.TryParse(clientType, out IdentityClientType c); - IdentityClientType = c; - } - if (IdentityClientType == IdentityClientType.ServiceAccount) { ServiceAccountOrganizationId = new Guid(GetClaimValue(claimsDict, Claims.Organization)); diff --git a/test/Core.Test/Context/CurrentContextTests.cs b/test/Core.Test/Context/CurrentContextTests.cs new file mode 100644 index 0000000000..b868d6ceaa --- /dev/null +++ b/test/Core.Test/Context/CurrentContextTests.cs @@ -0,0 +1,733 @@ +using System.Security.Claims; +using Bit.Core.AdminConsole.Context; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.Identity; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Http; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Context; + +[SutProviderCustomize] +public class CurrentContextTests +{ + #region BuildAsync(HttpContext) Tests + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_SetsHttpContext( + SutProvider sutProvider) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + // Act + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + + // Assert + Assert.Equal(httpContext, sutProvider.Sut.HttpContext); + } + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_OnlyBuildsOnce( + SutProvider sutProvider) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + // Arrange + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + var firstContext = sutProvider.Sut.HttpContext; + + var secondHttpContext = new DefaultHttpContext(); + + // Act + await sutProvider.Sut.BuildAsync(secondHttpContext, globalSettings); + + // Assert + Assert.Equal(firstContext, sutProvider.Sut.HttpContext); + Assert.NotEqual(secondHttpContext, sutProvider.Sut.HttpContext); + } + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_SetsDeviceIdentifier( + SutProvider sutProvider, + string expectedValue) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + sutProvider.Sut.DeviceIdentifier = null; + // Arrange + httpContext.Request.Headers["Device-Identifier"] = expectedValue; + + // Act + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + + // Assert + Assert.Equal(expectedValue, sutProvider.Sut.DeviceIdentifier); + } + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_SetsCountryName( + SutProvider sutProvider, + string countryName) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + // Arrange + httpContext.Request.Headers["country-name"] = countryName; + + // Act + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + + // Assert + Assert.Equal(countryName, sutProvider.Sut.CountryName); + } + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_SetsDeviceType( + SutProvider sutProvider) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + // Arrange + var deviceType = DeviceType.Android; + httpContext.Request.Headers["Device-Type"] = ((int)deviceType).ToString(); + + // Act + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + + // Assert + Assert.Equal(deviceType, sutProvider.Sut.DeviceType); + } + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_SetsCloudflareFlags( + SutProvider sutProvider) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + sutProvider.Sut.BotScore = null; + // Arrange + var botScore = 85; + httpContext.Request.Headers["X-Cf-Bot-Score"] = botScore.ToString(); + httpContext.Request.Headers["X-Cf-Worked-Proxied"] = "1"; + httpContext.Request.Headers["X-Cf-Is-Bot"] = "1"; + httpContext.Request.Headers["X-Cf-Maybe-Bot"] = "1"; + + // Act + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + + // Assert + Assert.True(sutProvider.Sut.CloudflareWorkerProxied); + Assert.True(sutProvider.Sut.IsBot); + Assert.True(sutProvider.Sut.MaybeBot); + Assert.Equal(botScore, sutProvider.Sut.BotScore); + } + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_SetsClientVersion( + SutProvider sutProvider) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + // Arrange + var version = "2024.1.0"; + httpContext.Request.Headers["Bitwarden-Client-Version"] = version; + httpContext.Request.Headers["Is-Prerelease"] = "1"; + + // Act + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + + // Assert + Assert.Equal(new Version(version), sutProvider.Sut.ClientVersion); + Assert.True(sutProvider.Sut.ClientVersionIsPrerelease); + } + + #endregion + + #region SetContextAsync Tests + + [Theory, BitAutoData] + public async Task SetContextAsync_NullUser_DoesNotThrow( + SutProvider sutProvider) + { + // Act & Assert + await sutProvider.Sut.SetContextAsync(null); + // Should not throw + } + + [Theory, BitAutoData] + public async Task SetContextAsync_UserWithNoClaims_DoesNotThrow( + SutProvider sutProvider) + { + // Arrange + var user = new ClaimsPrincipal(); + + // Act & Assert + await sutProvider.Sut.SetContextAsync(user); + // Should not throw + } + + [Theory, BitAutoData] + public async Task SetContextAsync_SendClient_ShortCircuits( + SutProvider sutProvider, + Guid userId) + { + // Arrange + sutProvider.Sut.UserId = null; + var claims = new List + { + new(Claims.Type, IdentityClientType.Send.ToString()), + new("sub", userId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(IdentityClientType.Send, sutProvider.Sut.IdentityClientType); + Assert.Null(sutProvider.Sut.UserId); // Should not be set for Send clients + } + + [Theory, BitAutoData] + public async Task SetContextAsync_RegularUser_SetsUserId( + SutProvider sutProvider, + Guid userId, + string clientId) + { + // Arrange + var claims = new List + { + new("sub", userId.ToString()), + new("client_id", clientId) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(userId, sutProvider.Sut.UserId); + Assert.Equal(clientId, sutProvider.Sut.ClientId); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_InstallationClient_SetsInstallationId( + SutProvider sutProvider, + Guid installationId) + { + // Arrange + var claims = new List + { + new("client_id", "installation.12345"), + new("client_sub", installationId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(installationId, sutProvider.Sut.InstallationId); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_OrganizationClient_SetsOrganizationId( + SutProvider sutProvider, + Guid organizationId) + { + // Arrange + var claims = new List + { + new("client_id", "organization.12345"), + new("client_sub", organizationId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(organizationId, sutProvider.Sut.OrganizationId); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_ServiceAccount_SetsServiceAccountOrganizationId( + SutProvider sutProvider, + Guid organizationId) + { + // Arrange + var claims = new List + { + new(Claims.Type, IdentityClientType.ServiceAccount.ToString()), + new(Claims.Organization, organizationId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(IdentityClientType.ServiceAccount, sutProvider.Sut.IdentityClientType); + Assert.Equal(organizationId, sutProvider.Sut.ServiceAccountOrganizationId); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_WithDeviceClaims_SetsDeviceInfo( + SutProvider sutProvider, + string deviceIdentifier) + { + // Arrange + var claims = new List + { + new(Claims.Device, deviceIdentifier), + new(Claims.DeviceType, ((int)DeviceType.iOS).ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(deviceIdentifier, sutProvider.Sut.DeviceIdentifier); + Assert.Equal(DeviceType.iOS, sutProvider.Sut.DeviceType); + } + + #endregion + + #region Organization Claims Tests + + [Theory] + [BitAutoData(Claims.OrganizationOwner, OrganizationUserType.Owner)] + [BitAutoData(Claims.OrganizationAdmin, OrganizationUserType.Admin)] + [BitAutoData(Claims.OrganizationUser, OrganizationUserType.User)] + public async Task SetContextAsync_OrganizationClaims_SetsOrganizations( + string userOrgAssociation, + OrganizationUserType userType, + SutProvider sutProvider, + Guid org1Id, + Guid org2Id) + { + // Arrange + var claims = new List + { + new(userOrgAssociation, org1Id.ToString()), + new(userOrgAssociation, org2Id.ToString()), + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(2, sutProvider.Sut.Organizations.Count); + Assert.All(sutProvider.Sut.Organizations, org => Assert.Equal(userType, org.Type)); + Assert.Contains(sutProvider.Sut.Organizations, org => org.Id == org1Id); + Assert.Contains(sutProvider.Sut.Organizations, org => org.Id == org2Id); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_OrganizationCustomClaims_SetsOrganizationsWithPermissions( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + var claims = new List + { + new(Claims.OrganizationCustom, orgId.ToString()), + new("accesseventlogs", orgId.ToString()), + new("manageusers", orgId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Single(sutProvider.Sut.Organizations); + var org = sutProvider.Sut.Organizations.First(); + Assert.Equal(OrganizationUserType.Custom, org.Type); + Assert.Equal(orgId, org.Id); + Assert.True(org.Permissions.AccessEventLogs); + Assert.True(org.Permissions.ManageUsers); + Assert.False(org.Permissions.ManageGroups); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_SecretsManagerAccess_SetsAccessSecretsManager( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + var claims = new List + { + new(Claims.OrganizationOwner, orgId.ToString()), + new(Claims.SecretsManagerAccess, orgId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Single(sutProvider.Sut.Organizations); + Assert.True(sutProvider.Sut.Organizations.First().AccessSecretsManager); + } + + #endregion + + #region Provider Claims Tests + + [Theory, BitAutoData] + public async Task SetContextAsync_ProviderAdminClaims_SetsProviders( + SutProvider sutProvider, + Guid providerId) + { + // Arrange + var claims = new List + { + new(Claims.ProviderAdmin, providerId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Single(sutProvider.Sut.Providers); + Assert.Equal(ProviderUserType.ProviderAdmin, sutProvider.Sut.Providers.First().Type); + Assert.Equal(providerId, sutProvider.Sut.Providers.First().Id); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_ProviderServiceUserClaims_SetsProviders( + SutProvider sutProvider, + Guid providerId) + { + // Arrange + var claims = new List + { + new(Claims.ProviderServiceUser, providerId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Single(sutProvider.Sut.Providers); + Assert.Equal(ProviderUserType.ServiceUser, sutProvider.Sut.Providers.First().Type); + Assert.Equal(providerId, sutProvider.Sut.Providers.First().Id); + } + + #endregion + + #region Organization Permission Tests + + [Theory, BitAutoData] + public async Task OrganizationUser_WithDirectAccess_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() { Id = orgId, Type = OrganizationUserType.User } + }; + + // Act + var result = await sutProvider.Sut.OrganizationUser(orgId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task OrganizationUser_WithoutAccess_ReturnsFalse( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List(); + + // Act + var result = await sutProvider.Sut.OrganizationUser(orgId); + + // Assert + Assert.False(result); + } + + [Theory, BitAutoData] + public async Task OrganizationAdmin_WithAdminAccess_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() { Id = orgId, Type = OrganizationUserType.Admin } + }; + + // Act + var result = await sutProvider.Sut.OrganizationAdmin(orgId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task OrganizationOwner_WithOwnerAccess_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() { Id = orgId, Type = OrganizationUserType.Owner } + }; + + // Act + var result = await sutProvider.Sut.OrganizationOwner(orgId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task OrganizationCustom_WithCustomAccess_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() { Id = orgId, Type = OrganizationUserType.Custom } + }; + + // Act + var result = await sutProvider.Sut.OrganizationCustom(orgId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task AccessEventLogs_WithPermission_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() + { + Id = orgId, + Type = OrganizationUserType.Custom, + Permissions = new Permissions { AccessEventLogs = true } + } + }; + + // Act + var result = await sutProvider.Sut.AccessEventLogs(orgId); + + // Assert + Assert.True(result); + } + + #endregion + + #region Provider Permission Tests + + [Theory, BitAutoData] + public void ProviderProviderAdmin_WithAdminAccess_ReturnsTrue( + SutProvider sutProvider, + Guid providerId) + { + // Arrange + sutProvider.Sut.Providers = new List + { + new() { Id = providerId, Type = ProviderUserType.ProviderAdmin } + }; + + // Act + var result = sutProvider.Sut.ProviderProviderAdmin(providerId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public void ProviderUser_WithAnyAccess_ReturnsTrue( + SutProvider sutProvider, + Guid providerId) + { + // Arrange + sutProvider.Sut.Providers = new List + { + new() { Id = providerId, Type = ProviderUserType.ServiceUser } + }; + + // Act + var result = sutProvider.Sut.ProviderUser(providerId); + + // Assert + Assert.True(result); + } + + #endregion + + #region Secrets Manager Tests + + [Theory, BitAutoData] + public void AccessSecretsManager_WithServiceAccount_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.ServiceAccountOrganizationId = orgId; + + // Act + var result = sutProvider.Sut.AccessSecretsManager(orgId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public void AccessSecretsManager_WithOrgAccess_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() { Id = orgId, AccessSecretsManager = true } + }; + + // Act + var result = sutProvider.Sut.AccessSecretsManager(orgId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public void AccessSecretsManager_WithoutAccess_ReturnsFalse( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() { Id = orgId, AccessSecretsManager = false } + }; + + // Act + var result = sutProvider.Sut.AccessSecretsManager(orgId); + + // Assert + Assert.False(result); + } + + #endregion + + #region Membership Loading Tests + + [Theory, BitAutoData] + public async Task OrganizationMembershipAsync_LoadsFromRepository( + SutProvider sutProvider, + Guid userId, + List userOrgs) + { + // Arrange + sutProvider.Sut.UserId = userId; + sutProvider.Sut.Organizations = null; + var organizationUserRepository = Substitute.For(); + userOrgs.ForEach(org => org.Status = OrganizationUserStatusType.Confirmed); + + // Test complains about the JSON object that we store permissions as, so just set to empty object to pass the test. + userOrgs.ForEach(org => org.Permissions = "{}"); + organizationUserRepository.GetManyDetailsByUserAsync(userId) + .Returns(userOrgs); + + // Act + var result = await sutProvider.Sut.OrganizationMembershipAsync(organizationUserRepository, userId); + + // Assert + Assert.Equal(userOrgs.Count, result.Count); + Assert.Equal(userId, sutProvider.Sut.UserId); + await organizationUserRepository.Received(1).GetManyDetailsByUserAsync(userId); + } + + [Theory, BitAutoData] + public async Task ProviderMembershipAsync_LoadsFromRepository( + SutProvider sutProvider, + Guid userId, + List userProviders) + { + // Arrange + sutProvider.Sut.UserId = userId; + sutProvider.Sut.Providers = null; + + var providerUserRepository = Substitute.For(); + userProviders.ForEach(provider => provider.Status = ProviderUserStatusType.Confirmed); + + // Test complains about the JSON object that we store permissions as, so just set to empty object to pass the test. + userProviders.ForEach(provider => provider.Permissions = "{}"); + providerUserRepository.GetManyByUserAsync(userId) + .Returns(userProviders); + + // Act + var result = await sutProvider.Sut.ProviderMembershipAsync(providerUserRepository, userId); + + // Assert + Assert.Equal(userProviders.Count, result.Count); + Assert.Equal(userId, sutProvider.Sut.UserId); + await providerUserRepository.Received(1).GetManyByUserAsync(userId); + } + + #endregion + + #region Utility Tests + + [Theory, BitAutoData] + public void GetOrganization_WithExistingOrg_ReturnsOrganization( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + var org = new CurrentContextOrganization { Id = orgId }; + sutProvider.Sut.Organizations = new List { org }; + + // Act + var result = sutProvider.Sut.GetOrganization(orgId); + + // Assert + Assert.Equal(org, result); + } + + [Theory, BitAutoData] + public void GetOrganization_WithNonExistingOrg_ReturnsNull( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List(); + + // Act + var result = sutProvider.Sut.GetOrganization(orgId); + + // Assert + Assert.Null(result); + } + + #endregion +}