【问题标题】:How To design configurable field level permissions with Entity Framework如何使用实体框架设计可配置的字段级权限
【发布时间】:2017-09-18 10:02:08
【问题描述】:

假设我们有一张关于某些车型的信息表,例如:

如果我还需要用户可配置规则,我将如何最好地实现读写操作的字段级访问权限?我正在使用 MSSQL Server 2016 和 EF 6。

基于该表,我们可能有以下用例,这些用例描述了对某个角色或组可见的字段:

1) 公开数据的默认权限组

2) 基于实体的权限组

3) 基于自定义字段的权限组

要求是,隐藏数据必须与 NULL 值不同,并且规则/权限必须是用户可配置的。我还需要对列表进行分页,这需要对可见数据进行正确排序。为此,我需要一种处理数据类型的方法。例如,施工年份是不可为空的 DateTime,但当字段不可见时,需要将其设置为默认值,如 DateTime.MinValue。在处理位(布尔)值时,这变得更具挑战性:-)

我目前正在考虑使用表值函数的方法,这对于我的场景来说似乎更难以动态实现,或者使用单独的缓存层来保存全部数据,我需要保持同步与数据库。

【问题讨论】:

  • 基本上你需要一个函数,如果给定一些行,你的表中的用户权限包和访问修饰符返回位图,说明你实际上可以在这个修饰符中访问该行中的哪些字段?
  • @eocron,没错。然而?我认为对于分页输出,我需要在数据库(或特殊表)级别进行此操作。
  • SQL 数据库动态数据屏蔽可以完成这项工作吗? docs.microsoft.com/en-us/azure/sql-database/…
  • @sprinter252 权限应该取决于登录用户的应用程序。动态数据屏蔽适用于数据库用户级别。 SQL 服务器支持字段级支持,但不支持查询级。 :(
  • @cSteusloff 您的意思是为每个用户/组的每条记录及其字段指定权限吗?

标签: c# sql-server entity-framework permissions


【解决方案1】:

另一种选择是使用 Castle.DynamicProxy (https://github.com/castleproject/Core/blob/master/docs/dynamicproxy.md) 创建代理:

class Program
{
    static void Main(string[] args)
    {
        ProxyGenerator generator = new ProxyGenerator();
        var person = new Person { Id = 1, Name = "Bob", Age = 40 };
        var proxy = generator.CreateClassProxyWithTarget<Person>(person, new EFInterceptor(new SecurityInfo()));
        Console.WriteLine("Id: {0}, Name: {1}, Age: {2}", person.Id, person.Name, person.Age);
        Console.WriteLine("Id: {0}, Name: {1}, Age: {2}", proxy.Id, proxy.Name, proxy.Age);
    }
}

public class Person
{
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }
    public virtual int Age { get; set; }
}

public interface ISecurityInfo
{
    bool IsAllowed(string propName);
}

public class SecurityInfo : ISecurityInfo
{
    public bool IsAllowed(string propName)
    {
        return propName != nameof(Person.Age);
    }
}

class EFInterceptor : Castle.DynamicProxy.IInterceptor
{
    private readonly ISecurityInfo securityInfo;

    public EFInterceptor(ISecurityInfo info)
    {
        this.securityInfo = info;
    }

    public void Intercept(IInvocation invocation)
    {
        if (invocation.Method.Name.StartsWith("get_"))
        {
            var propName = invocation.Method.Name.Replace("get_", "");
            HandleAccess(invocation, propName);
        }
        if (invocation.Method.Name.StartsWith("set_"))
        {
            var propName = invocation.Method.Name.Replace("set_", "");
            HandleAccess(invocation, propName);
        }
    }

    private void HandleAccess(IInvocation invocation, string propName)
    {
        if (!securityInfo.IsAllowed(propName))
        {
            invocation.ReturnValue = GetDefault(invocation.Method.ReturnType);
        } else
        {
            invocation.Proceed();
        }
    }

    public static object GetDefault(Type type)
    {
        if (type.IsValueType)
        {
            return Activator.CreateInstance(type);
        }
        return null;
    }
}

【讨论】:

    【解决方案2】:

    实现目标的一种简单方法是创建一个设置表,在其中按组指定每个字段的可见性。

    首先你需要制作一个这样的组(品牌)表:

     public class Group
        {
            public int Id { get; set; }
            public string Name { get; set; }
        }
    

    那么您将需要一个用于可见性设置的表格:

      public class TableVisibilitySettings
        {
            public int Id { get; set; }
            public int GroupId { get; set; }
            public virtual Group Group { get; set; }
            public bool ContructionYear { get; set; }
            public bool Power { get; set; }
            public bool IsConvertible { get; set; }
        }
    

    然后您将需要您的表格和视图模型:

    public class Table
        {
            public int Id { get; set; }
            public int GroupId { get; set; }
            public virtual Group Grup { get; set; }
    
            public string Color { get; set; }
            public int? ConstructionYear { get; set; }
            public string Power { get; set; }
            public bool? IsConvertible { get; set; }
    
    
            public IEnumerable<TableVm> GetTableByGroupType(int groupId, ApplicationDbContext context)
            {
                var table = context.Tables.ToList();
                var visibility = context.TableVisibilitySettings.FirstOrDefault(x => x.GroupId == groupId);
    
                return table.Select(x => new TableVm
                {
                    Id = x.Id,
                    Brand= x.Grup.Name,
                    Color = x.Color,
                    ConstructionYear = visibility.ContructionYear == true ? x.ConstructionYear : null,
                    Power = visibility.Power == true ? x.Power : null,
                    IsConvertible = visibility.IsConvertible == true ? x.IsConvertible : null
                }).ToList();
            }
        }
    

    使用GetTableByGroupType 方法,您可以根据每个组的可见性设置检索数据库。

    如果您愿意,可以使用角色而不是组。

    编辑:

    一种应用分页的方法可以是这样的:

     public IEnumerable<TableVm> GetTableByGroupWithPag(int groupId, ApplicationDbContext context,int pageNumber, int rowsPerPage)
            {
    
                var table = context.Tables.Skip((pageNumber-1)*rowsPerPage).Take(rowsPerPage).ToList();
    
                var visibility = context.TableVisibilitySettings.FirstOrDefault(x => x.GroupId == groupId);
    
                return table.Select(x => new TableVm
                {
                    Id = x.Id,
                    Group = x.Grup.Name,
                    Color = x.Color,
                    ConstructionYear = visibility.ContructionYear == true ? x.ConstructionYear : null,
                    Power = visibility.Power == true ? x.Power : null,
                    IsConvertible = visibility.IsConvertible == true ? x.IsConvertible : null
                }).ToList();
            }
    

    首先,您需要从表格中获取要显示的行,而不是只需要应用可见性设置。

    编辑:

    根据您的应用程序设计和技能,有多种方法可以将组链接到用户。 最简单的方法是在ApplicationUserGroup 之间设置one to onemany to many 关系,如下所示:

    public class ApplicationUser
    {
     ...
     public int GroupId {get;set;}
     public virtual Group Group
    }
    

    并且在 Group 类中你需要添加:

     public virtual ICollection<ApplicationUser> Users {get;set;}
    

    另一种方法是为每个品牌创建角色,并根据您希望他读/写的品牌为每个用户分配一个或多个角色。

    另一种方法是使用声明,您只需向每个用户添加代表 groupId 或 groupName 或品牌的声明。

    希望这将帮助您选择一种将用户链接到组的方式。

    【讨论】:

    • 组的方法非常好。这就是我需要的。但是有了ToList,我把整个表都放在了内存中。大表上的排序和分页不是这样工作的。但是,我可以从这个概念创建一个 LINQ 查询。然后我得到一个巨大的查询,我会检查性能。
    • 在您的解决方案中,我错过了用户和汽车之间的基于字段权限的映射。我只看到所有汽车的基于列的权限。潜在买家可以从 ID 2 和 4 中看到 constructionYear,但不能从 ID 1 和 3 中看到。
    • 您使用@Bozhidar Stoinev 方法的解决方案是正确的方法。
    【解决方案3】:

    由于您需要像这样配置权限(请参阅我的评论),因此该问题与 EF 无关 - 这与您应用的业务逻辑有关。

    我建议在您的业务层中设计一个 API,用于读取数据(即汽车)并应用可能(或可能不会)提前读取的安全权限。

    IMO,权限配置表架构,应如下所示:

    CREATE TABLE [dbo].[PermissionsConfig] (
        [Id]         INT NOT NULL,
        [CarId]      INT NOT NULL,
        [UserId]     INT NOT NULL,
        [Permission] INT NOT NULL,
        PRIMARY KEY CLUSTERED ([Id] ASC), 
        CONSTRAINT [FK_PermissionsConfig_Car] FOREIGN KEY ([CarId]) REFERENCES [Car]([Id]), 
        CONSTRAINT [FK_PermissionsConfig_User] FOREIGN KEY ([UserId]) REFERENCES [User]([Id])
    );
    

    接下来创建一个flagged enum来指定权限:

    [Flags]
    public enum CarFieldPermission
    {
        Unknown = 0,
        ViewConstructionYear = 2,
        ViewPower = 4,
        ViewIsConvertible = 8
    }
    

    要配置权限,请遍历必要的用户/角色/组和所有汽车,并对标志执行按位OR 以计算权限。例如。

    var permissionConfigEntry.Permission = CarFieldPermission.ViewConstructionYear 
        | CarFieldPermission.ViewPower
    ;
    

    稍后,在您的业务层 API 中,从汽车表 (use LINQ Skip() and Take() methods) 中读取一个页面。然后循环记录并检查当前用户和汽车的权限配置; 隐藏必要的数据:

    public IEnumerable<Car> LoadCars(User user, int pageIndex, int pageSize)
    {
        var result = db.Cars
            .Skip((pageIndex - 1) * pageSize)
            .Take(pageSize)
            .ToArray()
        ;
    
        var carsInInterest = result.Select(c => c.Id).ToArray();
    
        var allThePermissions = db.PermissionConfiguration
            .Where(pc => pc.User.Equals(user))
            .Where(pc => carsInInterest.Contains(pc.CarId))
            .ToArray()
        ;
    
        foreach (var carX in result)
        {
            var current = allThePermissions.FirstOrDefault(pc => pc.User.Equals(user) && pc.Car.Equals(carX));
    
            if (current != null)
            {
                if (!current.Permissions.HasFlag(CarFieldPermission.ViewConstructionYear))
                {
                    carX.ConstructionYear = null;
                }
            }
        }
    
        return result;
    }
    

    【讨论】:

    • 谢谢,我认为权限配置表可以快速增长。我希望 MSSQL 可以处理超过 1 亿个条目。这个概念没问题,即使查询需要优化:-)
    • 是的,我的所有意图都是带来一个概念。这段代码远不是一个完整的解决方案。
    【解决方案4】:

    如果您完全关心保护此解决方案的安全,我认为您无法缓存,但相关用户除外。也就是说,我将通过返回由位 visible 字段组成的数据来解决此问题,并将那些不可见的字段清空。如果您将数据提取到临时表中,找出他们应该能够看到的内容(根据需要更新 visible 字段),然后将 null 取出这些值,您就可以清楚地看到向您的 UI 指示某些东西是 null,因为它是以这种方式存储的(visible 是 1 但在该字段中有 null)或者因为用户不应该能够看到它(visible 是 0 和数据是null)。

    是的,这有点乏味。这也意味着您不会冒着向用户返回他们不应该看到的数据的风​​险——如果数据不应该显示,您的 UI 开发人员/维护人员将根本无法显示数据。如果是你在做 UI 工作,这样你会更安全,因为你不会忘记你在路上的某个地方打算做什么并暴露数据。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-06-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-28
      • 1970-01-01
      相关资源
      最近更新 更多