1
0
mirror of https://github.com/bitwarden/server synced 2025-12-27 21:53:24 +00:00

Address pr feedback

This commit is contained in:
Matt Gibson
2025-11-18 15:58:42 -08:00
parent 58afdd5e5c
commit 833e181a5a
11 changed files with 491 additions and 68 deletions

View File

@@ -4,6 +4,11 @@ using Bit.Core.Utilities;
namespace Bit.Core.Entities;
/// <summary>
/// PlayData is a join table tracking entities created during automated testing.
/// A `PlayId` is supplied by the clients in the `x-play-id` header to inform the server
/// that any data created should be associated with the play, and therefore cleaned up with it.
/// </summary>
public class PlayData : ITableObject<Guid>
{
public Guid Id { get; set; }
@@ -12,14 +17,22 @@ public class PlayData : ITableObject<Guid>
public Guid? UserId { get; init; }
public Guid? OrganizationId { get; init; }
public DateTime CreationDate { get; init; }
protected PlayData() { }
/// <summary>
/// Generates and sets a new COMB GUID for the Id property.
/// </summary>
public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}
/// <summary>
/// Creates a new PlayData record associated with a User.
/// </summary>
/// <param name="user">The user entity created during the play.</param>
/// <param name="playId">The play identifier from the x-play-id header.</param>
/// <returns>A new PlayData instance tracking the user.</returns>
public static PlayData Create(User user, string playId)
{
return new PlayData
@@ -30,6 +43,12 @@ public class PlayData : ITableObject<Guid>
};
}
/// <summary>
/// Creates a new PlayData record associated with an Organization.
/// </summary>
/// <param name="organization">The organization entity created during the play.</param>
/// <param name="playId">The play identifier from the x-play-id header.</param>
/// <returns>A new PlayData instance tracking the organization.</returns>
public static PlayData Create(Organization organization, string playId)
{
return new PlayData

View File

@@ -0,0 +1,24 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Entities;
namespace Bit.Core.Services;
public interface IPlayDataService
{
/// <summary>
/// Records a PlayData entry for the given User created during a Play session.
///
/// Does nothing if no Play Id is set for this http scope.
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
Task Record(User user);
/// <summary>
/// Records a PlayData entry for the given Organization created during a Play session.
///
/// Does nothing if no Play Id is set for this http scope.
/// </summary>
/// <param name="organization"></param>
/// <returns></returns>
Task Record(Organization organization);
}

View File

@@ -1,7 +1,23 @@
namespace Bit.Core.Services;
/// <summary>
/// Service for managing Play identifiers in automated testing infrastructure.
/// A "Play" is a test session that groups entities created during testing to enable cleanup.
/// The PlayId flows from client request (x-play-id header) through PlayIdMiddleware to this service,
/// which repositories query to create PlayData tracking records via IPlayDataService. The SeederAPI uses these records
/// to bulk delete all entities associated with a PlayId. Only active in Development environments.
/// </summary>
public interface IPlayIdService
{
/// <summary>
/// Gets or sets the current Play identifier from the x-play-id request header.
/// </summary>
string? PlayId { get; set; }
/// <summary>
/// Checks whether the current request is part of an active Play session.
/// </summary>
/// <param name="playId">The Play identifier if active, otherwise empty string.</param>
/// <returns>True if in a Play session (has PlayId and in Development environment), otherwise false.</returns>
bool InPlay(out string playId);
}

View File

@@ -0,0 +1,26 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
public class PlayDataService(IPlayIdService playIdService, IPlayDataRepository playDataRepository, ILogger<PlayDataService> logger) : IPlayDataService
{
public async Task Record(User user)
{
if (playIdService.InPlay(out var playId))
{
logger.LogInformation("Associating user {UserId} with Play ID {PlayId}", user.Id, playId);
await playDataRepository.CreateAsync(PlayData.Create(user, playId));
}
}
public async Task Record(Organization organization)
{
if (playIdService.InPlay(out var playId))
{
logger.LogInformation("Associating organization {OrganizationId} with Play ID {PlayId}", organization.Id, playId);
await playDataRepository.CreateAsync(PlayData.Create(organization, playId));
}
}
}

View File

@@ -256,32 +256,21 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
public class TestOrganizationTrackingOrganizationRepository : OrganizationRepository
{
private readonly IPlayIdService _playIdService;
private readonly IPlayDataRepository _playDataRepository;
private readonly IPlayDataService _playDataService;
public TestOrganizationTrackingOrganizationRepository(
IPlayIdService playIdService,
IPlayDataRepository playDataRepository,
IPlayDataService playDataService,
GlobalSettings globalSettings,
ILogger<OrganizationRepository> logger)
: base(globalSettings, logger)
{
_playIdService = playIdService;
_playDataRepository = playDataRepository;
_playDataService = playDataService;
}
public override async Task<Organization> CreateAsync(Organization obj)
{
var createdOrganization = await base.CreateAsync(obj);
if (_playIdService.InPlay(out var playId))
{
_logger.LogInformation("Associating organization {OrganizationId} with Play ID {PlayId}",
createdOrganization.Id, playId);
await _playDataRepository.CreateAsync(PlayData.Create(createdOrganization, playId));
}
await _playDataService.Record(createdOrganization);
return createdOrganization;
}
}

View File

@@ -404,32 +404,23 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
public class TestUserTrackingUserRepository : UserRepository
{
private readonly IPlayIdService _playIdService;
private readonly IPlayDataRepository _playDataRepository;
private readonly IPlayDataService _playDataService;
public TestUserTrackingUserRepository(
IPlayIdService playIdService,
GlobalSettings globalSettings,
IPlayDataRepository playDataRepository,
IDataProtectionProvider dataProtectionProvider,
ILogger<UserRepository> logger)
: base(dataProtectionProvider, globalSettings, logger)
IPlayDataService playDataService,
GlobalSettings globalSettings,
IDataProtectionProvider dataProtectionProvider,
ILogger<UserRepository> logger)
: base(dataProtectionProvider, globalSettings, logger)
{
_playIdService = playIdService;
_playDataRepository = playDataRepository;
_playDataService = playDataService;
}
public override async Task<User> CreateAsync(User user)
{
var createdUser = await base.CreateAsync(user);
if (_playIdService.InPlay(out var playId))
{
_logger.LogInformation("Associating user {UserId} with Play ID {PlayId}",
user.Id, playId);
await _playDataRepository.CreateAsync(PlayData.Create(createdUser, playId));
}
await _playDataService.Record(createdUser);
return createdUser;
}
}

