在本教程中咱们将把Norhwind变成一个多租户应用程序。javascript
这是一个维基百科的多租户软件定义:java
软件多租户是指一个软件架构的一个实例软件运行在一个服务器和多个租户。租户是一组共享一个公共访问的用户与特定权限的软件实例。多租户架构,软件应用程序旨在提供每一个租户专用的实例包括数据、配置、用户管理、租户个体功能和非功能属性。多租户与多实例架构,独立的软件实例操做表明不一样的租户。——维基百科git
咱们将TenantId字段添加到每一个表,包括用户,让用户看只到和修改属于她的租户的记录。因此,租户将孤立地工做,就像他们有本身的独立数据库。github
多租户应用程序有一些优点,好比减小管理成本。web
可是他们也有一些缺点。例如,全部租户数据都在一个单一的数据库,一个租户不能简单地采起单独或备份本身的数据。性能问题是常见的,由于要处理有更多的记录。数据库
云应用程序增长的趋势,虚拟化、成本下降和迁移等功能,它如今更容易设置多实例应用。编程
我我的避免多租户应用程序。在我看来每客户最好有一个数据库。浏览器
但一些用户问到如何实现此功能。本教程将帮助咱们解释一些高级的的Serenity主题做为奖励,以及多租户。缓存
你能够在下面找到本教程得源代码安全
https://github.com/volkanceylan/Serenity-Tutorials/tree/master/MultiTenancy
在Visual Studio中单击文件- >新项目。确保你选择Serene 模板,输入MultiTenancy 并单击OK。
在解决方案资源管理器,您应该会看到两个项目 MultiTenancy.Web 和de MultiTenancy.Script.he
确保MultiTenancy.Web 是启动项目(被加粗的),若是不是,右键单击项目名称,而后单击设置为启动项目。
默认状况下,Visual Studio仅仅构建MultiTenancy.Web ,当你按F5运行Web项目。
这是由设置在Visual Studio和解决方案- >选项- >项目构建和运行- >“只构建启动项目和依赖运行”。不建议去改变它。
让脚本项目也创建运行Web项目时,右击 MultiTenancy.Web 项目,单击Build - >项目依赖项并检查多租户的依赖性。脚本依赖选项卡下。
不幸的是没有办法在Serene的模板设置这个依赖。
咱们须要给全部表添加一个TenantId字段,来给彼此孤立租户。
所以,咱们首先须要一个租户表。
Northwind表已经有记录了,咱们将定义一个主要租户ID为1,并将全部现有记录TenantId设置到它。
是时候写一个迁移了,实际上有两个迁移,一个用于Northwind ,一个用于Default数据库。
DefaultDB_20160110_092200_MultiTenant.cs:
using FluentMigrator; namespace MultiTenancy.Migrations.DefaultDB { [Migration(20160110092200)] public class DefaultDB_20160110_092200_MultiTenant : AutoReversingMigration { public override void Up() { Create.Table("Tenants") .WithColumn("TenantId").AsInt32() .Identity().PrimaryKey().NotNullable() .WithColumn("TenantName").AsString(100) .NotNullable(); Insert.IntoTable("Tenants") .Row(new { TenantName = "Primary Tenant" }); Insert.IntoTable("Tenants") .Row(new { TenantName = "Second Tenant" }); Insert.IntoTable("Tenants") .Row(new { TenantName = "Third Tenant" }); Alter.Table("Users") .AddColumn("TenantId").AsInt32() .NotNullable().WithDefaultValue(1); Alter.Table("Roles") .AddColumn("TenantId").AsInt32() .NotNullable().WithDefaultValue(1); Alter.Table("Languages") .AddColumn("TenantId").AsInt32() .NotNullable().WithDefaultValue(1); } } }
我已经在用户表的默认数据库建立了租户表。这里咱们加3个预约义的租户。实际上咱们只须要第一个ID为1。
咱们不像UserPermissions UserRoles,RolePermissions等添加TenantId列表,由于它们都经过他们的用户id或RoleId 带着TenantId信息。
NorthwindDB_20160110_093500_MultiTenant.cs:
using FluentMigrator; namespace MultiTenancy.Migrations.NorthwindDB { [Migration(20160110093500)] public class NorthwindDB_20160110_093500_MultiTenant : AutoReversingMigration { public override void Up() { Alter.Table("Employees") .AddColumn("TenantId").AsInt32() .NotNullable().WithDefaultValue(1); Alter.Table("Categories") .AddColumn("TenantId").AsInt32() .NotNullable().WithDefaultValue(1); Alter.Table("Customers") .AddColumn("TenantId").AsInt32() .NotNullable().WithDefaultValue(1); Alter.Table("Shippers") .AddColumn("TenantId").AsInt32() .NotNullable().WithDefaultValue(1); Alter.Table("Suppliers") .AddColumn("TenantId").AsInt32() .NotNullable().WithDefaultValue(1); Alter.Table("Orders") .AddColumn("TenantId").AsInt32() .NotNullable().WithDefaultValue(1); Alter.Table("Products") .AddColumn("TenantId").AsInt32() .NotNullable().WithDefaultValue(1); Alter.Table("Region") .AddColumn("TenantId").AsInt32() .NotNullable().WithDefaultValue(1); Alter.Table("Territories") .AddColumn("TenantId").AsInt32() .NotNullable().WithDefaultValue(1); } } }
启动Sergen 而且给 Tenants 表生成代码在 Default 链接上:
接下来,咱们将定义一个查找脚本TenantRow和设置实例名称属性到租户:
namespace MultiTenancy.Administration.Entities { //... [ConnectionKey("Default"), DisplayName("Tenants"), InstanceName("Tenant"), TwoLevelCached] [LookupScript("Administration.Tenant")] public sealed class TenantRow : Row, IIdRow, INameRow { [DisplayName("Tenant Id"), Identity] public Int32? TenantId { get { return Fields.TenantId[this]; } set { Fields.TenantId[this] = value; } } //...
让咱们定义一个管理:租户权限,只有admin用户有
namespace MultiTenancy.Administration { public class PermissionKeys { public const string Security = "Administration:Security"; public const string Translation = "Administration:Translation"; public const string Tenants = "Administration:Tenants"; } }
在TenantRow上设置:
[ConnectionKey("Default"), DisplayName("Tenants"), InstanceName("Tenant"), TwoLevelCached] [ReadPermission(PermissionKeys.Tenants)] [ModifyPermission(PermissionKeys.Tenants)] [LookupScript("Administration.Tenant")] public sealed class TenantRow : Row, IIdRow, INameRow {
咱们添加一个TenantId字段到 Users 表, 可是它没有在UserRow定义, 因此它在user 弹出框中是不可见的。
这个字段,只能被admin用户编辑何看到。其余用户,即便咱们给他们访问页面来管理租户用户,用户不该该可以看到或更改这些信息。
让咱们首先添加到UserRow.cs:
namespace MultiTenancy.Administration.Entities { //... public sealed class UserRow : LoggingRow, IIdRow, INameRow { //... [DisplayName("Last Directory Update"), Insertable(false), Updatable(false)] public DateTime? LastDirectoryUpdate { get { return Fields.LastDirectoryUpdate[this]; } set { Fields.LastDirectoryUpdate[this] = value; } } [DisplayName("Tenant"), ForeignKey("Tenants", "TenantId"), LeftJoin("tnt")] [LookupEditor(typeof(TenantRow))] public Int32? TenantId { get { return Fields.TenantId[this]; } set { Fields.TenantId[this] = value; } } [DisplayName("Tenant"), Expression("tnt.TenantName")] public String TenantName { get { return Fields.TenantName[this]; } set { Fields.TenantName[this] = value; } } //... public class RowFields : LoggingRowFields { //... public readonly DateTimeField LastDirectoryUpdate; public readonly Int32Field TenantId; public readonly StringField TenantName; //... } } }
要编辑它,咱们须要将它添加到UserForm.cs:
namespace MultiTenancy.Administration.Forms { using Serenity; using Serenity.ComponentModel; using System; using System.ComponentModel; [FormScript("Administration.User")] [BasedOnRow(typeof(Entities.UserRow))] public class UserForm { public String Username { get; set; } public String DisplayName { get; set; } [EmailEditor] public String Email { get; set; } [PasswordEditor] public String Password { get; set; } [PasswordEditor, OneWay] public String PasswordConfirm { get; set; } [OneWay] public string Source { get; set; } public Int32? TenantId { get; set; } } }
还须要在site.administration.less增长用户对话框的大小来容下租户选择框。
.s-UserDialog { > .size { .widthAndMin(650px); } .dialog-styles(@h: auto, @l: 150px, @e: 400px); .categories { height: 300px; } }
如今打开用户管理页面,建立一个用户tenant2属于第二个租户。
建立这个用户后,编辑其权限,授予他用户,角色管理和权限许可,由于这将是咱们第二个租户的管理用户。
以Tenant2登录
以用户tenant2 登出和登陆。
当你打开用户管理页面,您将看到该用户能够看到和编辑管理用户,除了本身的tenant2用户。他甚至能够在用户对话框查看和编辑租户。
这不是咱们想要的。
让咱们阻止他看到其余租户的用户。
咱们首先须要在UserDefinition加载和缓存用户租户信息。
打开在Multitenancy.Web/ Modules/ Administration/ User/ Authentication下的 UserDefinition.cs 添加一个TenantId 属性.
namespace MultiTenancy.Administration { using Serenity; using System; [Serializable] public class UserDefinition : IUserDefinition { public string Id { get { return UserId.ToInvariant(); } } public string DisplayName { get; set; } public string Email { get; set; } public short IsActive { get; set; } public int UserId { get; set; } public string Username { get; set; } public string PasswordHash { get; set; } public string PasswordSalt { get; set; } public string Source { get; set; } public DateTime? UpdateDate { get; set; } public DateTime? LastDirectoryUpdate { get; set; } public int TenantId { get; set; } } }
当你经过Authorization.UserDefinition请求当前用户,这是当你返回的类。
咱们还须要修改代码加载这个类。在同一个文件夹中,编辑UserRetrieveService.cs和改变列表以下:
private UserDefinition GetFirst(IDbConnection connection, BaseCriteria criteria) { var user = connection.TrySingle<Entities.UserRow>(criteria); if (user != null) return new UserDefinition { UserId = user.UserId.Value, Username = user.Username, Email = user.Email, DisplayName = user.DisplayName, IsActive = user.IsActive.Value, Source = user.Source, PasswordHash = user.PasswordHash, PasswordSalt = user.PasswordSalt, UpdateDate = user.UpdateDate, LastDirectoryUpdate = user.LastDirectoryUpdate, TenantId = user.TenantId.Value }; return null; }
如今,是时候来经过TenantId过滤用户列表,打开UserRepository.cs,定位MyListHandler类修改:
private class MyListHandler : ListRequestHandler<MyRow> { protected override void ApplyFilters(SqlQuery query) { base.ApplyFilters(query); var user = (UserDefinition)Authorization.UserDefinition; if (!Authorization.HasPermission(PermissionKeys.Tenants)) query.Where(fld.TenantId == user.TenantId); } }
在这里,咱们首先得到当前登陆用户的用户定义的缓存。
咱们检查他是否有租户管理权限,只有管理员才会有。若是没有,咱们经过TenantId筛选记录。
重建后,启动,如今用户页面将是这样的:
是的,他看不到admin用户了,可是有些错误。当您单击tenant2时,什么都不会发生,你会获得一个错误“没法加载脚本数据:Lookup.Administration.Tenant”:
这个错误与咱们最近在仓储层过滤得过滤没有关系。它不能加载这个查找脚本,由于当前用户没有权限租户表。可是他是怎么最后一次看到它?
他能看到它,由于咱们第一次登陆了管理员和用户,当咱们打开编辑对话框加载这个查找脚本。浏览器缓存了它,因此当咱们登陆tenant2和打开编辑对话框,它从浏览器缓存中加载租户。
但这一次,咱们重建项目中,浏览器试图从服务器加载它,咱们获得了这个错误,tenant2没有这个权限。不要紧,咱们不但愿他有这个权限,但如何避免这个错误呢?
咱们须要从用户表单移除租户字段。可是咱们须要该字段为管理员用户,因此咱们从UserForm.cs不能简单地删除它。所以,咱们须要有条件地这样作。
变换T4全部文件,而后打开UserDialog.cs和重写GetPropertyItems方法以下:
namespace MultiTenancy.Administration { using jQueryApi; using Serenity; using System.Collections.Generic; using System.Linq; //... public class UserDialog : EntityDialog<UserRow> { //... protected override List<PropertyItem> GetPropertyItems() { var items = base.GetPropertyItems(); if (!Authorization.HasPermission("Administration:Tenants")) items = items.Where(x => x.Name != UserRow.Fields.TenantId).ToList(); return items; } } }
GetPropertyItems方法,对话框的表单字段的列表,从服务器端表单定义。这些字段从咱们定义服务器端UserForm读取。
若是用户没有租户管理权限,咱们从客户端表单定义删除TenantId字段。
这并不改变实际的形式定义,只是删除这个对话框的TenantId字段实例。
如今能够本身编辑tenant2用户。
一些用户报告,也要为admin用户删除租户选择。确保你HasPermission方法在MultiTenancy.Script 项目下的Authorization.cs 里面就像下图:
public static bool HasPermission(string permissionKey) { return UserDefinition.Username == "admin" || UserDefinition.Permissions[permissionKey]; }
当你以tenant2登陆用户而且打开他得编辑表单,租户选择下拉不显示,因此他没法改变他的租户对吧?
错!
若是他是一个普通的用户,他不能。可是若是他有一些Serenity 及其服务如何工做的知识,他能够。
当你使用网络,你要认真得多地对待安全。
在web应用程序中很容易建立安全漏洞,除非你在客户端和服务器端处理验证。
让咱们展现它。打开浏览器控制台,以用户tenant2登陆。
复制这个并粘贴到控制台:
Q.serviceCall({ service: 'Administration/User/Update', request: { EntityId: 2, Entity: { UserId: 2, TenantId: 1 } } });
如今刷新用户管理页面,您将看到tenant2如今能够看到admin用户!
咱们称为用户更新服务使用javascript,改变tenant2用户TenaNntId 1(主要租户)。
首先让咱们恢复它回到第二个租户(2),而后咱们会修复这个安全漏洞:
Q.serviceCall({ service: 'Administration/User/Update', request: { EntityId: 2, Entity: { UserId: 2, TenantId: 2 } } });
打开UserRepository.cs, 定位到MySaveHandler 类像下面这样修改GetEditableFields方法:
protected override void GetEditableFields(HashSet<Field> editable) { base.GetEditableFields(editable); if (!Authorization.HasPermission(Administration.PermissionKeys.Security)) { editable.Remove(fld.Source); editable.Remove(fld.IsActive); } if (!Authorization.HasPermission(Administration.PermissionKeys.Tenants)) { editable.Remove(fld.TenantId); } }
构建您的项目,而后尝试再次输入到控制台:
Q.serviceCall({ service: 'Administration/User/Update', request: { EntityId: 2, Entity: { UserId: 2, TenantId: 1 } } });
你将获得这个错误:
Tenant field is read only!
SaveRequestHandler调用GetEditableField 方法来肯定那些可更新的用户哪些字段是可编辑的。默认状况下,这些字段是由Updatable 和 Insertable 的行特性定义的。
除非另有规定,全部字段是可插入的和可更新的。
若是用户没有租户管理权限,咱们从auto-determined可编辑字段列表删除TenantId 。
以Tenant2登陆时,试着建立一个新用户,User2。
你不会获得任何错误,而是惊喜,你不会看到新建立的用户列表。User2怎么了?
As we set default value for TenantId to 1 in migrations, now User2 has 1 as TenantId and is a member of Primary Tenant.
当咱们在迁移中TenantId设置默认值为1,如今User2有一个 1做为 TenantId和属于主要的租户。
咱们必须设置新用户TenantId与登陆用户的值相同。
修改UserRepository的SetInternalFields方法,像下面这样:
protected override void SetInternalFields() { base.SetInternalFields(); if (IsCreate) { Row.Source = "site"; Row.IsActive = Row.IsActive ?? 1; if (!Authorization.HasPermission(Administration.PermissionKeys.Tenants) || Row.TenantId == null) { Row.TenantId = ((UserDefinition)Authorization.UserDefinition) .TenantId; } } if (IsCreate || !Row.Password.IsEmptyOrNull()) { string salt = null; Row.PasswordHash = GenerateHash(password, ref salt); Row.PasswordSalt = salt; } }
在这里,咱们与当前用户TenantId设置为相同的值,除非他有租户管理权限。
如今尝试建立一个新的用户User2b,这一次你会看到他在名单上。
记住用户tenant2能够更新他的TenantId服务调用,并且咱们必须确保服务器端。
相似的,即便他在默认状况下从其余租户看不到用户,他能够检索和更新他们。
再次攻击的时间。
打开浏览器控制台输入:
new MultiTenancy.Administration.UserDialog().loadByIdAndOpenDialog(1)
他能够打开admin用户对话框和更新!
当你点击一个username在用户administration 页面,MultiTenancy.Administration.UserDialog是这个对话框类。
咱们建立了一个新实例,要求加载用户实体的ID。管理员用户ID为1。
加载ID为1的实体,对话框调用UserRepository的检索服务。
记住咱们在UserRepository列表过滤方法,不是检索。服务不知道,从另外一个租户,若是它应该返回记录。
是时候在UserRepository安全检索服务:
private class MyRetrieveHandler : RetrieveRequestHandler<MyRow> { protected override void PrepareQuery(SqlQuery query) { base.PrepareQuery(query); var user = (UserDefinition)Authorization.UserDefinition; if (!Authorization.HasPermission(PermissionKeys.Tenants)) query.Where(fld.TenantId == user.TenantId); } }
咱们之前在MyListHandler作了一样的改变。
若是你如今尝试相同的Javascript代码,你会获得一个错误:
Record not found. It might be deleted or you don't have required permissions!
But, we could still update record calling Update
service manually. So, need to secure MySaveHandler too.
可是,咱们仍然能够调用更新服务手动更新记录。所以,也须要确保MySaveHandler。
这样改变其ValidateRequest方法:
protected override void ValidateRequest() { base.ValidateRequest(); if (IsUpdate) { var user = (UserDefinition)Authorization.UserDefinition; if (Old.TenantId != user.TenantId) Authorization.ValidatePermission(PermissionKeys.Tenants); // ...
咱们检查是否更新,若是TenantId记录被更新(Old.TenantId)是不一样于当前登陆用户的TenantId。若是是这样,咱们调用 Authorization.ValidatePermission方法来确保用户有租户管理的权限。若是没有,它会报错。
Authorization has been denied for this request!
UserRepository有删除和恢复处理程序,他们遭受相似的安全漏洞。
使用相似的方法,咱们须要确保他们:
private class MyDeleteHandler : DeleteRequestHandler<MyRow> { protected override void ValidateRequest() { base.ValidateRequest(); var user = (UserDefinition)Authorization.UserDefinition; if (Row.TenantId != user.TenantId) Authorization.ValidatePermission(PermissionKeys.Tenants); } } private class MyUndeleteHandler : UndeleteRequestHandler<MyRow> { protected override void ValidateRequest() { base.ValidateRequest(); var user = (UserDefinition)Authorization.UserDefinition; if (Row.TenantId != user.TenantId) Authorization.ValidatePermission(PermissionKeys.Tenants); } }
隐藏租户管理权限
咱们如今有一个小问题。用户tenant2有权限Administration:Security ,因此他能够访问用户和角色权限对话框。所以,能够本身在权限UI上给本身受权Administration:Tenants
Serenity 扫描你的程序集属性像ReadPermission WritePermission,PageAuthorize,ServiceAuthorize等等,在编辑权限对话框中列出这些权限。
咱们应该先从预填充的列表中删除它。
找到方法, ListPermissionKeys 在UserPermissionRepository.cs中:
如今,这个权限不会列在编辑用户权限或编辑角色权限对话框。
可是,尽管如此,他本身能够授予此权限,经过UserPermissionRepository.Update or RolePermissionRepository.Update或者其余得方法黑入。
咱们应该添加一些检查来防止这种状况:
咱们检查是否有新的权限试图得到key,不列入许可对话框。
若是是这样,这多是黑客尝试。
实际上这个检查应该是默认的,即便没有多租户系统,可是一般咱们信任管理用户。这里,管理员将只管理本身的租户,因此咱们确定须要这个检查。
3.2.10把角色变成多租户
到目前为止,咱们已经把用户页面工做在多租户的风格上。彷佛咱们作太多的改变使其工做。但请记住,咱们正在努力把一个系统变成,而不是被一开始设计成多租户成这样。
咱们将类似的原理应用到角色表。
再一次,一个用户不该该看到或修改其余工做区隔离的角色。
We start by adding TenantId property to RoleRow.cs:咱们开始经过添加TenantId属性到 RoleRow.cs:
而后咱们会在RoleRepository.cs作几个变化:使用Serenity 的服务行为
若是想在Northwind扩展这个多租户系统到其余表,咱们会对角色自行重复相同的步骤。虽然它看起来不那么难,太多的手工工做。Serenity 提供服务的行为系统,你能够拦截建立、更新、检索、列表,删除处理程序和添加自定义代码。
在这些处理程序的一些操做,好比捕获日志,惟一约束验证等已实现为服务行为。行为可能对全部行被激活,或基于一些规则,若有特定属性或接口。
例如,CaptureLogBehavior激活行[CaptureLog]特性。
咱们将首先定义一个会触发咱们的新行为接口IMultiTenantRow。这个类在文件IMultiTenantRow.cs,TenantRow.cs旁边:
而后添加这个behavior在MultiTenantBehavior.cs,旁边
行为类IImplicitBehavior接口决定是否应该为特定的行类型被激活。他们这样作经过实现ActivateFor方法,它被请求处理程序调用。在这种方法中,咱们检查是否行实现IMultiTenantRow接口类型。若是不是它只是返回false。
而后咱们获得一个私有引用到TenantIdField,之后来在后面其余方法重用。
每一个处理器类型和行,ActivateFor只调用一次。若是这个方法返回true,因为性能的缘由行为实例被缓存,和重用任何请求为这一行和处理类型。
所以,全部你写在其余方法必须是线程安全的,由于一个实例被共享到全部请求。
行为,可能会拦截一个或多个检索、列表,保存、删除处理程序。它经过实现IRetrieveBehavior,IListBehavior ISaveBehavior或IDeleteBehavior接口。
在这里,咱们须要拦截全部这些服务调用,因此咱们实现全部接口。
咱们只填写感兴趣的方法,其余的留空。
咱们这里实现的方法,对应于咱们覆盖RoleRepository.cs方法。
在前一节。它们包含的代码几乎是相同的,只是在这里咱们须要更通用的,由于这种行为将为任何行类型工做实现IMultiTenantRow。
Reimplementing RoleRepository With Using the Behavior
如今咱们恢复在RoleRepository.cs里面的更改:
And add IMultiTenantRow interface to RoleRow:
你应该用更少的代码获得相同的结果。声明性编程几乎老是更好的。
public ListResponse<string> ListPermissionKeys() { return LocalCache.Get("Administration:PermissionKeys", TimeSpan.Zero, () => { //... result.Remove(Administration.PermissionKeys.Tenants); result.Remove("*"); result.Remove("?"); //...public class UserPermissionRepository { public SaveResponse Update(IUnitOfWork uow, UserPermissionUpdateRequest request) { //...var newList = new Dictionary<string, bool>( StringComparer.OrdinalIgnoreCase); foreach (var p in request.Permissions) newList[p.PermissionKey] = p.Grant ?? false; var allowedKeys = ListPermissionKeys() .Entities.ToDictionary(x => x); if (newList.Keys.Any(x => !allowedKeys.ContainsKey(x))) throw new AccessViolationException(); //...public class RolePermissionRepository { public SaveResponse Update(IUnitOfWork uow, RolePermissionUpdateRequest request) { //...var newList = new HashSet<string>( request.Permissions.ToList(), StringComparer.OrdinalIgnoreCase); var allowedKeys = new UserPermissionRepository() .ListPermissionKeys() .Entities.ToDictionary(x => x); if (newList.Any(x => !allowedKeys.ContainsKey(x))) throw new AccessViolationException(); //...namespace MultiTenancy.Administration.Entities { //... public sealed class RoleRow : Row, IIdRow, INameRow { [Insertable(false), Updatable(false)] public Int32? TenantId { get { return Fields.TenantId[this]; } set { Fields.TenantId[this] = value; } } //... public class RowFields : RowFieldsBase { //... public readonly Int32Field TenantId; //... } } }private class MySaveHandler : SaveRequestHandler<MyRow> { protected override void SetInternalFields() { base.SetInternalFields(); if (IsCreate) Row.TenantId = ((UserDefinition)Authorization.UserDefinition).TenantId; } } private class MyDeleteHandler : DeleteRequestHandler<MyRow> { protected override void ValidateRequest() { base.ValidateRequest(); var user = (UserDefinition)Authorization.UserDefinition; if (Row.TenantId != user.TenantId) Authorization.ValidatePermission(PermissionKeys.Tenants); } } private class MyRetrieveHandler : RetrieveRequestHandler<MyRow> { protected override void PrepareQuery(SqlQuery query) { base.PrepareQuery(query); var user = (UserDefinition)Authorization.UserDefinition; if (!Authorization.HasPermission(PermissionKeys.Tenants)) query.Where(fld.TenantId == user.TenantId); } } private class MyListHandler : ListRequestHandler<MyRow> { protected override void ApplyFilters(SqlQuery query) { base.ApplyFilters(query); var user = (UserDefinition)Authorization.UserDefinition; if (!Authorization.HasPermission(PermissionKeys.Tenants)) query.Where(fld.TenantId == user.TenantId); } }using Serenity.Data; namespace MultiTenancy { public interface IMultiTenantRow { Int32Field TenantIdField { get; } } }using MultiTenancy.Administration; using Serenity; using Serenity.Data; using Serenity.Services; namespace MultiTenancy { public class MultiTenantBehavior : IImplicitBehavior, ISaveBehavior, IDeleteBehavior, IListBehavior, IRetrieveBehavior { private Int32Field fldTenantId; public bool ActivateFor(Row row) { var mt = row as IMultiTenantRow; if (mt == null) return false; fldTenantId = mt.TenantIdField; return true; } public void OnPrepareQuery(IRetrieveRequestHandler handler, SqlQuery query) { var user = (UserDefinition)Authorization.UserDefinition; if (!Authorization.HasPermission(PermissionKeys.Tenants)) query.Where(fldTenantId == user.TenantId); } public void OnPrepareQuery(IListRequestHandler handler, SqlQuery query) { var user = (UserDefinition)Authorization.UserDefinition; if (!Authorization.HasPermission(PermissionKeys.Tenants)) query.Where(fldTenantId == user.TenantId); } public void OnSetInternalFields(ISaveRequestHandler handler) { if (handler.IsCreate) fldTenantId[handler.Row] = ((UserDefinition)Authorization .UserDefinition).TenantId; } public void OnValidateRequest(IDeleteRequestHandler handler) { var user = (UserDefinition)Authorization.UserDefinition; if (fldTenantId[handler.Row] != user.TenantId) Authorization.ValidatePermission( PermissionKeys.Tenants); } public void OnAfterDelete(IDeleteRequestHandler handler) { } public void OnAfterExecuteQuery(IRetrieveRequestHandler handler) { } public void OnAfterExecuteQuery(IListRequestHandler handler) { } public void OnAfterSave(ISaveRequestHandler handler) { } public void OnApplyFilters(IListRequestHandler handler, SqlQuery query) { } public void OnAudit(IDeleteRequestHandler handler) { } public void OnAudit(ISaveRequestHandler handler) { } public void OnBeforeDelete(IDeleteRequestHandler handler) { } public void OnBeforeExecuteQuery(IRetrieveRequestHandler handler) { } public void OnBeforeExecuteQuery(IListRequestHandler handler) { } public void OnBeforeSave(ISaveRequestHandler handler) { } public void OnPrepareQuery(IDeleteRequestHandler handler, SqlQuery query) { } public void OnPrepareQuery(ISaveRequestHandler handler, SqlQuery query) { } public void OnReturn(IDeleteRequestHandler handler) { } public void OnReturn(IRetrieveRequestHandler handler) { } public void OnReturn(IListRequestHandler handler) { } public void OnReturn(ISaveRequestHandler handler) { } public void OnValidateRequest(IRetrieveRequestHandler handler) { } public void OnValidateRequest(IListRequestHandler handler) { } public void OnValidateRequest(ISaveRequestHandler handler) { } } }private class MySaveHandler : SaveRequestHandler<MyRow> { } private class MyDeleteHandler : DeleteRequestHandler<MyRow> { } private class MyRetrieveHandler : RetrieveRequestHandler<MyRow> { } private class MyListHandler : ListRequestHandler<MyRow> { }namespace MultiTenancy.Administration.Entities { //... public sealed class RoleRow : Row, IIdRow, INameRow, IMultiTenantRow { //... public Int32Field TenantIdField { get { return Fields.TenantId; } } //... } }