* Move audit log endpoints to their respective silos and clean up * Fix failing integration tests --------- Co-authored-by: Mads Rasmussen <madsr@hey.com>
424 lines
15 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|