diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/SecurityLog/DefaultSecurityLogManager.cs b/framework/src/Volo.Abp.Security/Volo/Abp/SecurityLog/DefaultSecurityLogManager.cs index fcc03965f2..ab260ff708 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/SecurityLog/DefaultSecurityLogManager.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/SecurityLog/DefaultSecurityLogManager.cs @@ -19,7 +19,7 @@ namespace Volo.Abp.SecurityLog SecurityLogOptions = securityLogOptions.Value; } - public async Task SaveAsync(Action saveAction) + public async Task SaveAsync(Action saveAction = null) { var securityLogInfo = await CreateAsync(); saveAction?.Invoke(securityLogInfo); diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/SecurityLog/ISecurityLogManager.cs b/framework/src/Volo.Abp.Security/Volo/Abp/SecurityLog/ISecurityLogManager.cs index 0bedfd0411..f368489fb0 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/SecurityLog/ISecurityLogManager.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/SecurityLog/ISecurityLogManager.cs @@ -5,6 +5,6 @@ namespace Volo.Abp.SecurityLog { public interface ISecurityLogManager { - Task SaveAsync(Action saveAction); + Task SaveAsync(Action saveAction = null); } } diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/SecurityLog/SecurityLogInfo.cs b/framework/src/Volo.Abp.Security/Volo/Abp/SecurityLog/SecurityLogInfo.cs index 6185777b97..e40b877fcf 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/SecurityLog/SecurityLogInfo.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/SecurityLog/SecurityLogInfo.cs @@ -5,7 +5,7 @@ using Volo.Abp.Data; namespace Volo.Abp.SecurityLog { [Serializable] - public class SecurityLogInfo : IHasExtraProperties + public class SecurityLogInfo { /// /// The name of the application or service writing user security logs. diff --git a/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/AbpSecurityTestModule.cs b/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/AbpSecurityTestModule.cs index b71a0a214a..a1ce5c56aa 100644 --- a/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/AbpSecurityTestModule.cs +++ b/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/AbpSecurityTestModule.cs @@ -1,4 +1,5 @@ using Volo.Abp.Modularity; +using Volo.Abp.SecurityLog; namespace Volo.Abp.Security { @@ -8,6 +9,12 @@ namespace Volo.Abp.Security )] public class AbpSecurityTestModule : AbpModule { - + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(x => + { + x.ApplicationName = "AbpSecurityTest"; + }); + } } } diff --git a/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/SecurityLog/SecurityLogManager_Tests.cs b/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/SecurityLog/SecurityLogManager_Tests.cs new file mode 100644 index 0000000000..94fb6bccda --- /dev/null +++ b/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/SecurityLog/SecurityLogManager_Tests.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Volo.Abp.SecurityLog; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Security.SecurityLog +{ + + public class SecurityLogManager_Tests : AbpIntegratedTest + { + private readonly ISecurityLogManager _securityLogManager; + + private ISecurityLogStore _auditingStore; + + public SecurityLogManager_Tests() + { + _securityLogManager = GetRequiredService(); + } + + protected override void AfterAddApplication(IServiceCollection services) + { + _auditingStore = Substitute.For(); + services.AddSingleton(_auditingStore); + } + + [Fact] + public async Task SaveAsync() + { + await _securityLogManager.SaveAsync(securityLog => + { + securityLog.Identity = "Test"; + securityLog.Action = "Test-Action"; + securityLog.UserName = "Test-User"; + }); + + await _auditingStore.Received().SaveAsync(Arg.Is(log => + log.ApplicationName == "AbpSecurityTest" && + log.Identity == "Test" && + log.Action == "Test-Action" && + log.UserName == "Test-User")); + } + } +} diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs index 0d90972792..d26e64181d 100644 --- a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs +++ b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs @@ -130,7 +130,7 @@ namespace Volo.Abp.Account.Web.Pages.Account true ); - await LocalEventBus.PublishAsync(new SecurityLogEvent + await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = result.ToIdentitySecurityLogAction(), diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLogoutModel.cs b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLogoutModel.cs index 43857cc111..b25b34799d 100644 --- a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLogoutModel.cs +++ b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLogoutModel.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Volo.Abp.DependencyInjection; +using Volo.Abp.Identity; namespace Volo.Abp.Account.Web.Pages.Account { @@ -19,6 +20,12 @@ namespace Volo.Abp.Account.Web.Pages.Account public override async Task OnGetAsync() { + await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent + { + Identity = IdentitySecurityLogIdentityConsts.Identity, + Action = IdentitySecurityLogActionConsts.Logout + }); + await SignInManager.SignOutAsync(); var logoutId = Request.Query["logoutId"].ToString(); diff --git a/modules/account/src/Volo.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs b/modules/account/src/Volo.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs index 69b57a42f4..aa22882701 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs @@ -62,7 +62,7 @@ namespace Volo.Abp.Account.Web.Areas.Account.Controllers true ); - await LocalEventBus.PublishAsync(new SecurityLogEvent + await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = signInResult.ToIdentitySecurityLogAction(), @@ -76,7 +76,7 @@ namespace Volo.Abp.Account.Web.Areas.Account.Controllers [Route("logout")] public virtual async Task Logout() { - await LocalEventBus.PublishAsync(new SecurityLogEvent + await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = IdentitySecurityLogActionConsts.Logout @@ -147,7 +147,7 @@ namespace Volo.Abp.Account.Web.Areas.Account.Controllers return new AbpLoginResult(LoginResultType.InvalidUserNameOrPassword); } - return new AbpLoginResult(LoginResultType.Succeeded); + return new AbpLoginResult(LoginResultType.Success); } protected virtual void ValidateLoginInfo(UserLoginInfo login) diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs index 06903ad44f..8dc2ef3273 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs @@ -96,7 +96,7 @@ namespace Volo.Abp.Account.Web.Pages.Account true ); - await LocalEventBus.PublishAsync(new SecurityLogEvent + await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = result.ToIdentitySecurityLogAction(), @@ -192,7 +192,7 @@ namespace Volo.Abp.Account.Web.Pages.Account if (!result.Succeeded) { - await LocalEventBus.PublishAsync(new SecurityLogEvent + await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent { Identity = IdentitySecurityLogIdentityConsts.IdentityExternal, Action = "Login" + result @@ -222,7 +222,7 @@ namespace Volo.Abp.Account.Web.Pages.Account await SignInManager.SignInAsync(user, false); - await LocalEventBus.PublishAsync(new SecurityLogEvent + await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent { Identity = IdentitySecurityLogIdentityConsts.IdentityExternal, Action = result.ToIdentitySecurityLogAction(), diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Logout.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Logout.cshtml.cs index a9d4ed9a53..fe7361e4ce 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Logout.cshtml.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Logout.cshtml.cs @@ -16,7 +16,7 @@ namespace Volo.Abp.Account.Web.Pages.Account public virtual async Task OnGetAsync() { - await LocalEventBus.PublishAsync(new SecurityLogEvent + await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = IdentitySecurityLogActionConsts.Logout diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentitySecurityLogConsts.cs b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentitySecurityLogConsts.cs new file mode 100644 index 0000000000..43b2dd25fb --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentitySecurityLogConsts.cs @@ -0,0 +1,52 @@ +namespace Volo.Abp.Identity +{ + public class IdentitySecurityLogConsts + { + /// + /// Default value: 96 + /// + public static int MaxApplicationNameLength { get; set; } = 96; + + /// + /// Default value: 96 + /// + public static int MaxIdentityLength { get; set; } = 96; + + /// + /// Default value: 96 + /// + public static int MaxActionLength { get; set; } = 96; + + + /// + /// Default value: 256 + /// + public static int MaxUserNameLength { get; set; } = 256; + + /// + /// Default value: 64 + /// + public static int MaxTenantNameLength { get; set; } = 64; + + /// + /// Default value: 64 + /// + public static int MaxClientIpAddressLength { get; set; } = 64; + + /// + /// Default value: 64 + /// + public static int MaxClientIdLength { get; set; } = 64; + + /// + /// Default value: 64 + /// + public static int MaxCorrelationIdLength { get; set; } = 64; + + /// + /// Default value: 512 + /// + public static int MaxBrowserInfoLength { get; set; } = 512; + + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySecurityLogRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySecurityLogRepository.cs new file mode 100644 index 0000000000..8732355c14 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySecurityLogRepository.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Identity +{ + public interface IIdentitySecurityLogRepository : IBasicRepository + { + Task> GetListAsync( + string sorting = null, + int maxResultCount = 50, + int skipCount = 0, + DateTime? startTime = null, + DateTime? endTime = null, + string applicationName = null, + string identity = null, + string action = null, + string userName = null, + string clientId = null, + string correlationId = null, + bool includeDetails = false, + CancellationToken cancellationToken = default); + + Task GetCountAsync( + DateTime? startTime = null, + DateTime? endTime = null, + string applicationName = null, + string identity = null, + string action = null, + string userName = null, + string clientId = null, + string correlationId = null, + CancellationToken cancellationToken = default); + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLog.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLog.cs new file mode 100644 index 0000000000..b720f667f0 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLog.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; +using Volo.Abp.SecurityLog; + +namespace Volo.Abp.Identity +{ + public class IdentitySecurityLog : AggregateRoot, IMultiTenant + { + public Guid? TenantId { get; protected set; } + + public string ApplicationName { get; protected set; } + + public string Identity { get; protected set; } + + public string Action { get; protected set; } + + public Guid? UserId { get; protected set; } + + public string UserName { get; protected set; } + + public string TenantName { get; protected set; } + + public string ClientId { get; protected set; } + + public string CorrelationId { get; protected set; } + + public string ClientIpAddress { get; protected set; } + + public string BrowserInfo { get; protected set; } + + public DateTime CreationTime { get; protected set; } + + protected IdentitySecurityLog() + { + ExtraProperties = new Dictionary(); + } + + public IdentitySecurityLog(IGuidGenerator guidGenerator, SecurityLogInfo securityLogInfo) + { + Id = guidGenerator.Create(); + TenantId = securityLogInfo.TenantId; + TenantName = securityLogInfo.TenantName.Truncate(IdentitySecurityLogConsts.MaxTenantNameLength); + + ApplicationName = securityLogInfo.ApplicationName.Truncate(IdentitySecurityLogConsts.MaxApplicationNameLength); + Identity = securityLogInfo.Identity.Truncate(IdentitySecurityLogConsts.MaxIdentityLength); + Action = securityLogInfo.Action.Truncate(IdentitySecurityLogConsts.MaxActionLength); + + UserId = securityLogInfo.UserId; + UserName = securityLogInfo.UserName.Truncate(IdentitySecurityLogConsts.MaxUserNameLength); + + CreationTime = securityLogInfo.CreationTime; + + ClientIpAddress = securityLogInfo.ClientIpAddress.Truncate(IdentitySecurityLogConsts.MaxClientIpAddressLength); + ClientId = securityLogInfo.ClientId.Truncate(IdentitySecurityLogConsts.MaxClientIdLength); + CorrelationId = securityLogInfo.CorrelationId.Truncate(IdentitySecurityLogConsts.MaxCorrelationIdLength); + BrowserInfo = securityLogInfo.BrowserInfo.Truncate(IdentitySecurityLogConsts.MaxBrowserInfoLength); + + ExtraProperties = securityLogInfo.ExtraProperties; + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/SecurityLogEvent.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogEvent.cs similarity index 84% rename from modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/SecurityLogEvent.cs rename to modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogEvent.cs index d8857d02d9..93296619f6 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/SecurityLogEvent.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogEvent.cs @@ -3,7 +3,7 @@ using Volo.Abp.MultiTenancy; namespace Volo.Abp.Identity { - public class SecurityLogEvent : IMultiTenant + public class IdentitySecurityLogEvent : IMultiTenant { public Guid? TenantId { get; set; } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/SecurityLogHandler.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogHandler.cs similarity index 67% rename from modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/SecurityLogHandler.cs rename to modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogHandler.cs index 43321668b1..4e0c46b2f7 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/SecurityLogHandler.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogHandler.cs @@ -10,32 +10,29 @@ using Volo.Abp.Users; namespace Volo.Abp.Identity { - public class SecurityLogHandler : ILocalEventHandler, ITransientDependency + public class IdentitySecurityLogHandler : ILocalEventHandler, ITransientDependency { protected ISecurityLogManager SecurityLogManager { get; } protected IdentityUserManager UserManager { get; } protected ICurrentPrincipalAccessor CurrentPrincipalAccessor { get; } protected IUserClaimsPrincipalFactory UserClaimsPrincipalFactory { get; } protected ICurrentUser CurrentUser { get; } - protected IUnitOfWorkManager UnitOfWorkManager { get; } - public SecurityLogHandler( + public IdentitySecurityLogHandler( ISecurityLogManager securityLogManager, IdentityUserManager userManager, ICurrentPrincipalAccessor currentPrincipalAccessor, IUserClaimsPrincipalFactory userClaimsPrincipalFactory, - ICurrentUser currentUser, - IUnitOfWorkManager unitOfWorkManager) + ICurrentUser currentUser) { SecurityLogManager = securityLogManager; UserManager = userManager; CurrentPrincipalAccessor = currentPrincipalAccessor; UserClaimsPrincipalFactory = userClaimsPrincipalFactory; CurrentUser = currentUser; - UnitOfWorkManager = unitOfWorkManager; } - public async Task HandleEventAsync(SecurityLogEvent eventData) + public async Task HandleEventAsync(IdentitySecurityLogEvent eventData) { Action securityLogAction = securityLog => { @@ -53,32 +50,31 @@ namespace Volo.Abp.Identity } }; - using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) + if (CurrentUser.IsAuthenticated) { - if (CurrentUser.IsAuthenticated) + await SecurityLogManager.SaveAsync(securityLogAction); + } + else + { + if (eventData.UserName.IsNullOrWhiteSpace()) { await SecurityLogManager.SaveAsync(securityLogAction); } else { - if (eventData.UserName.IsNullOrWhiteSpace()) + var user = await UserManager.FindByNameAsync(eventData.UserName); + if (user != null) { - await SecurityLogManager.SaveAsync(securityLogAction); + using (CurrentPrincipalAccessor.Change(await UserClaimsPrincipalFactory.CreateAsync(user))) + { + await SecurityLogManager.SaveAsync(securityLogAction); + } } else { - var user = await UserManager.FindByNameAsync(eventData.UserName); - if (user != null) - { - using (CurrentPrincipalAccessor.Change(await UserClaimsPrincipalFactory.CreateAsync(user))) - { - await SecurityLogManager.SaveAsync(securityLogAction); - } - } + await SecurityLogManager.SaveAsync(securityLogAction); } } - - await uow.CompleteAsync(); } } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogStore.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogStore.cs new file mode 100644 index 0000000000..696fc3ad6a --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogStore.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.SecurityLog; +using Volo.Abp.Uow; + +namespace Volo.Abp.Identity +{ + [Dependency(ReplaceServices = true)] + public class IdentitySecurityLogStore : ISecurityLogStore, ITransientDependency + { + public ILogger Logger { get; set; } + + protected AbpSecurityLogOptions SecurityLogOptions { get; } + protected IIdentitySecurityLogRepository IdentitySecurityLogRepository { get; } + protected IGuidGenerator GuidGenerator { get; } + protected IUnitOfWorkManager UnitOfWorkManager { get; } + + public IdentitySecurityLogStore( + ILogger logger, + IOptions securityLogOptions, + IIdentitySecurityLogRepository identitySecurityLogRepository, + IGuidGenerator guidGenerator, + IUnitOfWorkManager unitOfWorkManager) + { + Logger = logger; + SecurityLogOptions = securityLogOptions.Value; + IdentitySecurityLogRepository = identitySecurityLogRepository; + GuidGenerator = guidGenerator; + UnitOfWorkManager = unitOfWorkManager; + } + + public async Task SaveAsync(SecurityLogInfo securityLogInfo) + { + using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) + { + await IdentitySecurityLogRepository.InsertAsync(new IdentitySecurityLog(GuidGenerator, securityLogInfo)); + await uow.CompleteAsync(); + } + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EFCoreIdentitySecurityLogRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EFCoreIdentitySecurityLogRepository.cs new file mode 100644 index 0000000000..1801a49136 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EFCoreIdentitySecurityLogRepository.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Volo.Abp.Identity.EntityFrameworkCore +{ + public class EFCoreIdentitySecurityLogRepository : EfCoreRepository, IIdentitySecurityLogRepository + { + public EFCoreIdentitySecurityLogRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + + } + + public async Task> GetListAsync( + string sorting = null, + int maxResultCount = 50, + int skipCount = 0, + DateTime? startTime = null, + DateTime? endTime = null, + string applicationName = null, + string identity = null, + string action = null, + string userName = null, + string clientId = null, + string correlationId = null, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var query = GetListQuery( + startTime, + endTime, + applicationName, + identity, + action, + userName, + clientId, + correlationId + ); + + return await query.OrderBy(sorting ?? nameof(IdentitySecurityLog.CreationTime) + " desc") + .PageBy(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public async Task GetCountAsync( + DateTime? startTime = null, + DateTime? endTime = null, + string applicationName = null, + string identity = null, + string action = null, + string userName = null, + string clientId = null, + string correlationId = null, + CancellationToken cancellationToken = default) + { + var query = GetListQuery( + startTime, + endTime, + applicationName, + identity, + action, + userName, + clientId, + correlationId + ); + + return await query.LongCountAsync(GetCancellationToken(cancellationToken)); + } + + protected virtual IQueryable GetListQuery( + DateTime? startTime = null, + DateTime? endTime = null, + string applicationName = null, + string identity = null, + string action = null, + string userName = null, + string clientId = null, + string correlationId = null) + { + return DbSet.AsNoTracking() + .WhereIf(startTime.HasValue, securityLog => securityLog.CreationTime >= startTime) + .WhereIf(endTime.HasValue, securityLog => securityLog.CreationTime >= endTime) + .WhereIf(!applicationName.IsNullOrWhiteSpace(), securityLog => securityLog.ApplicationName == applicationName) + .WhereIf(!identity.IsNullOrWhiteSpace(), securityLog => securityLog.Identity == identity) + .WhereIf(!action.IsNullOrWhiteSpace(), securityLog => securityLog.Action == action) + .WhereIf(!userName.IsNullOrWhiteSpace(), securityLog => securityLog.UserName == userName) + .WhereIf(!clientId.IsNullOrWhiteSpace(), securityLog => securityLog.ClientId == clientId) + .WhereIf(!correlationId.IsNullOrWhiteSpace(), securityLog => securityLog.CorrelationId == correlationId); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IIdentityDbContext.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IIdentityDbContext.cs index f27c9b534a..48da4daf87 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IIdentityDbContext.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IIdentityDbContext.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.SecurityLog; namespace Volo.Abp.Identity.EntityFrameworkCore { @@ -14,5 +15,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore DbSet ClaimTypes { get; set; } DbSet OrganizationUnits { get; set; } + + DbSet IdentitySecurityLogs { get; set; } } } diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs index 811280de7b..16679429b8 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs @@ -18,6 +18,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore public DbSet OrganizationUnits { get; set; } + public DbSet IdentitySecurityLogs { get; set; } + public IdentityDbContext(DbContextOptions options) : base(options) { @@ -31,4 +33,4 @@ namespace Volo.Abp.Identity.EntityFrameworkCore builder.ConfigureIdentity(); } } -} \ No newline at end of file +} diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs index 000a2a8f00..373abbe804 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs @@ -204,6 +204,32 @@ namespace Volo.Abp.Identity.EntityFrameworkCore b.HasIndex(ou => new {ou.UserId, ou.OrganizationUnitId}); }); + + builder.Entity(b => + { + b.ToTable(options.TablePrefix + "SecurityLogs", options.Schema); + + b.ConfigureByConvention(); + + b.Property(x => x.TenantName).HasMaxLength(IdentitySecurityLogConsts.MaxTenantNameLength); + + b.Property(x => x.ApplicationName).HasMaxLength(IdentitySecurityLogConsts.MaxApplicationNameLength); + b.Property(x => x.Identity).HasMaxLength(IdentitySecurityLogConsts.MaxIdentityLength); + b.Property(x => x.Action).HasMaxLength(IdentitySecurityLogConsts.MaxActionLength); + + b.Property(x => x.UserName).HasMaxLength(IdentitySecurityLogConsts.MaxUserNameLength); + + b.Property(x => x.ClientIpAddress).HasMaxLength(IdentitySecurityLogConsts.MaxClientIpAddressLength); + b.Property(x => x.ClientId).HasMaxLength(IdentitySecurityLogConsts.MaxClientIdLength); + b.Property(x => x.CorrelationId).HasMaxLength(IdentitySecurityLogConsts.MaxCorrelationIdLength); + b.Property(x => x.BrowserInfo).HasMaxLength(IdentitySecurityLogConsts.MaxBrowserInfoLength); + + b.HasIndex(x => new { x.TenantId, x.ApplicationName }); + b.HasIndex(x => new { x.TenantId, x.Identity }); + b.HasIndex(x => new { x.TenantId, x.Action }); + b.HasIndex(x => new { x.TenantId, x.UserId }); + }); + } } -} \ No newline at end of file +} diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContext.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContext.cs index 3240ce1454..19c3316b88 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContext.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContext.cs @@ -15,6 +15,8 @@ namespace Volo.Abp.Identity.MongoDB public IMongoCollection OrganizationUnits => Collection(); + public IMongoCollection IdentitySecurityLogs => Collection(); + protected override void CreateModel(IMongoModelBuilder modelBuilder) { base.CreateModel(modelBuilder); @@ -22,4 +24,4 @@ namespace Volo.Abp.Identity.MongoDB modelBuilder.ConfigureIdentity(); } } -} \ No newline at end of file +} diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContextExtensions.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContextExtensions.cs index bc303e0eb5..89aeddeb92 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContextExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContextExtensions.cs @@ -36,6 +36,11 @@ namespace Volo.Abp.Identity.MongoDB { b.CollectionName = options.CollectionPrefix + "OrganizationUnits"; }); + + builder.Entity(b => + { + b.CollectionName = options.CollectionPrefix + "SecurityLogs"; + }); } } -} \ No newline at end of file +} diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbModule.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbModule.cs index 268c718b12..3ceab94e23 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbModule.cs @@ -18,6 +18,7 @@ namespace Volo.Abp.Identity.MongoDB options.AddRepository(); options.AddRepository(); options.AddRepository(); + options.AddRepository(); }); } } diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/IAbpIdentityMongoDbContext.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/IAbpIdentityMongoDbContext.cs index c903c5d96d..54819ffa19 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/IAbpIdentityMongoDbContext.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/IAbpIdentityMongoDbContext.cs @@ -14,5 +14,7 @@ namespace Volo.Abp.Identity.MongoDB IMongoCollection ClaimTypes { get; } IMongoCollection OrganizationUnits { get; } + + IMongoCollection IdentitySecurityLogs { get; } } -} \ No newline at end of file +} diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentitySecurityLogRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentitySecurityLogRepository.cs new file mode 100644 index 0000000000..5fccb0d2ab --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentitySecurityLogRepository.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using Volo.Abp.Domain.Repositories.MongoDB; +using Volo.Abp.MongoDB; + +namespace Volo.Abp.Identity.MongoDB +{ + public class MongoIdentitySecurityLogRepository : MongoDbRepository, IIdentitySecurityLogRepository + { + public MongoIdentitySecurityLogRepository(IMongoDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task> GetListAsync( + string sorting = null, + int maxResultCount = 50, + int skipCount = 0, + DateTime? startTime = null, + DateTime? endTime = null, + string applicationName = null, + string identity = null, + string action = null, + string userName = null, + string clientId = null, + string correlationId = null, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var query = GetListQuery( + startTime, + endTime, + applicationName, + identity, + action, + userName, + clientId, + correlationId + ); + + return await query.OrderBy(sorting ?? nameof(IdentitySecurityLog.CreationTime) + " desc") + .As>() + .PageBy>(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public async Task GetCountAsync( + DateTime? startTime = null, + DateTime? endTime = null, + string applicationName = null, + string identity = null, + string action = null, + string userName = null, + string clientId = null, + string correlationId = null, + CancellationToken cancellationToken = default) + { + var query = GetListQuery( + startTime, + endTime, + applicationName, + identity, + action, + userName, + clientId, + correlationId + ); + + return await query.As>().LongCountAsync(GetCancellationToken(cancellationToken)); + } + + protected virtual IQueryable GetListQuery( + DateTime? startTime = null, + DateTime? endTime = null, + string applicationName = null, + string identity = null, + string action = null, + string userName = null, + string clientId = null, + string correlationId = null) + { + return GetMongoQueryable() + .WhereIf(startTime.HasValue, securityLog => securityLog.CreationTime >= startTime) + .WhereIf(endTime.HasValue, securityLog => securityLog.CreationTime >= endTime) + .WhereIf(!applicationName.IsNullOrWhiteSpace(), securityLog => securityLog.ApplicationName == applicationName) + .WhereIf(!identity.IsNullOrWhiteSpace(), securityLog => securityLog.Identity == identity) + .WhereIf(!action.IsNullOrWhiteSpace(), securityLog => securityLog.Action == action) + .WhereIf(!userName.IsNullOrWhiteSpace(), securityLog => securityLog.UserName == userName) + .WhereIf(!clientId.IsNullOrWhiteSpace(), securityLog => securityLog.ClientId == clientId) + .WhereIf(!correlationId.IsNullOrWhiteSpace(), securityLog => securityLog.CorrelationId == correlationId); + } + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/IdentitySecurityLogRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/IdentitySecurityLogRepository_Tests.cs new file mode 100644 index 0000000000..434f74706d --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/IdentitySecurityLogRepository_Tests.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Identity.EntityFrameworkCore +{ + public class IdentitySecurityLogRepository_Tests : IdentitySecurityLogRepository_Tests + { + + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/IdentitySecurityLogRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/IdentitySecurityLogRepository_Tests.cs new file mode 100644 index 0000000000..85d496fdb2 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/IdentitySecurityLogRepository_Tests.cs @@ -0,0 +1,10 @@ +using Xunit; + +namespace Volo.Abp.Identity.MongoDB +{ + [Collection(MongoTestCollection.Name)] + public class IdentitySecurityLogRepository_Tests : IdentitySecurityLogRepository_Tests + { + + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs index 9258be16af..79dd858c20 100644 --- a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Volo.Abp.DependencyInjection; using Volo.Abp.Guids; +using Volo.Abp.SecurityLog; namespace Volo.Abp.Identity { @@ -14,6 +15,7 @@ namespace Volo.Abp.Identity private readonly IIdentityClaimTypeRepository _identityClaimTypeRepository; private readonly IIdentityRoleRepository _roleRepository; private readonly IOrganizationUnitRepository _organizationUnitRepository; + private readonly IIdentitySecurityLogRepository _identitySecurityLogRepository; private readonly ILookupNormalizer _lookupNormalizer; private readonly IdentityTestData _testData; private readonly OrganizationUnitManager _organizationUnitManager; @@ -31,6 +33,7 @@ namespace Volo.Abp.Identity IIdentityClaimTypeRepository identityClaimTypeRepository, IIdentityRoleRepository roleRepository, IOrganizationUnitRepository organizationUnitRepository, + IIdentitySecurityLogRepository identitySecurityLogRepository, ILookupNormalizer lookupNormalizer, IdentityTestData testData, OrganizationUnitManager organizationUnitManager) @@ -43,6 +46,7 @@ namespace Volo.Abp.Identity _testData = testData; _organizationUnitRepository = organizationUnitRepository; _organizationUnitManager = organizationUnitManager; + _identitySecurityLogRepository = identitySecurityLogRepository; } public async Task Build() @@ -51,6 +55,7 @@ namespace Volo.Abp.Identity await AddOrganizationUnits(); await AddUsers(); await AddClaimTypes(); + await AddSecurityLogs(); } private async Task AddRoles() @@ -69,7 +74,7 @@ namespace Volo.Abp.Identity } /* Creates OU tree as shown below: - * + * * - OU1 * - OU11 * - OU111 @@ -138,5 +143,30 @@ namespace Volo.Abp.Identity var ou = await _organizationUnitRepository.InsertAsync(new OrganizationUnit(_guidGenerator.Create(), displayName, parentId) { Code = code }); return ou; } + + private async Task AddSecurityLogs() + { + await _identitySecurityLogRepository.InsertAsync(new IdentitySecurityLog(_guidGenerator, new SecurityLogInfo + { + ApplicationName = "Test-ApplicationName", + Identity = "Test-Identity", + Action = "Test-Action", + UserId = _testData.UserJohnId, + UserName = "john.nash", + + CreationTime = new DateTime(2020, 01, 01, 10, 0, 0) + })); + + await _identitySecurityLogRepository.InsertAsync(new IdentitySecurityLog(_guidGenerator, new SecurityLogInfo + { + ApplicationName = "Test-ApplicationName", + Identity = "Test-Identity", + Action = "Test-Action", + UserId = _testData.UserDavidId, + UserName = "david", + + CreationTime = new DateTime(2020, 01, 02, 10, 0, 0) + })); + } } -} \ No newline at end of file +} diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentitySecurityLogRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentitySecurityLogRepository_Tests.cs new file mode 100644 index 0000000000..13ff8a06a3 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentitySecurityLogRepository_Tests.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Modularity; +using Xunit; + +namespace Volo.Abp.Identity +{ + public abstract class IdentitySecurityLogRepository_Tests : AbpIdentityTestBase + where TStartupModule : IAbpModule + { + protected IIdentitySecurityLogRepository RoleRepository { get; } + protected IdentityTestData TestData { get; } + + protected IdentitySecurityLogRepository_Tests() + { + RoleRepository = GetRequiredService(); + TestData = GetRequiredService(); + } + + [Fact] + public async Task GetListAsync() + { + var logs = await RoleRepository.GetListAsync(); + logs.ShouldNotBeEmpty(); + logs.ShouldContain(x => x.ApplicationName == "Test-ApplicationName" && x.UserId == TestData.UserJohnId); + logs.ShouldContain(x => x.ApplicationName == "Test-ApplicationName" && x.UserId == TestData.UserDavidId); + } + + [Fact] + public async Task GetCountAsync() + { + var count = await RoleRepository.GetCountAsync(); + count.ShouldBe(2); + } + } +}