Starts in:
1 DAY
20 HRS
8 MIN
51 SEC
Starts in:
1 D
20 H
8 M
51 S

Activities of "alexander.nikonov"

Understood... Thank you.

Unfortunately there is no better way

Well, maybe Ocelot Gateway project would help it? Will it work to merge localizations from all running sites (i.e. merge the "application-localization" endpoints results) and finally get the full list at the site with Role Management page, will the page "see" all those?

I do have localization for permissions, but the translations are stored in the resource files (in Domain.Shared project) of the respective applications: A, B, C. So to show the display names of all of them on a central Permission Management page, I need to retrieve the translations from those projects.

And to my knowledge, there are only two options here:

  1. to consume all the resources (i.e. Domain.Shared projects - which are now published as Nuget package) in the solution where the Permission Management page is;
  2. make an API call to A, B, C solutions endpoints to retrieve resources on the Permission Management page and map them on resource keys on front-end side;

I don't like variant #1 because of the reasons described previously. And I don't like variant #2 much either, because there could be a lot of excessive data - /api/abp/application-localization endpoint only accepts the language parameter, no any resource key mask...

I thought that maybe there's a better way?

I cannot hardcode display name of the permission in the property, if this is what you meant, because I have a multi-language app, so the permission display name needs to be changed depending on the current UI language.

What is the project of this error log? You can try to add your code to Domain module.

https://abp.io/support/questions/7699/Random-exception-after-switching-from-IdentityServer-to-OpenDict#answer-3a147d57-7267-7eaf-1d74-ba36bdfea929

Sorry, I am not sure I am following you. The first exception (DI issue) needs to be reproduced yet, I am waiting for my colleague trying to assist me, because I did not manage to get this exception. So I cannot say anything here yet.

But instead I received another exception related to the transaction level. According to your recommendation, I have overriden all the stores with the code you provided for the older version of ABP - in order to override a transaction level. After adding this code, I can see that the Store property is properly filled with my custom class. All the constructors of all three custom stores did invoke - so the code was applied as expected. Still, I received the same exception again at some point. I cannot see how this may ever happen, if all my front-end applications interact with the same OpenId Server where I made the override and - I expect - there is only one "entry point" to reach out the TokenCleanupBackgroundWorker where the exception happens and this "entry point" is the interaction between front-end applications and OpenId Server?

I see no problem with this code in the solution where I received the exception - the Store contains correct class:

It was related to using .First inside the loop. And we had many permissions. Replaced IEnumerable with the Dictionary and now it's fine.

Your code is no problem, that strange. Can you share a test project?

