在本教程中咱們將把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; } } //... } }