Back
Close

Multi-tenant ASP.NET Core 3 - Implementing blob-storage based tenant provider

gpeipman
13.9K views

Blob storage based tenant provider

It's possible that multi-tenant web application has access only to tenants definitions or settings but it has no access to actual tenants data. This decision is done often to have one risk vector less - multi-tenant web application cannot be used to attack tenants database.

Let's start with tenant DTO and tenant provider interface.

public class Tenant
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string HostName { get; set; }
    
    // other tenant settings
}

public interface ITenantProvider
{
    Guid GetTenantId();
}

Suppose tenants information is in JSON file hosted on Microsoft Azure blob storage.

[
  {
    "Id": "51aab199-1482-4f0d-8ff1-5ca0e7bc525a",
    "Name": "Imaginary corp.",
    "HostName": "imaginary.example.com"
  },
  {
    "Id": "ae4e21fa-57cb-4733-b971-fdd14c4c667e",
    "Name": "The Very Big corp.",
    "HostName": "big.example.com"
  }
]

We need tenant provider that is able to read the JSON file, deserialize it and cache it locally. Communicating with blob storage means we need blob storage settings that we hold in application config file. For this we inject IConfiguration to tenant provider. Example here uses local demo data so the application can run in tech.io environment.

public class BlobStorageTenantProvider : ITenantProvider
{
    private static IList<Tenant> _tenants;

    private Guid _tenantId = Guid.Empty;

    public BlobStorageTenantProvider(IHttpContextAccessor accessor, IConfiguration conf)
    {
        if (_tenants == null)
        {
            // Remove with method itself for real solution
            LoadDemoTenants();

            // Comment out for real solution
            //LoadTenants(conf["StorageConnectionString"], conf["TenantsContainerName"], conf["TenantsBlobName"]);
        }

        // Comment out for real solution
        //var host = accessor.HttpContext.Request.Host.Value;

        // Remove for real solution
        var host = "imaginary.example.com";

        var tenant = _tenants.FirstOrDefault(t => t.HostName.ToLower() == host.ToLower());
        if (tenant != null)
        {
            _tenantId = tenant.Id;
        }
    }

    private void LoadTenants(string connStr, string containerName, string blobName)
    {
        var storageAccount = CloudStorageAccount.Parse(connStr);
        var blobClient = storageAccount.CreateCloudBlobClient();
        var container = blobClient.GetContainerReference(containerName);
        var blob = container.GetBlobReference(blobName);

        blob.FetchAttributesAsync().GetAwaiter().GetResult();

        var fileBytes = new byte[blob.Properties.Length];

        using (var stream = blob.OpenReadAsync().GetAwaiter().GetResult())
        using (var textReader = new StreamReader(stream))
        using (var reader = new JsonTextReader(textReader))
        {
            _tenants = JsonSerializer.Create().Deserialize<List<Tenant>>(reader);
        }
    }

    private void LoadDemoTenants()
    {
        _tenants = new List<Tenant>();

        _tenants.Add(new Tenant
        {
            Id = MultitenantDbContext.Tenant1Id,
            Name = "Imaginary corp.",
            HostName = "imaginary.example.com"
        });

        _tenants.Add(new Tenant
        {
            Id = MultitenantDbContext.Tenant2Id,
            Name = "The Very Big corp.",
            HostName = "big.example.com"
        });
    }

    public Guid GetTenantId()
    {
        return _tenantId;
    }
}

We need this tenant provider in database context used by multi-tenant web application.

public class MultitenantDbContext : DbContext
{
    public static Guid Tenant1Id = Guid.Parse("51aab199-1482-4f0d-8ff1-5ca0e7bc525a");
    public static Guid Tenant2Id = Guid.Parse("ae4e21fa-57cb-4733-b971-fdd14c4c667e");

    public DbSet<Person> People { get; set; }

    private ITenantProvider _tenantProvider;

    public MultitenantDbContext(DbContextOptions<MultitenantDbContext> options,
                                ITenantProvider tenantProvider) : base(options)
    {
        _tenantProvider = tenantProvider;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Person>().HasQueryFilter(p => p.TenantId == _tenantProvider.GetTenantId());
    }

    public void AddSampleData()
    {
        People.Add(new Person {
            Id = Guid.Parse("79865406-e01b-422f-bd09-92e116a0664a"),
            TenantId = Tenant1Id,
            FirstName = "Gunnar",
            LastName = "Peipman"
        });

        People.Add(new Person
        {
            Id = Guid.Parse("d5674750-7f6b-43b9-b91b-d27b7ac13572"),
            TenantId = Tenant2Id,
            FirstName = "John",
            LastName = "Doe"
        });

        People.Add(new Person
        {
            Id = Guid.Parse("e41446f9-c779-4ff6-b3e5-752a3dad97bb"),
            TenantId = Tenant1Id,
            FirstName = "Mary",
            LastName = "Jones"
        });

        SaveChanges();
    }
}

Before runnig the application we need to register services so dependency injection knows what to do. Services are configured in ConfigureServices() method of Startup class.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<MultitenantDbContext>(o => o.UseInMemoryDatabase(Guid.NewGuid().ToString()));

    services.AddMvc();

    services.AddTransient<ITenantProvider, BlobStorageTenantProvider>();
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

Demo

Now everything is ready for running the demo. Compare data added to People set of database context given above to data shown on front page of demo application.

Click Run to run the demo

References

Create your playground on Tech.io
This playground was created on Tech.io, our hands-on, knowledge-sharing platform for developers.
Go to tech.io