Unfortunately I cannot. I only can follow some recommendations. So from what you say I can deduce, that some of the custom store or all of them are not invoked, right? Where should I put the breakpoint in debug?

	using AbxEps.MyApp.AuthServer.Grants;
	using AbxEps.MyApp.AuthServer.Stores;
	using AbxEps.MyApp.Cryptography;
	using AbxEps.MyApp.EntityFrameworkCore;
	using AbxEps.MyApp.Filters;
	using AbxEps.MyApp.Localization;
	using AbxEps.MyApp.Middlewares;
	using AbxEps.MyApp.MultiTenancy;
	using AbxEps.LanguageCodeConvertor.Extensions;
	using AbxEps.Middleware.AbxRequestContext.Extensions;
	using AbxEps.Middleware.CookieAccessToken.Extensions;
	using Localization.Resources.AbpUi;
	using Medallion.Threading;
	using Medallion.Threading.Oracle;
	using Microsoft.AspNetCore.Builder;
	using Microsoft.AspNetCore.Cors;
	using Microsoft.AspNetCore.DataProtection;
	using Microsoft.AspNetCore.Extensions.DependencyInjection;
	using Microsoft.AspNetCore.Mvc;
	using Microsoft.Extensions.Configuration;
	using Microsoft.Extensions.DependencyInjection;
	using Microsoft.Extensions.Hosting;
	using Microsoft.Net.Http.Headers;
	using OpenIddict.Server.AspNetCore;
	using OpenIddict.Validation.AspNetCore;
	using Serilog;
	using StackExchange.Redis;
	using System;
	using System.IO;
	using System.Linq;
	using System.Security.Cryptography.X509Certificates;
	using Volo.Abp;
	using Volo.Abp.Account;
	using Volo.Abp.Account.Public.Web;
	using Volo.Abp.Account.Public.Web.Impersonation;
	using Volo.Abp.Account.Web;
	using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
	using Volo.Abp.AspNetCore.Mvc.UI.Theme.Lepton;
	using Volo.Abp.AspNetCore.Mvc.UI.Theme.Lepton.Bundling;
	using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared;
	using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Pages.Shared.Components.AbpApplicationPath;
	using Volo.Abp.AspNetCore.Serilog;
	using Volo.Abp.Auditing;
	using Volo.Abp.Autofac;
	using Volo.Abp.BackgroundJobs;
	using Volo.Abp.Caching;
	using Volo.Abp.Caching.StackExchangeRedis;
	using Volo.Abp.DistributedLocking;
	using Volo.Abp.FeatureManagement;
	using Volo.Abp.Identity;
	using Volo.Abp.Localization;
	using Volo.Abp.Modularity;
	using Volo.Abp.OpenIddict;
	using Volo.Abp.OpenIddict.Applications;
	using Volo.Abp.OpenIddict.Authorizations;
	using Volo.Abp.OpenIddict.ExtensionGrantTypes;
	using Volo.Abp.OpenIddict.Tokens;
	using Volo.Abp.PermissionManagement;
	using Volo.Abp.Ui.LayoutHooks;
	using Volo.Abp.UI.Navigation.Urls;
	using Volo.Abp.VirtualFileSystem;
	using Volo.Saas.Host;
	using AbxEps.CentralTools.AuthServer.Grants;
	using AbxEps.CentralTools.AuthServer.Stores;
	using AbxEps.CentralTools.Cryptography;
	using AbxEps.CentralTools.Filters;
	using AbxEps.CentralTools.Middlewares;
	using Twilio.Rest.Microvisor.V1;

	namespace AbxEps.MyApp;

	[DependsOn(
		typeof(AbpAutofacModule),
		typeof(AbpCachingStackExchangeRedisModule),
		typeof(AbpDistributedLockingModule),
		typeof(AbpAspNetCoreSerilogModule),
		typeof(AbpAccountPublicWebOpenIddictModule),
		typeof(AbpAccountPublicHttpApiModule),
		typeof(AbpAspNetCoreMvcUiLeptonThemeModule),
		typeof(AbpAccountPublicApplicationModule),
		typeof(AbpAccountPublicWebImpersonationModule),
		typeof(SaasHostApplicationContractsModule),
		typeof(MyAppEntityFrameworkCoreModule)
		)]
	public class MyAppAuthServerModule : AbpModule
	{
		private const string DefaultCorsPolicyName = "Default";

		public override void PreConfigureServices(ServiceConfigurationContext context)
		{
			var configuration = context.Services.GetConfiguration();
			var hostingEnvironment = context.Services.GetHostingEnvironment();

			//var logger = context.Services.GetInitLogger<MyAppAuthServerModule>();

			Log.Information($"*** {nameof(MyAppAuthServerModule)}.{nameof(PreConfigureServices)}: HostingEnvironment: {hostingEnvironment.EnvironmentName}");

			PreConfigure<AbpOpenIddictAspNetCoreOptions>(options =>
			{
				// https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html
				if (!hostingEnvironment.IsDevelopment())
				{
					options.AddDevelopmentEncryptionAndSigningCertificate = false;
				}
			});

			PreConfigure<OpenIddictServerBuilder>(builder =>
			{
				// https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html

				// not needed because default of AddDevelopmentEncryptionAndSigningCertificate is true in Development...
				/*if (hostingEnvironment.IsDevelopment())
				{
					builder
						.AddDevelopmentEncryptionCertificate()
						.AddDevelopmentSigningCertificate();
				}*/

				if (!hostingEnvironment.IsDevelopment())
				{
					// Encryption
					string encryptionCertThumbPrint = configuration["Certificates:Encryption:ThumbPrint"];
					string encryptionCertFile = configuration["Certificates:Encryption:File"];
					string encryptionCertPassword = configuration["Certificates:Encryption:Password"];

					if (string.IsNullOrEmpty(encryptionCertThumbPrint) && string.IsNullOrEmpty(encryptionCertFile))
					{
						string message = "Neither Certificates:Encryption:ThumbPrint nor Certificates:Encryption:File is set!";
						Log.Error($"*** {message}!");
						throw new NotSupportedException(message);
					}
					if (!string.IsNullOrEmpty(encryptionCertFile) && !File.Exists(encryptionCertFile))
					{
						string message = $"Cannot find encryption certificate '{encryptionCertFile}'!";
						Log.Error($"*** {message}!");
						throw new NotSupportedException(message);
					}

					X509Certificate2 certificate = X509Helper.GetCertificate(encryptionCertThumbPrint, encryptionCertFile, encryptionCertPassword, Log.Logger);

					if (certificate == null)
					{
						// throw exception, stop service...
						string cert = string.IsNullOrEmpty(encryptionCertThumbPrint) ? encryptionCertFile : encryptionCertThumbPrint;
						throw new NotSupportedException($"Encryption Certificate '{cert}' not found!");
					}
					else
					{
						Log.Debug($"Encryption Certificate: Issuer is '{certificate.Issuer}'");
						builder.AddEncryptionCertificate(certificate);
					}

					// Signing Certificate
					string signingCertThumbPrint = configuration["Certificates:Signing:ThumbPrint"];
					string signingCertFile = configuration["Certificates:Signing:file"];
					string signingCertPassword = configuration["Certificates:Signing:Password"];

					if (string.IsNullOrEmpty(signingCertThumbPrint) && string.IsNullOrEmpty(signingCertFile))
					{
						string message = "Neither Certificates:Signing:ThumbPrint nor Certificates:Signing:File is set!";
						Log.Error($"*** {message}!");
						throw new NotSupportedException(message);
					}
					if (!string.IsNullOrEmpty(signingCertFile) && !File.Exists(signingCertFile))
					{
						string message = $"Cannot find signing certificate '{signingCertFile}'!";
						Log.Error($"*** {message}!");
						throw new NotSupportedException(message);
					}

					certificate = X509Helper.GetCertificate(signingCertThumbPrint, signingCertFile, signingCertPassword, Log.Logger);

					if (certificate == null)
					{
						// throw exception, stop service...
						string cert = string.IsNullOrEmpty(signingCertThumbPrint) ? signingCertFile : signingCertThumbPrint;
						throw new NotSupportedException($"Signing Certificate '{cert}' not found!");
					}
					else
					{
						Log.Debug($"Signing Certificate: Issuer is '{certificate.Issuer}'");
						builder.AddSigningCertificate(certificate);
					}
				}

				builder.Configure(options =>
				{
					// adobu TODO
					// https://documentation.openiddict.com/configuration/token-formats.html#disabling-jwt-access-token-encryption
					// DisableAccessTokenEncryption is set to true in Volo.Abp.OpenIddict.AbpOpenIddictAspNetCoreModule.AddOpenIddictServer
					// options.DisableAccessTokenEncryption = false;

					//options.GrantTypes.Add(AbxLoginGrant.ExtensionGrantName);
					options.GrantTypes.Add(SwitchToTenantGrant.ExtensionGrantName);
				});
			});

			PreConfigure<OpenIddictBuilder>(builder =>
			{
				builder.AddValidation(options =>
				{
					options.AddAudiences("MyApp");
					options.UseLocalServer();
					options.UseAspNetCore();
				});
			});
		}

		public override void ConfigureServices(ServiceConfigurationContext context)
		{
			var hostingEnvironment = context.Services.GetHostingEnvironment();
			var configuration = context.Services.GetConfiguration();

			if (!Convert.ToBoolean(configuration["App:DisablePII"]))
			{
				Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
			}

			if (!Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]))
			{
				Configure<OpenIddictServerAspNetCoreOptions>(options =>
				{
					options.DisableTransportSecurityRequirement = true;
				});
			}

			context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);

			// This is added for a correct root path in js files
			Configure<AbpLayoutHookOptions>(options =>
			{
				options.Add(LayoutHooks.Head.Last,
				typeof(AbpApplicationPathViewComponent));
			});

			Configure<AbpLocalizationOptions>(options =>
			{
				options.Resources
					.Get<MyAppResource>()
					.AddBaseTypes(
						typeof(AbpUiResource)
					);
			});

			Configure<AbpBundlingOptions>(options =>
			{
				options.StyleBundles.Configure(
					LeptonThemeBundles.Styles.Global,
					bundle =>
					{
						bundle.AddFiles("/global-styles.css");
					}
				);
			});

			Configure<AbpAuditingOptions>(options =>
			{
				//options.IsEnabledForGetRequests = true;
				options.ApplicationName = "AuthServer";
			});

			if (hostingEnvironment.IsDevelopment())
			{
				Configure<AbpVirtualFileSystemOptions>(options =>
				{
					options.FileSets.ReplaceEmbeddedByPhysical<MyAppDomainSharedModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}AbxEps.MyApp.Domain.Shared", Path.DirectorySeparatorChar)));
					options.FileSets.ReplaceEmbeddedByPhysical<MyAppDomainModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}AbxEps.MyApp.Domain", Path.DirectorySeparatorChar)));
				});
			}

			Configure<AppUrlOptions>(options =>
			{
				options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];
				options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"].Split(','));
				options.Applications["Angular"].RootUrl = configuration["App:AngularUrl"];
				options.Applications["Angular"].Urls[AccountUrlNames.PasswordReset] = "account/reset-password";
				options.Applications["Angular"].Urls[AccountUrlNames.EmailConfirmation] = "account/email-confirmation";
			});

			Configure<AbpBackgroundJobOptions>(options =>
			{
				options.IsJobExecutionEnabled = false;
			});

			Configure<AbpDistributedCacheOptions>(options =>
			{
				options.KeyPrefix = "Abx:";
			});

			Configure<MvcOptions>(options =>
			{
				options.Filters.Add(typeof(TokenExpiredExceptionFilter));
			});

			var dataProtectionBuilder = context.Services.AddDataProtection().SetApplicationName("MyApp");
			if (!hostingEnvironment.IsDevelopment())
			{
				var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]);
				dataProtectionBuilder.PersistKeysToStackExchangeRedis(redis, "AuthServer-Protection-Keys");
			}

			context.Services.AddSingleton<IDistributedLockProvider>(sp =>
			{
				//var connection = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]);
				//return new RedisDistributedSynchronizationProvider(connection.GetDatabase());
				return new OracleDistributedSynchronizationProvider(configuration["ConnectionStrings:Default"]);
			});

			ConfigureCors(context, configuration);
			ConfigureAuth(context, configuration);
			ConfigureOpenIdDict(context, configuration);
			ConfigureStores(context, configuration);

			context.Services.Configure<AbpAccountOptions>(options =>
			{
				options.TenantAdminUserName = "admin";
				options.ImpersonationTenantPermission = SaasHostPermissions.Tenants.Impersonation;
				options.ImpersonationUserPermission = IdentityPermissions.Users.Impersonation;
			});

			Configure<PermissionManagementOptions>(options =>
			{
				options.IsDynamicPermissionStoreEnabled = true;
			});

			Configure<FeatureManagementOptions>(options =>
			{
				options.IsDynamicFeatureStoreEnabled = true;
			});
		}

		private static void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration)
		{
			context.Services.AddCors(options =>
			{
				options.AddPolicy(DefaultCorsPolicyName, builder =>
				{
					builder
						.WithOrigins(
							configuration["App:CorsOrigins"]
								.Split(",", StringSplitOptions.RemoveEmptyEntries)
								.Select(o => o.Trim().RemovePostFix("/"))
								.ToArray()
						)
						.WithAbpExposedHeaders()
						.SetIsOriginAllowedToAllowWildcardSubdomains()
						.AllowAnyHeader()
						.AllowAnyMethod()
						.AllowCredentials();
				});
			});
		}

		private static void ConfigureAuth(ServiceConfigurationContext context, IConfiguration configuration)
		{
			context.Services.AddAuthentication();
		}

		private static void ConfigureStores(ServiceConfigurationContext context, IConfiguration configuration)
		{
			context.Services.AddOpenIddict()
				.AddCore(builder =>
				{
					builder
						.AddApplicationStore<AbxAbpOpenIddictApplicationStore>()
						.AddAuthorizationStore<AbxAbpOpenIddictAuthorizationStore>()
						.AddTokenStore<AbxAbpOpenIddictTokenStore>();
				});
		}

		private void ConfigureOpenIdDict(ServiceConfigurationContext context, IConfiguration configuration)
		{
			// Configure<TokenCleanupOptions>(options => { });

			Configure<AbpOpenIddictExtensionGrantsOptions>(options =>
			{
				//options.Grants.Add(AbxLoginGrant.ExtensionGrantName, new AbxLoginGrant());
				options.Grants.Add(SwitchToTenantGrant.ExtensionGrantName, new SwitchToTenantGrant());
			});

			// This is required for working of AbxRequestContext
			context.Services.AddHttpContextAccessor();
			context.Services.InitLanguageCodeConvertor(configuration);
			context.Services.InitAbxRequestContext();
		}

		public override void OnApplicationInitialization(ApplicationInitializationContext context)
		{

			var app = context.GetApplicationBuilder();
			var env = context.GetEnvironment();

			if (env.IsDevelopment())
			{
				app.UseDeveloperExceptionPage();
			}

			app.UseAbpRequestLocalization();

			if (!env.IsDevelopment())
			{
				app.UseErrorPage();
			}

			app.UseCorrelationId();
			app.UseAbpSecurityHeaders();
			app.UseStaticFiles();
			app.UseRouting();
			app.UseCors(DefaultCorsPolicyName);
			app.UseAuthentication();
			//app.UseAbpOpenIddictValidation();  // UseIdentityServer
			app.UseCookieAccessTokenDelete(HeaderNames.Authorization);

			/* https://docs.abp.io/en/abp/7.0/Migration-Guides/IdentityServer_To_OpenIddict
			app.UseJwtTokenMiddleware();*/

			if (MultiTenancyConsts.IsEnabled)
			{
				app.UseMultiTenancy();
			}

			app.UseUnitOfWork();
			app.UseMiddleware<SessionEndAuditingMiddleware>();
			app.UseMiddleware<TenantSwitchMiddleware>();

			app.UseAbpOpenIddictValidation();  // UseIdentityServer
			app.UseAuthorization();

			app.UseAuditing();
			app.UseAbpSerilogEnrichers();
			app.UseConfiguredEndpoints();
		}
	}

