Files
Umbraco-CMS/src/Umbraco.Core/Services/AuditService.cs
Kenn Jacobsen 374d699fd9 Move audit log endpoints to their respective silos and clean up (#16170)
* Move audit log endpoints to their respective silos and clean up

* Fix failing integration tests

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
2024-05-01 12:07:06 +02:00

424 lines
15 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
namespace Umbraco.Cms.Core.Services.Implement;
public sealed class AuditService : RepositoryService, IAuditService
{
private readonly IAuditEntryRepository _auditEntryRepository;
private readonly IUserService _userService;
private readonly IAuditRepository _auditRepository;
private readonly IEntityService _entityService;
private readonly Lazy<bool> _isAvailable;
[Obsolete("Use the non-obsolete constructor. Will be removed in V15.")]
public AuditService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IAuditRepository auditRepository,
IAuditEntryRepository auditEntryRepository,
IUserService userService,
IEntityRepository entityRepository)
: this(
provider,
loggerFactory,
eventMessagesFactory,
auditRepository,
auditEntryRepository,
userService,
StaticServiceProvider.Instance.GetRequiredService<IEntityService>()
)
{
}
[Obsolete("Use the non-obsolete constructor. Will be removed in V15.")]
public AuditService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IAuditRepository auditRepository,
IAuditEntryRepository auditEntryRepository,
IUserService userService,
IEntityRepository entityRepository,
IEntityService entityService)
: this(
provider,
loggerFactory,
eventMessagesFactory,
auditRepository,
auditEntryRepository,
userService,
entityService
)
{
}
public AuditService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IAuditRepository auditRepository,
IAuditEntryRepository auditEntryRepository,
IUserService userService,
IEntityService entityService)
: base(provider, loggerFactory, eventMessagesFactory)
{
_auditRepository = auditRepository;
_auditEntryRepository = auditEntryRepository;
_userService = userService;
_entityService = entityService;
_isAvailable = new Lazy<bool>(DetermineIsAvailable);
}
public void Add(AuditType type, int userId, int objectId, string? entityType, string comment, string? parameters = null)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
_auditRepository.Save(new AuditItem(objectId, type, userId, entityType, comment, parameters));
scope.Complete();
}
}
public IEnumerable<IAuditItem> GetLogs(int objectId)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
IEnumerable<IAuditItem> result = _auditRepository.Get(Query<IAuditItem>().Where(x => x.Id == objectId));
scope.Complete();
return result;
}
}
public IEnumerable<IAuditItem> GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
IEnumerable<IAuditItem> result = sinceDate.HasValue == false
? _auditRepository.Get(type, Query<IAuditItem>().Where(x => x.UserId == userId))
: _auditRepository.Get(
type,
Query<IAuditItem>().Where(x => x.UserId == userId && x.CreateDate >= sinceDate.Value));
scope.Complete();
return result;
}
}
public IEnumerable<IAuditItem> GetLogs(AuditType type, DateTime? sinceDate = null)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
IEnumerable<IAuditItem> result = sinceDate.HasValue == false
? _auditRepository.Get(type, Query<IAuditItem>())
: _auditRepository.Get(type, Query<IAuditItem>().Where(x => x.CreateDate >= sinceDate.Value));
scope.Complete();
return result;
}
}
public void CleanLogs(int maximumAgeOfLogsInMinutes)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
_auditRepository.CleanLogs(maximumAgeOfLogsInMinutes);
scope.Complete();
}
}
/// <summary>
/// Returns paged items in the audit trail for a given entity
/// </summary>
/// <param name="entityId"></param>
/// <param name="pageIndex"></param>
/// <param name="pageSize"></param>
/// <param name="totalRecords"></param>
/// <param name="orderDirection">
/// By default this will always be ordered descending (newest first)
/// </param>
/// <param name="auditTypeFilter">
/// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query
/// or the custom filter
/// so we need to do that here
/// </param>
/// <param name="customFilter">
/// Optional filter to be applied
/// </param>
/// <returns></returns>
public IEnumerable<IAuditItem> GetPagedItemsByEntity(
int entityId,
long pageIndex,
int pageSize,
out long totalRecords,
Direction orderDirection = Direction.Descending,
AuditType[]? auditTypeFilter = null,
IQuery<IAuditItem>? customFilter = null)
{
if (pageIndex < 0)
{
throw new ArgumentOutOfRangeException(nameof(pageIndex));
}
if (pageSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(pageSize));
}
if (entityId == Constants.System.Root || entityId <= 0)
{
totalRecords = 0;
return Enumerable.Empty<IAuditItem>();
}
using (ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IAuditItem> query = Query<IAuditItem>().Where(x => x.Id == entityId);
return _auditRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderDirection, auditTypeFilter, customFilter);
}
}
/// <summary>
/// Returns paged items in the audit trail for a given user
/// </summary>
/// <param name="userId"></param>
/// <param name="pageIndex"></param>
/// <param name="pageSize"></param>
/// <param name="totalRecords"></param>
/// <param name="orderDirection">
/// By default this will always be ordered descending (newest first)
/// </param>
/// <param name="auditTypeFilter">
/// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query
/// or the custom filter
/// so we need to do that here
/// </param>
/// <param name="customFilter">
/// Optional filter to be applied
/// </param>
/// <returns></returns>
public IEnumerable<IAuditItem> GetPagedItemsByUser(
int userId,
long pageIndex,
int pageSize,
out long totalRecords,
Direction orderDirection = Direction.Descending,
AuditType[]? auditTypeFilter = null,
IQuery<IAuditItem>? customFilter = null)
{
if (pageIndex < 0)
{
throw new ArgumentOutOfRangeException(nameof(pageIndex));
}
if (pageSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(pageSize));
}
if (userId < Constants.Security.SuperUserId)
{
totalRecords = 0;
return Enumerable.Empty<IAuditItem>();
}
using (ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IAuditItem> query = Query<IAuditItem>().Where(x => x.UserId == userId);
return _auditRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderDirection, auditTypeFilter, customFilter);
}
}
public async Task<PagedModel<IAuditItem>> GetItemsByKeyAsync(
Guid entityKey,
UmbracoObjectTypes entityType,
int skip,
int take,
Direction orderDirection = Direction.Descending,
DateTime? sinceDate = null,
AuditType[]? auditTypeFilter = null)
{
if (skip < 0)
{
throw new ArgumentOutOfRangeException(nameof(skip));
}
if (take <= 0)
{
throw new ArgumentOutOfRangeException(nameof(take));
}
Attempt<int> keyToIdAttempt = _entityService.GetId(entityKey, entityType);
if (keyToIdAttempt.Success is false)
{
return await Task.FromResult(new PagedModel<IAuditItem> { Items = Enumerable.Empty<IAuditItem>(), Total = 0 });
}
using (ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IAuditItem> query = Query<IAuditItem>().Where(x => x.Id == keyToIdAttempt.Result);
IQuery<IAuditItem>? customFilter = sinceDate.HasValue ? Query<IAuditItem>().Where(x => x.CreateDate >= sinceDate) : null;
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
IEnumerable<IAuditItem> auditItems = _auditRepository.GetPagedResultsByQuery(query, pageNumber, pageSize, out var totalRecords, orderDirection, auditTypeFilter, customFilter);
return new PagedModel<IAuditItem> { Items = auditItems, Total = totalRecords };
}
}
public async Task<PagedModel<IAuditItem>> GetPagedItemsByUserAsync(
Guid userKey,
int skip,
int take,
Direction orderDirection = Direction.Descending,
AuditType[]? auditTypeFilter = null,
DateTime? sinceDate = null)
{
if (skip < 0)
{
throw new ArgumentOutOfRangeException(nameof(skip));
}
if (take <= 0)
{
throw new ArgumentOutOfRangeException(nameof(take));
}
IUser? user = await _userService.GetAsync(userKey);
if (user is null)
{
return await Task.FromResult(new PagedModel<IAuditItem>());
}
using (ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IAuditItem> query = Query<IAuditItem>().Where(x => x.UserId == user.Id);
IQuery<IAuditItem>? customFilter = sinceDate.HasValue ? Query<IAuditItem>().Where(x => x.CreateDate >= sinceDate) : null;
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
IEnumerable<IAuditItem> auditItems = _auditRepository.GetPagedResultsByQuery(query, pageNumber, pageSize, out var totalRecords, orderDirection, auditTypeFilter, customFilter);
return await Task.FromResult(new PagedModel<IAuditItem> { Items = auditItems, Total = totalRecords });
}
}
/// <inheritdoc />
public IAuditEntry Write(int performingUserId, string perfomingDetails, string performingIp, DateTime eventDateUtc, int affectedUserId, string? affectedDetails, string eventType, string eventDetails)
{
if (performingUserId < 0 && performingUserId != Constants.Security.SuperUserId)
{
throw new ArgumentOutOfRangeException(nameof(performingUserId));
}
if (string.IsNullOrWhiteSpace(perfomingDetails))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(perfomingDetails));
}
if (string.IsNullOrWhiteSpace(eventType))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(eventType));
}
if (string.IsNullOrWhiteSpace(eventDetails))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(eventDetails));
}
// we need to truncate the data else we'll get SQL errors
affectedDetails =
affectedDetails?[..Math.Min(affectedDetails.Length, Constants.Audit.DetailsLength)];
eventDetails = eventDetails[..Math.Min(eventDetails.Length, Constants.Audit.DetailsLength)];
// validate the eventType - must contain a forward slash, no spaces, no special chars
var eventTypeParts = eventType.ToCharArray();
if (eventTypeParts.Contains('/') == false ||
eventTypeParts.All(c => char.IsLetterOrDigit(c) || c == '/' || c == '-') == false)
{
throw new ArgumentException(nameof(eventType) +
" must contain only alphanumeric characters, hyphens and at least one '/' defining a category");
}
if (eventType.Length > Constants.Audit.EventTypeLength)
{
throw new ArgumentException($"Must be max {Constants.Audit.EventTypeLength} chars.", nameof(eventType));
}
if (performingIp != null && performingIp.Length > Constants.Audit.IpLength)
{
throw new ArgumentException($"Must be max {Constants.Audit.EventTypeLength} chars.", nameof(performingIp));
}
var entry = new AuditEntry
{
PerformingUserId = performingUserId,
PerformingDetails = perfomingDetails,
PerformingIp = performingIp,
EventDateUtc = eventDateUtc,
AffectedUserId = affectedUserId,
AffectedDetails = affectedDetails,
EventType = eventType,
EventDetails = eventDetails,
};
if (_isAvailable.Value == false)
{
return entry;
}
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
_auditEntryRepository.Save(entry);
scope.Complete();
}
return entry;
}
// TODO: Currently used in testing only, not part of the interface, need to add queryable methods to the interface instead
internal IEnumerable<IAuditEntry>? GetAll()
{
if (_isAvailable.Value == false)
{
return Enumerable.Empty<IAuditEntry>();
}
using (ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _auditEntryRepository.GetMany();
}
}
// TODO: Currently used in testing only, not part of the interface, need to add queryable methods to the interface instead
internal IEnumerable<IAuditEntry> GetPage(long pageIndex, int pageCount, out long records)
{
if (_isAvailable.Value == false)
{
records = 0;
return Enumerable.Empty<IAuditEntry>();
}
using (ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _auditEntryRepository.GetPage(pageIndex, pageCount, out records);
}
}
/// <summary>
/// Determines whether the repository is available.
/// </summary>
private bool DetermineIsAvailable()
{
using (ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _auditEntryRepository.IsAvailable();
}
}
}