Подтвердить что ты не робот

Как создать настраиваемые разрешения на уровне поля с помощью Entity Framework

Скажем, у нас есть таблица информации, относящаяся к определенным моделям автомобилей, например: введите описание изображения здесь

Как лучше всего реализовать разрешения доступа на уровне поля для операций чтения и записи, если мне также нужно, чтобы правила настраивались пользователем? Я использую MSSQL Server 2016 и EF 6.

На основе этой таблицы мы можем иметь следующие варианты использования, которые описывают поля, видимые для определенной роли или группы:

1) Группа разрешений по умолчанию для общедоступных данных

введите описание изображения здесь

2) Группа разрешений на основе сущностей

введите описание изображения здесь

3) Пользовательская группа разрешений на основе полей

введите описание изображения здесь

Требования заключаются в том, что скрытые данные должны отличаться от значений NULL, а правила/разрешения должны настраиваться пользователем. Мне также нужно разбивать списки, что требует правильной сортировки по видимым данным. Для этого мне нужен способ обработки типов данных. Например, год строительства - это значение, не равное null null, но когда поле не отображается, оно должно быть установлено на значение по умолчанию, такое как DateTime.MinValue. Это становится намного сложнее при работе с битовыми (булевыми) значениями: -)

В настоящее время я рассматриваю подход с табличными функциями, которые, как представляется, сложнее реализовать динамически для моего сценария или отдельным слоем кэширования, который содержит все данные, которые мне нужно будет синхронизировать с базой данных.

4b9b3361

Ответ 1

Одним из простых способов достижения вашей цели может быть создание таблицы настроек, где можно указать видимость каждого поля по группе.

Сначала вам понадобится сделать таблицу групп (для бренда) следующим образом:

 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();
        }

Сначала вам нужно взять строки для отображения из вашей таблицы, чем вам нужно только применить настройки видимости.

Edit:

Существует несколько способов связать группу с пользователем, в зависимости от вашего дизайна приложения и ваших навыков. Самый простой способ - установить отношения one to one или many to many между ApplicationUser и Group, например:

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

и в классе группы вы должны добавить:

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

Другим способом является создание ролей для каждой марки и предоставление каждому пользователю одной или нескольких ролей на основе брендов, которые вы хотите, чтобы он читал/писал.

Другим способом является использование претензий, и все, что вам нужно сделать, - добавить каждому пользователю заявку, представляющую groupId или groupName или бренд.

Надеемся, что это поможет вам выбрать способ связать пользователя с группой.

Ответ 2

Другим вариантом будет создание прокси с помощью 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;
    }
}

Ответ 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])
);

Затем создайте с отметкой enum, чтобы указать разрешения:

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

Чтобы настроить разрешения, прокрутите необходимые пользователи/роли/группы и все автомобили и поразрядьте OR на флажках для вычисления разрешений. Например.

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

Позже, в API бизнес-уровня, прочитайте страницу из таблицы автомобилей ( с помощью методов LINQ Skip() и Take()). Затем прокрутите записи и проверьте конфигурацию разрешений на текущего пользователя и автомобиль; скрыть данные по мере необходимости:

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;
}

Ответ 4

Если вы вообще обеспокоены обеспечением этого решения, я не думаю, что вы можете кэшировать, за исключением соответствующего пользователя. Тем не менее, я бы подошел к этому, возвращая данные, состоящие из битового поля visible, и опущу те поля, которые не видны. Если вы поместите свои данные в таблицу temp, выясните, что они должны видеть (обновив поле visible, если это необходимо), а затем null вне значения, вы сможете четко укажите, что что-то есть null, потому что оно хранится таким образом (visible равно 1, но имеет null в этом поле) или потому, что пользователь не должен его видеть (visible равен 0 и данные null).

Да, это немного утомительно. Это также означает, что вы не рискуете вернуть данные пользователю, которого они не должны видеть - ваш разработчик/сопровождающий пользовательский интерфейс просто не сможет отображать данные, если они не должны отображаться. Вы более безопасны таким образом, если вы работаете с пользовательским интерфейсом, потому что не забудете, что вы намеревались где-то в дороге, и выставлять данные.