So while the initial exception reproducing still pending, I received this again:

> [09:26:33 INF] Lock is acquired for TokenCleanupBackgroundWorker
> [09:26:33 INF] Start cleanup.
> [09:26:33 INF] Start cleanup tokens.
> fail: Microsoft.EntityFrameworkCore.Infrastructure[0]
> 2024-08-19 09:26:34.569195 ThreadID:500 (ERROR)   OracleExecutionStrategy.ExecuteAsync() :  System.ArgumentException: IsolationLevel must be ReadCommitted or Serializable (Parameter 'isolationLevel')
> at Oracle.ManagedDataAccess.Client.OracleConnection.BeginTransaction(IsolationLevel isolationLevel)
> at Oracle.ManagedDataAccess.Client.OracleConnection.BeginDbTransaction(IsolationLevel isolationLevel)
> at System.Data.Common.DbConnection.BeginDbTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken)
> --- End of stack trace from previous location ---
> at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken)
> at Oracle.EntityFrameworkCore.Storage.Internal.OracleExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
> [09:26:34 ERR] IsolationLevel must be ReadCommitted or Serializable (Parameter 'isolationLevel')
> System.ArgumentException: IsolationLevel must be ReadCommitted or Serializable (Parameter 'isolationLevel')
> at Oracle.ManagedDataAccess.Client.OracleConnection.BeginTransaction(IsolationLevel isolationLevel)
> at Oracle.ManagedDataAccess.Client.OracleConnection.BeginDbTransaction(IsolationLevel isolationLevel)
> at System.Data.Common.DbConnection.BeginDbTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken)
> --- End of stack trace from previous location ---
> at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken)
> at Oracle.EntityFrameworkCore.Storage.Internal.OracleExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
> at Volo.Abp.Uow.EntityFrameworkCore.UnitOfWorkDbContextProvider`1.CreateDbContextWithTransactionAsync(IUnitOfWork unitOfWork)
> at Volo.Abp.Uow.EntityFrameworkCore.UnitOfWorkDbContextProvider`1.CreateDbContextAsync(IUnitOfWork unitOfWork)
> at Volo.Abp.Uow.EntityFrameworkCore.UnitOfWorkDbContextProvider`1.CreateDbContextAsync(IUnitOfWork unitOfWork, String connectionStringName, String connectionString)
> at Volo.Abp.Uow.EntityFrameworkCore.UnitOfWorkDbContextProvider`1.GetDbContextAsync()
> at Volo.Abp.Domain.Repositories.EntityFrameworkCore.EfCoreRepository`2.GetDbSetAsync()
> at Volo.Abp.Domain.Repositories.EntityFrameworkCore.EfCoreRepository`2.GetQueryableAsync()
> at Volo.Abp.OpenIddict.Tokens.EfCoreOpenIddictTokenRepository.PruneAsync(DateTime date, CancellationToken cancellationToken)
> at Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo)
> at Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue`1.ProceedAsync()
> at Volo.Abp.Uow.UnitOfWorkInterceptor.InterceptAsync(IAbpMethodInvocation invocation)
> at Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter`1.InterceptAsync[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo, Func`3 proceed)
> at Volo.Abp.OpenIddict.Tokens.AbpOpenIddictTokenStore.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
> at Volo.Abp.OpenIddict.Tokens.TokenCleanupService.CleanAsync()

And it is happening after I replaced all the stores as you suggested. The code of the stores was copy-pasted from yours:

context.Services.AddOpenIddict()
    .AddCore(builder =>
    {
        builder
            .AddApplicationStore<AbxAbpOpenIddictApplicationStore>()
            .AddAuthorizationStore<AbxAbpOpenIddictAuthorizationStore>()
            .AddTokenStore<AbxAbpOpenIddictTokenStore>();
    });

Maybe I am adding this code too late inside ConfigureServices if the order matters at all here? What is it supposed to follow in OpenIDServer module?

Please share full request and response logs.

Sorry - my bad (due to very late time here). I've updated the message above. So it is seen the time is spent here. But there is no more information:

2024-08-16 04:25:17.610 -05:00 [DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Roles 2024-08-16 04:25:55.009 -05:00 [DBG] Added 0 entity changes to the current audit log

Showing 31 to 40 of 289 entries
Made with ❤️ on ABP v9.1.0-preview. Updated on November 20, 2024, 13:06