View File

@@ -443,34 +443,22 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
public class TestOrganizationTrackingOrganizationRepository : OrganizationRepository
{
private readonly IPlayIdService _playIdService;
private readonly IPlayDataRepository _playDataRepository;
private readonly IPlayDataService _playDataService;
public TestOrganizationTrackingOrganizationRepository(
IServiceScopeFactory serviceScopeFactory,
IMapper mapper,
ILogger<OrganizationRepository> logger,
IPlayIdService playIdService,
IPlayDataRepository playDataRepository)
: base(serviceScopeFactory, mapper, logger)
IPlayDataService playDataService,
IServiceScopeFactory serviceScopeFactory,
IMapper mapper,
ILogger<OrganizationRepository> logger)
: base(serviceScopeFactory, mapper, logger)
{
_playIdService = playIdService;
_playDataRepository = playDataRepository;
_playDataService = playDataService;
}
public override async Task<Core.AdminConsole.Entities.Organization> CreateAsync(Core.AdminConsole.Entities.Organization organization)
{
var createdOrganization = await base.CreateAsync(organization);
if (_playIdService.InPlay(out var playId))
{
_logger.LogInformation("Associating organization {OrganizationId} with Play ID {PlayId}",
organization.Id, playId);
await _playDataRepository.CreateAsync(Core.Entities.PlayData.Create(organization, playId));
}
await _playDataService.Record(createdOrganization);
return createdOrganization;
}
}

View File

@@ -404,33 +404,22 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR
public class TestUserTrackingUserRepository : UserRepository
{
private readonly IPlayIdService _playIdService;
private readonly IPlayDataRepository _playDataRepository;
private readonly IPlayDataService _playDataService;
public TestUserTrackingUserRepository(
IPlayIdService playIdService,
IPlayDataRepository playDataRepository,
IPlayDataService playDataService,
IServiceScopeFactory serviceScopeFactory,
IMapper mapper,
ILogger<UserRepository> logger)
: base(serviceScopeFactory, mapper, logger)
{
_playIdService = playIdService;
_playDataRepository = playDataRepository;
_playDataService = playDataService;
}
public override async Task<Core.Entities.User> CreateAsync(Core.Entities.User user)
{
var createdUser = await base.CreateAsync(user);
if (_playIdService.InPlay(out var playId))
{
_logger.LogInformation("Associating user {UserId} with Play ID {PlayId}",
user.Id, playId);
await _playDataRepository.CreateAsync(Core.Entities.PlayData.Create(user, playId));
}
await _playDataService.Record(createdUser);
return createdUser;
}
}

View File

@@ -3,6 +3,12 @@ using Microsoft.AspNetCore.Http;
namespace Bit.SharedWeb.Utilities;
/// <summary>
/// Middleware to extract the x-play-id header and set it in the PlayIdService.
///
/// PlayId is used in testing infrastructure to track data created during automated testing and fa cilitate cleanup.
/// </summary>
/// <param name="next"></param>
public sealed class PlayIdMiddleware(RequestDelegate next)
{
public Task Invoke(HttpContext context, PlayIdService playIdService)