Hi @RenuSolutions,
Thank you very much. The error is stated in that application log snippet.
Within the Tenant provisioning endpoint, the code attempts to send the activation email to the admin
user.
It appears that email sending is failing, and there isn't anything that is catching that Exception within the endpoint.
As a result, the UnitOfWork is failing / cancelling, and the endpoint is returning a 500 Internal Server Error response to the browers:
If you are running the applicaiton locally, go to this line in your TenantAppService and put a breakpoint there, and then if you step through the code execution, you'll see the exception occurring.
AnyCollect.Application\MultiTenancy\TenantAppService.cs:line 59
Do you have your SMTP email configured in this instance?
-Brian
Hi @kansoftware,
This is an interesting problem.
I'm just offering some thoughts here.
If you are open to manual intervention in the backend, you could login to the HOST, click on Tenants. Then for the Tenant, click Actions > Edit, and extend their subscription end date, so that they can log back in to renew their subscription. You could also set them in a Trial period if that would help at all.
I don't think there is a way to force their tenancy to accept user logins, but only allow access to the Renew / Upgrade interface.
I hope that helps, -Brian
Hi @RenuSolutions,
Have you done anything with your logging configuration? Is this a local environment or is this a deployed environment running on a hosting provider?
If this is running locally or a single node/server deployed on a hosting provider, you should be able to get your web logs, under the Administration menu.
If you click on Administration > Maintenance, that page should show you 2 tabs, 1 for Caches and another for Web Site Logs
If you click on the Web Site Logs tab, there should be a button to "Download All"
I would start there to see what the server-side logging states. If you can find a section of log statements that corresponds to the time that you produce that error, post it here and I'll see if I can identify anything that might help.
Cheers! -Brian
Thank you @ismcagdas.
Unfortunately, I was working with a newly generated v11.2.0 project for my test.
I will pull the latest source code of ABP 7.3.0 and see if there is a way to step through the Castle registration process and why the Abp.AspNetCore.Configuration.AbpAspNetCoreConfiguration
service might not be registered
Hi @ismcagdas,
The main thought behind this is for the publicly available endpoints (AllowAnonymous). For methods like TokenAuthController.Authenticate or AccountAppService.IsTenantAvailable, I think it's a reasonable consideration to want to rate-limit these endpoints.
Obviously there can be preventative measures in-place for upstream networking & security devices and rules, such as an Azure Application Gateway WAF, to implement DDoS attack prevention.
The "AspNetCoreRateLimit" project can work. I was hoping more for an Attribute-driven approach, similar to [AbpAuthorize] or [RequireFeature]. Additionally, I wasn't sure how this projects implementation of the IDistributedCache interface vs Abp's CacheManager would potentially conflict.
Thanks! -Brian
Hi @Astech,
The "UserFriendlyException" class and exception modal that you are showing in your code snippet and screenshot are generic, so in order to implement something like this you would need to provide a custom implementation.
An easy way to do this would be to define your own custom exception class, such as UserCountMaximumReachedException
.
Then in your UI code, you would need to modify the http-interceptor
behavior.
I'm not sure if this implementation has changed since the version I'm working with, but in my Angular typescript code, there are classes under MyProject.Web.Host > src > shared > common > interceptors.
You can breakpoint this code while developing locally to see how it works, and to see how you could potentially handle a custom response in the Angular UI to your custom Exception class.
I hope that helps! -Brian
Thanks @ismcagdas!
I let you know how it goes. -Brian
Hi @pliaspzero ,
just curious - are you deploying to Azure? And if so, are you using Azure Redis or are you managing your own Redis cluster?
-Brian
@Jason,
Here is my AzureTablesOnlineClientStore
class. Please note that this was originally written against ABP v4.5.0 and ANZ v6.9.0. I have not tried running this against ANZ v11.1.0.
using Abp.RealTime;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Azure.Data.Tables;
using BrianPieslak.ANZ.Configuration;
using Azure;
using Microsoft.Extensions.Configuration;
using Abp.Runtime.Security;
namespace BrianPieslak.ANZ.Notifications
{
public class AzureTablesOnlineClientStore : IOnlineClientStore
{
private readonly IAppConfigurationAccessor _configurationAccessor;
public AzureTablesOnlineClientStore(IAppConfigurationAccessor configurationAccessor)
{
_configurationAccessor = configurationAccessor;
}
public void Add(IOnlineClient client)
{
var id = PerformOperation<string>((tableClient) =>
{
var partitionKey = client.TenantId.HasValue ? client.TenantId.Value.ToString().PadLeft(10, '0') : "HOST";
var entity = new OnlineClientEntity()
{
PartitionKey = partitionKey,
RowKey = client.ConnectionId,
Data = SerializeObject(client)
};
tableClient.AddEntity<OnlineClientEntity>(entity);
return client.ConnectionId;
});
}
public bool Remove(string connectionId)
{
return TryRemove(connectionId, out IOnlineClient removed);
}
public bool TryRemove(string connectionId, out IOnlineClient client)
{
client = PerformOperation<IOnlineClient>((tableClient) =>
{
var resultsqueryResults = tableClient.Query<OnlineClientEntity>(ent => ent.RowKey == connectionId);
OnlineClientEntity entity = null;
IOnlineClient result = null;
if (resultsqueryResults != null && resultsqueryResults.Count() == 1)
{
foreach (Page<OnlineClientEntity> page in resultsqueryResults.AsPages())
{
foreach (OnlineClientEntity qEntity in page.Values)
{
try
{
result = DeserializeObject(qEntity.Data);
entity = qEntity;
}
catch (Exception)
{
//unable to decrypt the record so remove it
tableClient.DeleteEntity(qEntity.PartitionKey, qEntity.RowKey);
}
}
}
}
if(entity != null)
{
tableClient.DeleteEntity(entity.PartitionKey, entity.RowKey);
}
return result;
});
return client != null;
}
public bool TryGet(string connectionId, out IOnlineClient client)
{
client = PerformOperation<IOnlineClient>((tableClient) =>
{
var resultsqueryResults = tableClient.Query<OnlineClientEntity>(ent => ent.RowKey == connectionId);
IOnlineClient result = null;
if (resultsqueryResults != null && resultsqueryResults.Count() == 1)
{
foreach (Page<OnlineClientEntity> page in resultsqueryResults.AsPages())
{
foreach (OnlineClientEntity qEntity in page.Values)
{
try
{
result = DeserializeObject(qEntity.Data);
}
catch (Exception)
{
//unable to decrypt the record so remove it
tableClient.DeleteEntity(qEntity.PartitionKey, qEntity.RowKey);
}
}
}
}
return result;
});
return client != null;
}
public bool Contains(string connectionId)
{
var id = PerformOperation<string>((tableClient) =>
{
var results = tableClient.Query<OnlineClientEntity>(ent => ent.RowKey == connectionId);
if (results != null && results.Count() == 1)
return connectionId;
return null;
});
return !String.IsNullOrEmpty(id);
}
public IReadOnlyList<IOnlineClient> GetAll()
{
return PerformOperation<IReadOnlyList<IOnlineClient>>((tableClient) =>
{
var resultsqueryResults = tableClient.Query<OnlineClientEntity>();
var result = new List<IOnlineClient>();
if (resultsqueryResults != null)
{
foreach (Page<OnlineClientEntity> page in resultsqueryResults.AsPages())
{
foreach (OnlineClientEntity qEntity in page.Values)
{
try
{
result.Add(DeserializeObject(qEntity.Data));
}
catch (Exception)
{
//unable to decrypt the record so remove it
tableClient.DeleteEntity(qEntity.PartitionKey, qEntity.RowKey);
}
}
}
}
return result.ToImmutableList();
});
}
private T PerformOperation<T>(Func<TableClient, Object> function) where T : class
{
var tableClient = GetTableClient();
return function(tableClient) as T;
}
private TableServiceClient GetTableServiceClient()
{
var configuration = _configurationAccessor.Configuration;
string connectionStringName = configuration["App:SignalR:OnlineClientStore:Azure:ConnectionString"];
string connectionString = configuration.GetConnectionString(connectionStringName);
if (String.IsNullOrEmpty(connectionString))
{
connectionString = connectionStringName;
}
return new TableServiceClient(connectionString);
}
private TableClient GetTableClient()
{
return GetTableClient(GetTableServiceClient());
}
private TableClient GetTableClient(TableServiceClient serviceClient)
{
var configuration = _configurationAccessor.Configuration;
string tableName = configuration["App:SignalR:OnlineClientStore:Azure:TableName"];
serviceClient.CreateTableIfNotExists(tableName);
return serviceClient.GetTableClient(tableName);
}
private class OnlineClientEntity : ITableEntity
{
public string Data { get; set; }
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTimeOffset? Timestamp { get; set; }
public Azure.ETag ETag { get; set; }
}
private string SerializeObject(IOnlineClient client)
{
var configuration = _configurationAccessor.Configuration;
var data = Newtonsoft.Json.JsonConvert.SerializeObject(client);
if (bool.TryParse(configuration["App:SignalR:OnlineClientStore:StoreEncrypted"], out bool storeEncrypted) && storeEncrypted)
{
data = SimpleStringCipher.Instance.Encrypt(data);
}
return data;
}
private Abp.RealTime.OnlineClient DeserializeObject(string data)
{
var configuration = _configurationAccessor.Configuration;
if (bool.TryParse(configuration["App:SignalR:OnlineClientStore:StoreEncrypted"], out bool storeEncrypted) && storeEncrypted)
{
data = SimpleStringCipher.Instance.Decrypt(data);
}
return Newtonsoft.Json.JsonConvert.DeserializeObject<Abp.RealTime.OnlineClient>(data);
}
}
}
This class is defined in my ".Core" project.
Then to use this class, in the Module of your .Web.Core project (mine is: BrianPieslakWebCoreModule), in the PreInitialize
method, I have the following:
//Online Client Cache
Type replacementOnlineCacheStore = default;
if (bool.TryParse(_appConfiguration["App:SignalR:OnlineClientStore:Azure:Enabled"], out bool signalRAzureEnabled) && signalRAzureEnabled)
{
replacementOnlineCacheStore = typeof(AzureTablesOnlineClientStore);
}
if (replacementOnlineCacheStore != default)
{
if (IocManager.IsRegistered<IOnlineClientStore>())
{
Configuration.ReplaceService(typeof(IOnlineClientStore), replacementOnlineCacheStore, Abp.Dependency.DependencyLifeStyle.Singleton);
}
else
{
IocManager.IocContainer.Register(Component.For(typeof(IOnlineClientStore)).ImplementedBy(replacementOnlineCacheStore).LifestyleSingleton());
}
}
This code is slightly more complicated than it needs to be because I support other implementations of IOnlineClientStore, such as Sql & Redis. This could be more simply implemented as an extension method. I guess I just got a little lazy =D
Then lastly, to drive my configuration via settings, you'd need to add this tag to your appsettings.json (and then override as appropriate in your appsettings.<environment>.json
"App": {
"SignalR": {
"Azure": {
"Enabled": true,
"ConnectionString": "AzureSignalR"
},
"OnlineClientStore": {
"StoreEncrypted": true,
"Azure": {
"Enabled": true,
"ConnectionString": "AzureStorage",
"TableName": "OnlineClientCache"
}
}
}
}
The first object of "App:SignalR:Azure" drives if I'm using the Azure Signalr service, and the ConnectionString attribute references a named ConnectionString in the "ConnectionStrings" portion of the configuration file.
The "App:SignalR:OnlineClientStore" object drives how my PreInitialization code wires up a replacement IOnlineClientStore service, if configured.
I hope this helps.
@ismcagdas - feel free to use any/all of this in the ANZ / ABP framework if you want, or I can submit a ticket in github and contribute my code there.
Cheers! -Brian
Hi @dexter.cunanan
what technology are you using for your host service? are you deploying using Docker, by chance?
I have seen cases where DataProtection defaults to the local file system, but still fails to work properly on Docker container instances, even if you are running just a single instance.
https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-6.0
https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/default-settings?view=aspnetcore-6.0
When hosting in a Docker container, keys should be persisted in a folder that's a Docker volume (a shared volume or a host-mounted volume that persists beyond the container's lifetime) or in an external provider, such as Azure Key Vault or Redis. An external provider is also useful in web farm scenarios if apps can't access a shared network volume (see PersistKeysToFileSystem for more information).
@ismcagdas - does the ABP / ANZ framework call .AddDataProtection anywhere? I can see the DataProtection assemblies referenced, but I can't find anything in the ABP source code on github that calls .AddDataProtection.
-Brian