Related
I have an AuthorizationRequirement class like this-
internal class ClaimsRoleRequirement : IAuthorizationRequirement
{
private readonly EClaim eClaimValue;
public ClaimsRoleRequirement(object claimValue)
{
eClaimValue = (EClaim)claimValue;
//name = eClaimValue.ToString();
//description = eClaimValue.Description();
}
}
And I am calling this from Startup.cs file like this-
public void ConfigureServices(IServiceCollection services)
{
..................
..................
services.AddAuthorization(options => {
foreach (object eClaimValue in Enum.GetValues(typeof(EClaim)))
{
options.AddPolicy(eClaimValue.ToString(), policy => policy.Requirements.Add(new ClaimsRoleRequirement(eClaimValue)));
}
});
}
I like to query DB and get the current user from the session in ClaimsRoleRequirement class.
Is there any way of doing this?
Re-
EClaim enum is like this-
public enum EClaim
{
[Display(Name = "Role-Claim Policy")]
[Description("Role-Claim-View")]
RoleClaimView = 0,
[Description("Role Create")]
RoleCreate,
[Description("Claim Create")]
ClaimCreate
}
My DB context is like this-
public class ApplicationDbContext : IdentityDbContext<User, Role, Guid, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>
{
private string IdentitySchemaName = "Identity";
private readonly IWebHostEnvironment Environment;
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IWebHostEnvironment env)
: base(options)
{
Environment = env;
//this.Database.EnsureCreated();
}
........
........
}
The authorization requirement, ClaimsRoleRequirement in your case, is just meant as some kind of a marker that specifies that this requirement is needed for a user to access a resource. However, it is not the job of the requirement to specify how this requirement is verified for a user.
To evaluate the requirement, you will need to create an authorization handler for your requirement. The authorization will then get called when the authorization system is evaluating the requirement and the handler gets a chance of evaluating it. As part of that process, the handler can also access request-specific information, e.g. from the session, or query other services to retrieve further information.
The handler for your requirement could for example look like this:
public class ClaimsRoleAuthorizationHandler : AuthorizationHandler<ClaimsRoleRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ClaimsRoleAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ClaimsRoleRequirement requirement)
{
var user = context.User;
var httpContext = _httpContextAccessor.HttpContext;
var session = httpContext.Session;
var db = httpContext.RequestServices.GetService<ApplicationDbContext>();
// do stuff
// finally, call Succeed for the requirement if the user fulfills the requirement
context.Succeed(requirement);
}
}
That being said, when using ASP.NET Core Identity and you just want to verify the roles or custom claims of your identity, you won’t need to make a round-trip to your database in order to verify these requirement. Configured roles of your identity will automatically be added as role claims and custom claims you configured for the application user will also be available on the claims principal itself. So you should be able to verify these by doing something like context.User.IsInRole(roleName) or context.User.HasClaim(claimType).
Typically Options are singleton. However i am building options from the database, and one of the Options property is password which keep changing every month. So i wanted to create Scoped instance of Options. I am using IConfigureOptions<T> like below to build Options from the database
public class MyOptions
{
public string UserID {get;set;}
public string Password {get;set;
}
public class ConfigureMyOptions : IConfigureOptions<MyOptions>
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public ConfigureMyOptions(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
public void Configure(MyOptions options)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var provider = scope.ServiceProvider;
using (var dbContext = provider.GetRequiredService<MyDBContext>())
{
options.Configuration = dbContext.MyOptions
.SingleOrDefault()
.Select(x => new MyOptions()
{
UserID = x.UserID,
Password = x.Password
});
}
}
}
}
Use it in controller
public class HomeController : BaseController
{
private readonly MyOptions _options;
public HomeController(IOptions<MyOptions> option)
{
_options = option.Value;
}
[HttpGet]
[Route("home/getvalue")]
public string GetValue()
{
// do something with _options here
return "Success";
}
}
I want to create an instance of MyOptions for every new request so register it as Scoped in startup.cs
services.AddScoped<IConfigureOptions<MyOptions>, ConfigureMyOptions>();
However, when i put debugger inside ConfigureMyOptions's Configure method it only gets hit once for the first request. For next request onward the container returns the same instance (like singleton).
How do i set the scope here so MyOptions will get created for each request?
Use IOptionsSnapshot instead of IOptions in your controller and it will recreate options per request.
Why doesn't work with IOptions:
.AddOptions extension method of Configuration API registers the OptionsManager instance as a singlethon for IOptions<>
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
and OptionsManager class uses caching internally:
public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
// Store the options in our instance cache
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
The following issue on github helped to find above: OptionsSnapshot should always be recreated per request
After a few hours of research and finding no way to do this; it's time to ask the question.
I have an ASP.NET Core 1.1 project using EF Core and MVC that is used by multiple customers. Each customer has their own database with the exact same schema. This project is currently a Windows application being migrated to the web. At the login screen the user has three fields, Company Code, Username and Password. I need to be able to change the connection string when the user attempts to login based on what they type in the Company Code input then remember their input throughout the session duration.
I found some ways to do this with one database and multiple schema, but none with multiple databases using the same schema.
The way I solved this problem isn't an actual solution to the problem, but a work around that worked for me. My databases and app are hosted on Azure. My fix to this was to upgrade my app service to a plan that supports slots (only an extra $20 a month for 5 slots). Each slot has the same program but the environment variable that holds the connection string is company specific. This way I can also subdomain each companies access if I want. While this approach may not be what others would do, it was the most cost effective to me. It is easier to publish to each slot than to spend the hours doing the other programming that doesn't work right. Until Microsoft makes it easy to change the connection string this is my solution.
In response to Herzl's answer
This seems like it could work. I have tried to get it implemented. One thing I am doing though is using a repository class that accesses my context. My controllers get the repository injected into them to call methods in the repository that access the context. How do I do this in a repository class. There is no OnActionExecuting overload in my repository. Also, if this persists for the session, what happens when a user opens their browser to the app again and is still logged in with a cookie that lasts 7 days? Isn't this a new session? Sounds like the app would throw an exception because the session variable would be null and therefor not have a complete connection string. I guess I could also store it as a Claim and use the Claim if the session variable is null.
Here is my repository class. IDbContextService was ProgramContext but I started adding your suggestions to try and get it to work.
public class ProjectRepository : IProjectRepository
{
private IDbContextService _context;
private ILogger<ProjectRepository> _logger;
private UserManager<ApplicationUser> _userManager;
public ProjectRepository(IDbContextService context,
ILogger<ProjectRepository> logger,
UserManager<ApplicationUser> userManger)
{
_context = context;
_logger = logger;
_userManager = userManger;
}
public async Task<bool> SaveChangesAsync()
{
return (await _context.SaveChangesAsync()) > 0;
}
}
In response to The FORCE JB's answer
I tried to implement your approach. I get an exception in Program.cs on line
host.Run();
Here is my 'Program.cs' class. Untouched.
using System.IO;
using Microsoft.AspNetCore.Hosting;
namespace Project
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
}
}
And my 'Startup.cs' class.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using Project.Entities;
using Project.Services;
namespace Project
{
public class Startup
{
private IConfigurationRoot _config;
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables();
_config = builder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(_config);
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.User.RequireUniqueEmail = true;
config.Password.RequireDigit = true;
config.Password.RequireLowercase = true;
config.Password.RequireUppercase = true;
config.Password.RequireNonAlphanumeric = false;
config.Password.RequiredLength = 8;
config.Cookies.ApplicationCookie.LoginPath = "/Auth/Login";
config.Cookies.ApplicationCookie.ExpireTimeSpan = new TimeSpan(7, 0, 0, 0); // Cookies last 7 days
})
.AddEntityFrameworkStores<ProjectContext>();
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, AppClaimsPrincipalFactory>();
services.AddScoped<IProjectRepository, ProjectRepository>();
services.AddTransient<MiscService>();
services.AddLogging();
services.AddMvc()
.AddJsonOptions(config =>
{
config.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
Dictionary<string, string> connStrs = new Dictionary<string, string>();
connStrs.Add("company1", "1stconnectionstring"));
connStrs.Add("company2", "2ndconnectionstring";
DbContextFactory.SetDConnectionString(connStrs);
//app.UseDefaultFiles();
app.UseStaticFiles();
app.UseIdentity();
app.UseMvc(config =>
{
config.MapRoute(
name: "Default",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Auth", action = "Login" }
);
});
}
}
}
And the exception:
InvalidOperationException: Unable to resolve service for type 'Project.Entities.ProjectContext' while attempting to activate 'Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`4[Project.Entities.ApplicationUser,Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole,Project.Entities.ProjectContext,System.String]'.
Not sure what to do here.
Partial success edit
Okay I got your example working. I can set the connection string in my repository constructor using a different id. My problem now is logging in and choosing the right database. I thought about having the repository pull from a session or claim, whatever wasn't null. But I can't set the value before using the SignInManager in the Login controller because SignInManager is injected into the controller which creates a context before I update the session variable. The only way I can think of is to have a two page login. The first page will ask for the company code and update the session variable. The second page will use the SignInManager and have the repository injected into the controllers constructor. This would happen after the first page updates the session variable. This may actually be more visually appealing with animations between both login views. Unless anyone has any ideas on a way to do this without two login views I am going to try and implement the two page login and post the code if it works.
It is actually broken
When it was working, it is because I still had a valid cookie. I would run the project and it would skip the login. Now I get the exception InvalidOperationException: No database provider has been configured for this DbContext after clearing my cache. I have stepped through it all and the context is being created correctly. My guess is that Identity is having some sort of issues. Could the below code adding the entity framework stores in ConfigureServices be causing the issue?
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.User.RequireUniqueEmail = true;
config.Password.RequireDigit = true;
config.Password.RequireLowercase = true;
config.Password.RequireUppercase = true;
config.Password.RequireNonAlphanumeric = false;
config.Password.RequiredLength = 8;
config.Cookies.ApplicationCookie.LoginPath = "/Company/Login";
config.Cookies.ApplicationCookie.ExpireTimeSpan = new TimeSpan(7, 0, 0, 0); // Cookies last 7 days
})
.AddEntityFrameworkStores<ProgramContext>();
Edit
I verified Identity is the problem. I pulled data from my repository before executing PasswordSignInAsync and it pulled the data just fine. How is the DbContext created for Identity?
Create a DbContext factory
public static class DbContextFactory
{
public static Dictionary<string, string> ConnectionStrings { get; set; }
public static void SetConnectionString(Dictionary<string, string> connStrs)
{
ConnectionStrings = connStrs;
}
public static MyDbContext Create(string connid)
{
if (!string.IsNullOrEmpty(connid))
{
var connStr = ConnectionStrings[connid];
var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
optionsBuilder.UseSqlServer(connStr);
return new MyDbContext(optionsBuilder.Options);
}
else
{
throw new ArgumentNullException("ConnectionId");
}
}
}
Intialize DbContext factory
In startup.cs
public void Configure()
{
Dictionary<string, string> connStrs = new Dictionary<string, string>();
connStrs.Add("DB1", Configuration["Data:DB1Connection:ConnectionString"]);
connStrs.Add("DB2", Configuration["Data:DB2Connection:ConnectionString"]);
DbContextFactory.SetConnectionString(connStrs);
}
Usage
var dbContext= DbContextFactory.Create("DB1");
According to your question, I going to provide a solution assuming some things:
First, I've created three databases in my local SQL Server instance:
create database CompanyFoo
go
create database CompanyBar
go
create database CompanyZaz
go
Then, I going to create one table with one row in each database:
use CompanyFoo
go
drop table ConfigurationValue
go
create table ConfigurationValue
(
Id int not null identity(1, 1),
Name varchar(255) not null,
[Desc] varchar(max) not null
)
go
insert into ConfigurationValue values ('Company name', 'Foo Company')
go
use CompanyBar
go
drop table ConfigurationValue
go
create table ConfigurationValue
(
Id int not null identity(1, 1),
Name varchar(255) not null,
[Desc] varchar(max) not null
)
go
insert into ConfigurationValue values ('Company name', 'Bar Company')
go
use CompanyZaz
go
drop table ConfigurationValue
go
create table ConfigurationValue
(
Id int not null identity(1, 1),
Name varchar(255) not null,
[Desc] varchar(max) not null
)
go
insert into ConfigurationValue values ('Company name', 'Zaz Company')
go
Next step is create an user with SQL Authentication and grant access to read the databases, in my case my user name is johnd and password is 123.
Once we have these steps completed, we proceed to create an MVC application in ASP.NET Core, I used MultipleCompany as project name, I have two controllers: Home and Administration, the goal is to show a login view first and then redirect to another view to show data according to selected database in "login" view.
To accomplish your requirement, you'll need to use session on ASP.NET Core application you can change this way to storage and read data later, for now this is for concept test only.
HomeController code:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using MultipleCompany.Models;
namespace MultipleCompany.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[HttpPost]
public IActionResult Index(LoginModel model)
{
HttpContext.Session.SetString("CompanyCode", model.CompanyCode);
HttpContext.Session.SetString("UserName", model.UserName);
HttpContext.Session.SetString("Password", model.Password);
return RedirectToAction("Index", "Administration");
}
public IActionResult Error()
{
return View();
}
}
}
AdministrationController code:
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using MultipleCompany.Models;
using MultipleCompany.Services;
namespace MultipleCompany.Controllers
{
public class AdministrationController : Controller
{
protected IDbContextService DbContextService;
protected CompanyDbContext DbContext;
public AdministrationController(IDbContextService dbContextService)
{
DbContextService = dbContextService;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
DbContext = DbContextService.CreateCompanyDbContext(HttpContext.Session.CreateLoginModelFromSession());
base.OnActionExecuting(context);
}
public IActionResult Index()
{
var model = DbContext.ConfigurationValue.ToList();
return View(model);
}
}
}
Code for Home view:
#{
ViewData["Title"] = "Home Page";
}
<form action="/home" method="post">
<fieldset>
<legend>Log in</legend>
<div>
<label for="CompanyCode">Company code</label>
<select name="CompanyCode">
<option value="CompanyFoo">Foo</option>
<option value="CompanyBar">Bar</option>
<option value="CompanyZaz">Zaz</option>
</select>
</div>
<div>
<label for="UserName">User name</label>
<input type="text" name="UserName" />
</div>
<div>
<label for="Password">Password</label>
<input type="password" name="Password" />
</div>
<button type="submit">Log in</button>
</fieldset>
</form>
Code for Administration view:
#{
ViewData["Title"] = "Home Page";
}
<h1>Welcome!</h1>
<table class="table">
<tr>
<th>Name</th>
<th>Desc</th>
</tr>
#foreach (var item in Model)
{
<tr>
<td>#item.Name</td>
<td>#item.Desc</td>
</tr>
}
</table>
LoginModel code:
using System;
using Microsoft.AspNetCore.Http;
namespace MultipleCompany.Models
{
public class LoginModel
{
public String CompanyCode { get; set; }
public String UserName { get; set; }
public String Password { get; set; }
}
public static class LoginModelExtensions
{
public static LoginModel CreateLoginModelFromSession(this ISession session)
{
var companyCode = session.GetString("CompanyCode");
var userName = session.GetString("UserName");
var password = session.GetString("Password");
return new LoginModel
{
CompanyCode = companyCode,
UserName = userName,
Password = password
};
}
}
}
CompanyDbContext code:
using System;
using Microsoft.EntityFrameworkCore;
namespace MultipleCompany.Models
{
public class CompanyDbContext : Microsoft.EntityFrameworkCore.DbContext
{
public CompanyDbContext(String connectionString)
{
ConnectionString = connectionString;
}
public String ConnectionString { get; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(ConnectionString);
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
public DbSet<ConfigurationValue> ConfigurationValue { get; set; }
}
}
ConfigurationValue code:
using System;
namespace MultipleCompany.Models
{
public class ConfigurationValue
{
public Int32? Id { get; set; }
public String Name { get; set; }
public String Desc { get; set; }
}
}
AppSettings code:
using System;
namespace MultipleCompany.Models
{
public class AppSettings
{
public String CompanyConnectionString { get; set; }
}
}
IDbContextService code:
using MultipleCompany.Models;
namespace MultipleCompany.Services
{
public interface IDbContextService
{
CompanyDbContext CreateCompanyDbContext(LoginModel model);
}
}
DbContextService code:
using System;
using Microsoft.Extensions.Options;
using MultipleCompany.Models;
namespace MultipleCompany.Services
{
public class DbContextService : IDbContextService
{
public DbContextService(IOptions<AppSettings> appSettings)
{
ConnectionString = appSettings.Value.CompanyConnectionString;
}
public String ConnectionString { get; }
public CompanyDbContext CreateCompanyDbContext(LoginModel model)
{
var connectionString = ConnectionString.Replace("{database}", model.CompanyCode).Replace("{user id}", model.UserName).Replace("{password}", model.Password);
var dbContext = new CompanyDbContext(connectionString);
return dbContext;
}
}
}
Startup code:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MultipleCompany.Models;
using MultipleCompany.Services;
namespace MultipleCompany
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
services.AddEntityFrameworkSqlServer().AddDbContext<CompanyDbContext>();
services.AddScoped<IDbContextService, DbContextService>();
services.AddDistributedMemoryCache();
services.AddSession();
services.AddOptions();
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
services.AddSingleton<IConfiguration>(Configuration);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
I've added this packages for my project:
"Microsoft.EntityFrameworkCore": "1.0.1",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.1",
"Microsoft.AspNetCore.Session": "1.0.0"
My appsettings.json file:
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"AppSettings": {
"CompanyConnectionString": "server=(local);database={database};user id={user id};password={password}"
}
}
Please get focus on the concept about to connect to selected database in home view, you can change any part of this code as an improvement, please remember I'm providing this solution making some assumptions according to your brief question, please feel free to ask about any exposed aspect in this solution to improve this piece of code according to your requirements.
Basically, we need to define a service to create the instance of db context according to selected database, that's IDbContextService interface and DbContextService it's the implementation for that interface.
As you can see on DbContextService code, we replace the values inside of {} to build different connection string, in this case I've added the database names in drop down list but in real development please avoid this way because for security reasons it's better to don't expose the real names of your databases and other configurations; you can have a parity table from controller's side to resolve the company code according to selected database.
One improvement for this solution, it would be to add some code to serialize login model as json into session instead of store each value in separate way.
Please let me know if this answer is useful.
PD: Let me know in comments if you want the full code to upload in one drive
You found your answer but maybe my post can be helpful for someones. I had a similar problem like this question. I have had to change my entity framework connectionstring to connect different database server after user logged in. And for solution first I deleted this function from my context class,
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.
optionsBuilder.UseSqlServer("your connectionstring...");
}
because I couldn't called this function from outside. And I had this auto generated constructor
public ClientContext(DbContextOptions<ClientContext> options)
: base(options)
{
}
After deletion, I added this code to my context class.
public ClientContext CreateConnectionFromOut(string connectionString)
{
var optionsBuilder = new DbContextOptionsBuilder<Esdesk_ClientContext>();
optionsBuilder.UseSqlServer(connectionString);
var context = new ClientContext(optionsBuilder.Options);
return context;
}
Now finally, I can change my connection string from wherever I want. It is just like that,
ClientContext cc = new ClientContext();
var db = cc.CreateConnectionFromOut("your connection string");
Hope this may be fine for someone.
Since you are building a multi-tenant web application, you have to first decide how will you distinguish between tenants. Are you going to use differnent URL? or maybe the same URL but adding a part in the URL?
Assuming that you chose the latter, so tenant 1 would have a URL similar to this: http://localhost:9090/tenant1/orders
Tenant 2 would have a URL like: http://localhost:9090/tenant2/orders
You can do that using URL routing:
routes.MapRoute(
name: "Multitenant",
url: "{tenant}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
As for the connection string, you need a class to decide the connection string based on the URL, and inject this class into the DB context.
public interface ITenantIdentifier
{
string GetCurrentTenantId();
}
public class UrlTenantIdentifier : ITenantIdentifier
{
public string GetCurrentTenantId()
{
//Get the current Http Context and get the URL, you should have a table or configration that maps the URL to the tenant ID and connection string
}
}
In your DB Context:
public class MyDbContext: DbContext
{
public MyDbContext(ITenantIdentifier tenantIdentifier)
{
var connectionStringName = "TenantConnectionString"+tenantIdentifier.GetCurrentTenantId(); //here assuming that you are following a pattern, each tenant has a connection string in the shape of TenantConnectionString+ID
var connectionString = //get connection string
base(connectionString);
}
}
Inspired by (Link to article) this is the way I implemented it
Configuration:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddTransient<ClaimsPrincipal>(provider =>
provider.GetService<IHttpContextAccessor>().HttpContext.User
);
services.AddDbContext<MyDbContext>();
...
}
DbContext:
public class MyDbContext: DbContext
{
readonly ClaimsPrincipal _claimsPrincipal;
public MyDbContext(DbContextOptions options, ClaimsPrincipal claimsPrincipal) : base(options)
{
_claimsPrincipal = claimsPrincipal;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var email = _claimsPrincipal.Claims
.FirstOrDefault(e => e.Type == ClaimTypes.Email).Value;
var dbName = string.Concat(
email.Split(Path.GetInvalidFileNameChars())
);
var connectionString = $"Filename=./{dbName}.sqlite";
optionsBuilder.UseSqlite(connectionString);
base.OnConfiguring(optionsBuilder);
}
}
Update to pass connection string
To pass the dynamically generated connection to your context, create a partial class in the same context as your context-partial class would ensure it stays intact if someone ran the custom tool (for edmx), the auto generated code will be wiped out and regenerated. If you have this code in a partial class it will not be wiped out. For code first, this will not apply. Here is the code:
public class YourContext : DbContext
{
public YourContext(string connString)
{
}
}
The way I have done this in the past is to have one database where the accounts (usernames, passwords) of all the clients are stored. The account the application is running under would be used to communicate with this database to authenticate the client who is logging (CompanyID, Password).
Afterwards, once authenticated, a token is generated. Afterwards, the authenticated user will be interacting with that client's (Company) database. For this part, you can create the connection on the fly as shown here but I will copy and paste it her as well:
// Specify the provider name, server and database.
string providerName = "System.Data.SqlClient";
string serverName = ".";
string databaseName = "AdventureWorks";
// Initialize the connection string builder for the
// underlying provider.
SqlConnectionStringBuilder sqlBuilder =
new SqlConnectionStringBuilder();
// Set the properties for the data source.
sqlBuilder.DataSource = serverName;
sqlBuilder.InitialCatalog = databaseName;
sqlBuilder.IntegratedSecurity = true;
// Build the SqlConnection connection string.
string providerString = sqlBuilder.ToString();
// Initialize the EntityConnectionStringBuilder.
EntityConnectionStringBuilder entityBuilder =
new EntityConnectionStringBuilder();
//Set the provider name.
entityBuilder.Provider = providerName;
// Set the provider-specific connection string.
entityBuilder.ProviderConnectionString = providerString;
// Set the Metadata location.
entityBuilder.Metadata = #"res://*/AdventureWorksModel.csdl|
res://*/AdventureWorksModel.ssdl|
res://*/AdventureWorksModel.msl";
Console.WriteLine(entityBuilder.ToString());
You will need to provide your own csdl, ssdl, and msl names in the above code. If you are using Code First, then your connection string will not need the metadata.
You could try the following while creating your context instance:
// in class DBHelper
public static YourEntities GetDbContext(string tenantName)
{
var connectionStringTemplate =
#"metadata=res://*/yourModel.csdl|res://*/yourModel.ssdl|res://*/yourModel.msl;" +
#"provider=System.Data.SqlClient;" +
#"provider connection string=""data source=.;" +
#"initial catalog={0};" +
#"user id=sa;password=pwd;" +
#"MultipleActiveResultSets=True;App=EntityFramework"";";
var connectionString = string.Format(connection, tenantName);
var db = new YourEntities(connectionString);
return db;
}
Then make a constructor in your context class which accepts string as a parameter and use it as:
var db = DBHelper.GetDbContext(name of database to connect);
It's been a long time since I posted this question, and I never shared the solution I developed, so I figured I should.
I ended up going the route of using different subdomains for my tenants. Because of this, I simply created a TenantService that checked the url and returned a connection string from config. Inside my DbContext's OnConfiguring method, I simply called the tenant service and used the returned connection string. Here is some sample code:
Tenant Service
public class Tenant
{
public string Name { get; set; }
public string Hostname { get; set; }
public string ConnectionString { get; set; }
}
public interface ITenantService
{
Tenant GetCurrentTenant();
List<Tenant> GetTenantList();
}
public class TenantService : ITenantService
{
private readonly ILogger<TenantService> _logger;
private readonly IHttpContextAccessor _httpContext;
private readonly IConfiguration _configuration;
public TenantService(
ILogger<TenantService> logger,
IHttpContextAccessor httpContext,
IConfiguration configuration)
{
_logger = logger;
_httpContext = httpContext;
_configuration = configuration;
}
/// <summary>
/// Gets the current tenant from the host.
/// </summary>
/// <returns>The tenant.</returns>
public Tenant GetCurrentTenant()
{
Tenant tenant;
var host = _httpContext.HttpContext.Request.Host;
var tenants = GetTenantList();
tenant = tenants.SingleOrDefault(t => t.Hostname == host.Value);
if (tenant == null)
{
_logger.LogCritical("Could not find tenant from host: {host}", host);
throw new ArgumentException($"Could not find tenant from host: {host}");
}
return tenant;
}
/// <summary>
/// Gets a list of tenants in configuration.
/// </summary>
/// <returns>The list of tenants.</returns>
public List<Tenant> GetTenantList()
{
var tenants = new List<Tenant>();
_configuration.GetSection("Tenants").Bind(tenants);
return tenants;
}
}
DbContext
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
if (!optionsBuilder.IsConfigured)
{
if (_tenantService == null)
{
throw new ArgumentNullException(nameof(_tenantService));
}
optionsBuilder.UseSqlServer(_tenantService.GetCurrentTenant().ConnectionString);
}
}
I have a multitenant application where I want to keep track of two things across the app: The user and The tenant.
I have this three cases:
Anonymous Users: In this case user is null, and tenant is tracked by querystring.
Authenticated Users: user comes from cookie, tenant is saved in session.
Jobs: I have jobs in my app to do some work, the user in this case is null and the tenant is set manually.
In all my services I use the user and the tenant.
I tought at first of using an object per http session in my DI container, but this wouldn't work with the Jobs.
Any toughts on how can I handle this information, I'm currently using it in a Session variable, but I have many problems with this implementation, and I need to have a lot of special cases to handle jobs and unauthenticated users.
The least you should do is abstracting access to user context. For instance:
public interface IUserContext {
IPrincipal User { get; }
Tenant Tenant { get; }
}
You might want to have a separate ITenantContext abstraction, but let's stick to one for now.
Your system will probably have two applications: windows service that runs the jobs, and a web application that handles user interaction. Both applications have their own entry point, their own Composition Root, and their own unique DI configuration.
For the web application, I imagine the implementation to look as follows:
public AspNetUserContext : IUserContext
{
private readonly ITenantRepository tenantRepository;
public AspNetUserContext(ITenantRepository tenantRepository) {
this.tenantRepository = tenantRepository;
}
public IPrincipal User {
get { return HttpContext.Current.User; }
}
public Tenant Tenant {
get {
if (this.User.Identity.IsAuthenticated) {
return this.tenantRepository.GetByName(
HttpContext.Current.Session["tenant"]);
} else {
return this.tenantRepository.GetByName(
HttpContext.Current.Request.QueryString["tenant"]);
}
}
}
}
For the job service, things might look completely diffetent at first, but with the service, the processing of one job can be considered a request. So that means that when the request start, you need to set the context (something that ASP.NET does for us in the backgroudn). The IUserContext might look as follows:
public JobServiceUserContext : IUserContext
{
[ThreadStatic]
private static IPrinciple user;
[ThreadStatic]
private static Tenant tenant;
public IPrincipal User {
get { return this.user; }
set { this.user = user; }
}
public IPrincipal User {
get { return this.tenant; }
set { this.tenant = tenant; }
}
}
Now the execution of a job can be wrapped with some logic that sets the proper context, for instance:
public class JobRunner {
private readonly JobServiceUserContext context;
private readonly IJobDactory jobFactory;
public JobServiceUserContext(JobServiceUserContext context,
IJobDactory jobFactory) {
this.context = context;
this.jobFactory = jobFactory;
}
public void RunJob(JobDetails job) {
try {
this.context.User = job.User;
this.context.Tenant = job.Tenant;
IJob job = this.jobFactory.Create(job.Type);
job.Execute(job.Data);
Activator.CreateInsta
} finally {
// Reset
this.context.Tenant = null;
this.context.User = null;
}
}
}
UPDATE
In case they are both running within the same application, and the jobs are running on a background thread, you can introduce a proxy implementation for IUserContext that transparently switches to the right implementation. For instance:
public SelectingUserContextProxy : IUserContext {
private readonly Func<bool> selector;
private readonly IUserContext trueContext;
private readonly IUserContext falseContext;
public SelectingUserContextProxy(Func<bool> selector,
IUserContext trueContext, IUserContext falseContext) {
this.selector = selector;
this.trueContext = trueContext;
this.falseContext = falseContext;
}
public IPrincipal User { get { return this.Context.User; } }
public Tenant Tenant { get { return this.Context.Tenant; } }
private IUserContext Context {
get { return selector() ? trueContext : falseContext; }
}
}
And you can register this as follows:
var jobContext = new JobServiceUserContext();
container.RegisterSingle<IUserContext>(
new SelectingUserContextProxy(
() => HttpContext.Current != null,
trueContext: new AspNetUserContext(),
falseContext: jobContext));
I'm trying to avoid the use of the Role Provider and Membership Provider since its way too clumsy in my opinion, and therefore I'm trying to making my own "version" which is less clumsy and more manageable/flexible. Now is my question.. is there an alternative to the Role Provider which is decent? (I know that I can do custom Role provier, membership provider etc.)
By more manageable/flexible I mean that I'm limited to use the Roles static class and not implement directly into my service layer which interact with the database context, instead I'm bound to use the Roles static class which has its own database context etc, also the table names is awful..
Thanks in advance.
I'm in the same boat as you - I've always hated the RoleProviders. Yeah, they're great if you want to get things up and running for a small website, but they're not very realistic. The major downside I've always found is that they tie you directly to ASP.NET.
The way I went for a recent project was defining a couple of interfaces that are part of the service layer (NOTE: I simplified these quite a bit - but you could easily add to them):
public interface IAuthenticationService
{
bool Login(string username, string password);
void Logout(User user);
}
public interface IAuthorizationService
{
bool Authorize(User user, Roles requiredRoles);
}
Then your users could have a Roles enum:
public enum Roles
{
Accounting = 1,
Scheduling = 2,
Prescriptions = 4
// What ever else you need to define here.
// Notice all powers of 2 so we can OR them to combine role permissions.
}
public class User
{
bool IsAdministrator { get; set; }
Roles Permissions { get; set; }
}
For your IAuthenticationService, you could have a base implementation that does standard password checking and then you could have a FormsAuthenticationService that does a little bit more such as setting the cookie etc. For your AuthorizationService, you'd need something like this:
public class AuthorizationService : IAuthorizationService
{
public bool Authorize(User userSession, Roles requiredRoles)
{
if (userSession.IsAdministrator)
{
return true;
}
else
{
// Check if the roles enum has the specific role bit set.
return (requiredRoles & user.Roles) == requiredRoles;
}
}
}
On top of these base services, you could easily add services to reset passwords etc.
Since you're using MVC, you could do authorization at the action level using an ActionFilter:
public class RequirePermissionFilter : IAuthorizationFilter
{
private readonly IAuthorizationService authorizationService;
private readonly Roles permissions;
public RequirePermissionFilter(IAuthorizationService authorizationService, Roles requiredRoles)
{
this.authorizationService = authorizationService;
this.permissions = requiredRoles;
this.isAdministrator = isAdministrator;
}
private IAuthorizationService CreateAuthorizationService(HttpContextBase httpContext)
{
return this.authorizationService ?? new FormsAuthorizationService(httpContext);
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var authSvc = this.CreateAuthorizationService(filterContext.HttpContext);
// Get the current user... you could store in session or the HttpContext if you want too. It would be set inside the FormsAuthenticationService.
var userSession = (User)filterContext.HttpContext.Session["CurrentUser"];
var success = authSvc.Authorize(userSession, this.permissions);
if (success)
{
// Since authorization is performed at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether or not a page should be served from the cache.
var cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0));
cache.AddValidationCallback((HttpContext context, object data, ref HttpValidationStatus validationStatus) =>
{
validationStatus = this.OnCacheAuthorization(new HttpContextWrapper(context));
}, null);
}
else
{
this.HandleUnauthorizedRequest(filterContext);
}
}
private void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// Ajax requests will return status code 500 because we don't want to return the result of the
// redirect to the login page.
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new HttpStatusCodeResult(500);
}
else
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
public HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
{
var authSvc = this.CreateAuthorizationService(httpContext);
var userSession = (User)httpContext.Session["CurrentUser"];
var success = authSvc.Authorize(userSession, this.permissions);
if (success)
{
return HttpValidationStatus.Valid;
}
else
{
return HttpValidationStatus.IgnoreThisRequest;
}
}
}
Which you can then decorate on your controller actions:
[RequirePermission(Roles.Accounting)]
public ViewResult Index()
{
// ...
}
The advantage of this approach is you can also use dependency injection and an IoC container to wire things up. Also, you can use it across multiple applications (not just your ASP.NET one). You would use your ORM to define the appropriate schema.
If you need more details around the FormsAuthorization/Authentication services or where to go from here, let me know.
EDIT: To add "security trimming", you could do it with an HtmlHelper. This probably needs a little more... but you get the idea.
public static bool SecurityTrim<TModel>(this HtmlHelper<TModel> source, Roles requiredRoles)
{
var authorizationService = new FormsAuthorizationService();
var user = (User)HttpContext.Current.Session["CurrentUser"];
return authorizationService.Authorize(user, requiredRoles);
}
And then inside your view (using Razor syntax here):
#if(Html.SecurityTrim(Roles.Accounting))
{
<span>Only for accounting</span>
}
EDIT: The UserSession would look something like this:
public class UserSession
{
public int UserId { get; set; }
public string UserName { get; set; }
public bool IsAdministrator { get; set; }
public Roles GetRoles()
{
// make the call to the database or whatever here.
// or just turn this into a property.
}
}
This way, we don't expose the password hash and all other details inside the session of the current user since they're really not needed for the user's session lifetime.
I have implemented a role provider based on #TheCloudlessSky post here. There are few things that I thought I can add and share what I have done.
First if you want to use the RequirepPermission class for your action filters as an attribute you need to implement ActionFilterAttribute class for RequirepPermission class.
Interface classes IAuthenticationService and IAuthorizationService
public interface IAuthenticationService
{
void SignIn(string userName, bool createPersistentCookie);
void SignOut();
}
public interface IAuthorizationService
{
bool Authorize(UserSession user, string[] requiredRoles);
}
FormsAuthenticationService class
/// <summary>
/// This class is for Form Authentication
/// </summary>
public class FormsAuthenticationService : IAuthenticationService
{
public void SignIn(string userName, bool createPersistentCookie)
{
if (String.IsNullOrEmpty(userName)) throw new ArgumentException(#"Value cannot be null or empty.", "userName");
FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
}
public void SignOut()
{
FormsAuthentication.SignOut();
}
}
UserSession calss
public class UserSession
{
public string UserName { get; set; }
public IEnumerable<string> UserRoles { get; set; }
}
Another point is FormsAuthorizationServiceclass and how we can assign a user to the httpContext.Session["CurrentUser"]. My Approach in this situation is to create a new instance of userSession class and directly assign the user from httpContext.User.Identity.Name to the userSession variable as you can see in FormsAuthorizationService class.
[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)]
public class RequirePermissionAttribute : ActionFilterAttribute, IAuthorizationFilter
{
#region Fields
private readonly IAuthorizationService _authorizationService;
private readonly string[] _permissions;
#endregion
#region Constructors
public RequirePermissionAttribute(string requiredRoles)
{
_permissions = requiredRoles.Trim().Split(',').ToArray();
_authorizationService = null;
}
#endregion
#region Methods
private IAuthorizationService CreateAuthorizationService(HttpContextBase httpContext)
{
return _authorizationService ?? new FormsAuthorizationService(httpContext);
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var authSvc = CreateAuthorizationService(filterContext.HttpContext);
// Get the current user... you could store in session or the HttpContext if you want too. It would be set inside the FormsAuthenticationService.
if (filterContext.HttpContext.Session == null) return;
if (filterContext.HttpContext.Request == null) return;
var success = false;
if (filterContext.HttpContext.Session["__Roles"] != null)
{
var rolesSession = filterContext.HttpContext.Session["__Roles"];
var roles = rolesSession.ToString().Trim().Split(',').ToList();
var userSession = new UserSession
{
UserName = filterContext.HttpContext.User.Identity.Name,
UserRoles = roles
};
success = authSvc.Authorize(userSession, _permissions);
}
if (success)
{
// Since authorization is performed at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether or not a page should be served from the cache.
var cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0));
cache.AddValidationCallback((HttpContext context, object data, ref HttpValidationStatus validationStatus) =>
{
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}, null);
}
else
{
HandleUnauthorizedRequest(filterContext);
}
}
private static void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// Ajax requests will return status code 500 because we don't want to return the result of the
// redirect to the login page.
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new HttpStatusCodeResult(500);
}
else
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
private HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
{
var authSvc = CreateAuthorizationService(httpContext);
if (httpContext.Session != null)
{
var success = false;
if (httpContext.Session["__Roles"] != null)
{
var rolesSession = httpContext.Session["__Roles"];
var roles = rolesSession.ToString().Trim().Split(',').ToList();
var userSession = new UserSession
{
UserName = httpContext.User.Identity.Name,
UserRoles = roles
};
success = authSvc.Authorize(userSession, _permissions);
}
return success ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest;
}
return 0;
}
#endregion
}
internal class FormsAuthorizationService : IAuthorizationService
{
private readonly HttpContextBase _httpContext;
public FormsAuthorizationService(HttpContextBase httpContext)
{
_httpContext = httpContext;
}
public bool Authorize(UserSession userSession, string[] requiredRoles)
{
return userSession.UserRoles.Any(role => requiredRoles.Any(item => item == role));
}
}
then in your controller after the user is authenticated you can get roles from the database and assign it to the roles session:
var roles = Repository.GetRolesByUserId(Id);
if (ControllerContext.HttpContext.Session != null)
ControllerContext.HttpContext.Session.Add("__Roles",roles);
FormsService.SignIn(collection.Name, true);
After the user is logged out of the system you can clear the session
FormsService.SignOut();
Session.Abandon();
return RedirectToAction("Index", "Account");
The caveat in this model is that, when the user is signed into the system, if a role is assigned to the user, authorization doesn't work unless he logs out and logs back in the system.
Another thing is that there is no need to have a separate class for roles, since we can get roles directly from database and set it into roles session in a controller.
After you are done with implementing all these codes one last step is to bind this attribute to your methods in your controller:
[RequirePermission("Admin,DM")]
public ActionResult Create()
{
return View();
}
If you use Castle Windsor Dependency Injection you can inject lists of RoleProviders that can be used to ascertain user rights from any source you choose to implement.
http://ivida.co.uk/2011/05/18/mvc-getting-user-roles-from-multiple-sources-register-and-resolve-arrays-of-dependencis-using-the-fluent-api/
You don't need to use a static class for roles. For instance, the SqlRoleProvider allows you to define the roles in a database.
Of course, if you want to retrieve roles from your own service layer, it's not that hard to create your own role provider - there really aren't that many methods to implement.
You can implement your own membership and role providers by overriding the appropriate interfaces.
If you want to start from scratch, typically these types of things are implemented as a custom http module which stores the users credentials either in the httpcontext or the session. Either way you'll probably want to set a cookie with some sort of authentication token.