Agrege tabla de tenancy usando Fluent Migrator e incluye la columna “TenantId” en las tablas que ocupen ser filtradas por tenancy
using FluentMigrator;
namespace MultiTenancy.Migrations.DefaultDB
{
[Migration(20170203000002)]
public class DefaultDb_20170203_000002_MultiTenancy
: 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 = "Admin Tenant" //Your first tenant, the Admin
});
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);
//Your Tables, Repeat for each table needing tenancy
Alter.Table("Employees")
.AddColumn("TenantId").AsInt32()
.NotNullable().WithDefaultValue(1);
}
}
}
Nota: Las tablas UserPermissions, UserRoles, RolePermissions etc, ya tienen el comportamiento de tenancy usando UserID o RoleId asi que no ocupan el TenantId.
Usa Sergen para generar los archivos neccesarios igual a cualqier otra tabla.
Agrega un LookupScript a la tabla TenantRow en TenantRow.cs, Junto con la clave de permisso solo para el administrardor
En AdministrationPermissionKeys.cs
namespace MultiTenancy.Administration
{
public class PermissionKeys
{
public const string Security = "Administration:Security";
public const string Translation = "Administration:Translation";
public const string Tenants = "Administration:Tenants";
}
}
En Tenantrow.cs
namespace MultiTenancy.Administration.Entities
{
//...
[ConnectionKey("Default"), DisplayName("Tenants"),
InstanceName("Tenant"), TwoLevelCached]
[ReadPermission(PermissionKeys.Tenants)]
[ModifyPermission(PermissionKeys.Tenants)]
[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; }
}
//...
Agrega TenantId a UserRow.cs
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;
//...
Tambien agreagalas a el Formulario
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; }
}
}
En la carpeta Administration>User>Authenctication abre UserDefintion.cs y agrega
{
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; }
}
}
En UserRetrieveService.cs bajo ‘GetFirst’ haz el siguiente cambio
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;
}
En UserRepository.cs busca la classe MyListHandler y modificala
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);
}
}
En el mismo archive busca MySaveHandler y modifica el ‘GetEditableFields’
if (!Authorization.HasPermission(Administration.PermissionKeys.Security))
{
editable.Remove(fld.Source);
editable.Remove(fld.IsActive);
}
if (!Authorization.HasPermission(Administration.PermissionKeys.Tenants))
{
editable.Remove(fld.TenantId);
}
Igualmente en el mismo archive busca ‘SetInternalFields’ y modifica:
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;
}
}
Igualmente en el mismo archivo busca ‘MyRetrieveHandler’ y modifica:
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);
}
}
Igualmente en el mismo archivo busca ‘ValidateRequest’ bajo MySaveHandler y modifica:
protected override void ValidateRequest()
{
base.ValidateRequest();
if (IsUpdate)
{
var user = (UserDefinition)Authorization.UserDefinition;
if (Old.TenantId != user.TenantId)
Authorization.ValidatePermission(PermissionKeys.Tenants);
//…
Ahora busca MyDeleteHandler y MyUndeleteHandler y modifica
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);
}
}
En UserDialog.ts agrega:
protected getPropertyItems() {
let items = super.getPropertyItems();
if (!Authorization.hasPermission("Administration:Tenants"))
items = items.filter(x => x.name != UserRow.Fields.TenantId);
return items;
}
Asegurate que Authorization.ts tiene:
export function hasPermission(permissionKey: string) {
let ud = userDefinition;
return ud.Username === 'admin' || !!ud.Permissions[permissionKey];
En UserPermissionRepository.cs busca por “ListPermissionKeys” y modifica
public ListResponse<string> ListPermissionKeys()
{
return LocalCache.Get("Administration:PermissionKeys", TimeSpan.Zero, () =>
{
//...
result.Remove(Administration.PermissionKeys.Tenants);
result.Remove("*");
result.Remove("?");
//...
Modifica UserPermissionRepository.cs bajo ‘Update’
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();
//...
Modifica RolePermissionRepository.cs bajo ‘Update’
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();
//...
Agreaga TenantId a RoleRow.cs
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;
//...
}
}
}
Haz un Nuevo archive llamado IMultiTenantRow.cs junto a TenantRow.cs y agrega la siguiente interface.
using Serenity.Data;
namespace MultiTenancy
{
public interface IMultiTenantRow
{
Int32Field TenantIdField { get; }
}
}
Haz un Nuevo archive llamado MultiTenantBehavior.cs junto a TenantRow.cs y agrega lo siguente
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) { }
}
}
En RoleRow.cs Agrega el Nuevo comportamiento:
namespace MultiTenancy.Administration.Entities
{
//...
public sealed class RoleRow : Row, IIdRow, INameRow, IMultiTenantRow
{
//...
public Int32Field TenantIdField
{
get { return Fields.TenantId; }
}
//...
}
}
En el resto de los Row.cs que le pusiste TenantId, Implementa el nuevo comportaimento junto con la columna para TenantId.
namespace MultiTenancy.Administration.Entities
{
//...
public sealed class RoleRow : Row, IIdRow, INameRow, IMultiTenantRow
{
//...
[Insertable(false), Updatable(false)]
public Int32? TenantId
{
get { return Fields.TenantId[this]; }
set { Fields.TenantId[this] = value; }
}
public Int32Field TenantIdField
{
get { return Fields.TenantId; }
}
//...
public class RowFields : RowFieldsBase
{
//...
public readonly Int32Field TenantId;
}
}
}
}