Про виды разграничения прав

Кросс-пост с RSDN

Классическая rbs (role-based security)

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

Настраиваемая RBS

Тут роли разделяются на группы пользователей и наборы разрешений. Пользователи связаны многие-ко-многим с группами, группы — многие-ко-многим с наборами разрешений, наборы разрешений — многие-ко-многим разрешениями.
Разрешения также выдаются на некоторые действия, но при большом количестве удобнее управлять согласованными наборами разрешений, а не отдельными разрешениями. Наборы разрешений можно изменять в процессе работы программы, как и группы пользователей.
По опыту такая система сложнее классической раз в пять. Поэтому без необходимости городить не стоит.

Claims-based security

Почти тоже самое что и предыдущий пункт, только вместо групп пользователей используются claim_ы которые могу иметь разный тип (имеется ввиду бизнес-тип). Типичные claims - это имя пользователя, группы (может быть несколько), email, в бизнес-приложениях это также может быть имя менеджера, отдел итп.
Таким образом пользователи имеет много claim_ов, каждый claim определяется парой (тип, значение), набор разрешений привязывается к набору claim_ов.

А теперь самое интересное. Система разграничения прав в приложениях с CBS часто оказывается проще, чем RBS в настраиваемом вида. RBS заставляет плодить много групп, в том числе автоматически. При использовании CBS от этого можно совсем отказаться, а сам механизм получения claim_ов вынести отдельно, что уменьшит сложность самого приложения (например Windows Identity Foundation так и работает, и позволяет сделать claims-provider, работающий с AD).
Кроме того возможен fallback к RBS — достаточно один из типов claim_ов объявить группой\ролью.


Это неполный список, всегда можно придумать вариации.

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

Поначалу все просто : связь роль-разрешения, группа-группа разрешений, набор claim_ов — группа разрешений становится тренарной, то есть к каждой связи добавляет атрибут — ссылка на объект. Тогда сама связь, указывающая что для данного объекта данная роль имеет указанные разрешения становится неким ACE (Access Control Entry из терминологии Windows), множество ACE, сгруппированных по ссылке на объект, называются ACL (Access Control List).


Вроде все просто, но при построении такой системы надо решить несколько принципиальных вопросов:

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

Часто такую систему называют Row-Level Securty, потому что проверки реализуются на уровне строк БД, иначе очень медленно будет.



IQueryable и Generics

Довольно часто различные сущности данных обладают сходными свойствами и обрабатываются одинаково. При обработке объектов в программе общность  выражается интерфейсами, а обработка производится обобщенными методами (generics).

При использовании Linq можно написать аналогично

/// <summary>
/// Интерфейс для всех сущностей,
/// которые могут не показываться позьзователю
/// </summary>
public interface IVisible
{
    bool Visible { get; set; }
}

/// <summary>
/// Выбор только видимых сущностей
/// </summary>
public static class IVisibleExtensions
{
    public static IQueryable<T> Visible<T>(this IQueryable<T> q) 
        where T : IVisible
    {
        return q.Where(o => o.Visible);
    }
}

Потом можно создать модель данных (я использую EF), сделать сущности с полем Visible и с помощью partial-класса «прицепить» интерфейс к сущности

/// <summary>
/// Какая-то сущность
/// </summary>
public partial class Entity1 : IVisible
{
}

Теперь тестовый код…

static void Main(string[] args)
{
    var context = new Model1Container();
    foreach (var item in context.Entity1Set.Visible())
    {
        Console.WriteLine(item);
    }
}

Этот код отваливается с ошибкой Unable to cast the type 'Entity1' to type 'IVisible'. LINQ to Entities only supports casting Entity Data Model primitive types.

Проблема заключается в том что выражения o.Visible внутри generic метода Visible<T> преобразуется в expression tree вида
((IVisible)o).Visible. Linq2EF (как и другие Linq-провайдеры) не понимают что делать с типом IVisible и генерация SQL-выражения падает.

Можно конечно собирать expression нужного вида руками для каждого обобщенного метода обработки запроса, но это не наш путь.

Лучше написать метод, который устраняет очевидно лишние приведения типов в expression. Для этого сделаем extension-метод Fix.

public static IQueryable<T> Fix<T>(this IQueryable<T> q)
{
    var visitor = new FixupVisitor();
    return q.Provider.CreateQuery<T>(visitor.Visit(q.Expression));
}

Теперь осталось написать FixupVisitor. В .NET 4 включен класс ExpressionVisitor в пространстве имен System.Linq.Expressions, который поддерживает в том числе расширенные деревья выражений. Для .NET 3.5 можно взять IQToolkit.

internal class FixupVisitor: ExpressionVisitor
{
    protected override Expression VisitUnary(UnaryExpression u)
    {
        if (u.NodeType != ExpressionType.Convert)
        {
            return base.VisitUnary(u);
        }

        var operandType = u.Operand.Type;
        var expressionType = u.Type;
        if (expressionType.IsInterface 
            && operandType.GetInterfaces()
                          .Contains(expressionType))
        {
            return base.Visit(u.Operand);
        }
        else
        {
            return base.VisitUnary(u);
        }
    }
}

Этот визитор просто выкидывает избыточное приведение типа к интерфейсу.

Еще один тестовый код

static void Main(string[] args)
{
    var context = new Model1Container();
    foreach (var item in context.Entity1Set.Visible().Fix())
    {
        Console.WriteLine(item);
    }
}

Добавился только вызов Fix в конце и все работает.

Продолжение следует…



Аудит изменений с учетом контекста операций

В предыдущем посте я описал способ, который позволяет проводить аудит изменений данных в Entity Framework, не затрагивая сами классы сущностей.

Аудит изменений данных вполне возможно делать в самой БД с помощью триггеров, но в базу не попадают сведения о том какой пользователь системы произвел изменения (в случае приложения, построенного по принципу trusted subsystem) и с какой целью были проведены эти изменения.

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

Для задач логирования было бы удобно иметь доступ к некоторым параметрам контекста, особенно неявным. Эти параметры должны прозрачно передаваться по цепочке вызовов. Для решения таких задач можно применить монады, но тогда придется переписать весь код под использование монад, что очень проблематично. Можно воспользоваться возможностями AOP и IoC чтобы обеспечить неявную передачу явного контекста.

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

public interface IContextManager<T>
{
    void Push(T value);
    void Revert();
    IEnumerable<T> GetValues();
}

Реализация такого интерфейса тривиальна:

public class ContextManager<T>: IContextManager<T>
{
    Stack<T> _stack = new Stack<T>();

    public void Push(T value)
    {
        _stack.Push(value);
    }

    public void Revert()
    {
        _stack.Pop();
    }

    public IEnumerable<T> GetValues()
    {
        foreach (var item in _stack)
        {
            yield return item;
        }
    }
}

 

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

try
{
    _contextManager.Push(contextValue);
    //do actions
}
finally
{
    _contextManager.Revert();
}

Естественно каждый раз писать кучу try\finally не хочется, а учитывая что контекст в большинстве случаев статический, то хотелось бы задавать его декларативно и в одном месте.

Тут на помощь приходит AoP. Можно написать небольшой обработчик вызовов и с помощью Unity Interception устанавливать текущий контекст в зависимости от атрибута.

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class ContextAttribute : HandlerAttribute
{
    public ContextAttribute(object value)
    {
        this.Value = value;
    }

    public object Value { get; private set; }

    public override ICallHandler CreateHandler(IUnityContainer container)
    {
        var method = this.GetType().GetMethod("CreateHandlerInternal", BindingFlags.Instance | BindingFlags.NonPublic);
        return (ICallHandler)method.MakeGenericMethod(Value.GetType()).Invoke(this, new object[] { container });
    }

    private ContextCallHandler<T> CreateHandlerInternal<T>(IUnityContainer container)
    {
        return new ContextCallHandler<T>(container.Resolve<IContextManager<T>>(), (T)this.Value);
    }

}

public class ContextCallHandler<T>: ICallHandler
{
    IContextManager<T> _manager;
    T _value;

    public ContextCallHandler(IContextManager<T> manager, T value)
    {
        _manager = manager;
        _value = value;
    }

    public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
    {
        _manager.Push(_value);
        var result = getNext()(input, getNext);
        _manager.Revert();
        return result;
    }
    public int Order { get; set; }
}

 

Теперь навешивая атрибут [Context(value)] можно задавать контекст метода. А заинжектив в ObjectContext экземпляр типа IContextManager<T> можно при логировании получать текущее значение контекста, в случае если SaveChanges вызывается в самом конце цепочки вызовов.

Для того чтобы все работало надо правильно сконфигурировать контейнер как описано в этом посте.



Аудит изменений в Entity Framework

В Entity Framework v1 есть возможность подписаться на событие SavingChanges. Это событие вызывается перед записью в базу когда вызывается метод SaveChanges. Чтобы получить все измененные записи в контексте необходимо обратиться к свойству ObjectStateManager контекста и вызвать его метод GetObjectStateEntries.

Ниже приведен код partial класса контекста, который сохраняет изменения сущностей.

 

   1: partial void OnContextCreated()
   2: {
   3:     this.SavingChanges += new EventHandler(AuditLogHandler);
   4: }
   5:  
   6: void AuditLogHandler(object sender, EventArgs _)
   7: {
   8:     var entries = this.ObjectStateManager
   9:                       .GetObjectStateEntries(EntityState.Added 
  10:                                            | EntityState.Deleted 
  11:                                            | EntityState.Modified)
  12:                       .Where(e => !e.IsRelationship)
  13:                       .ToList()
  14:                       ;
  15:  
  16:     var tranId = Guid.NewGuid();
  17:     var now = DateTime.Now;
  18:  
  19:     var logEntries = from e in entries
  20:                      select new AuditLogEntry
  21:                      {
  22:                          TableName = e.EntitySet.Name,
  23:                          Action = (byte)e.State,
  24:                          Time = now,
  25:                          TransactionId = tranId,   
  26:                          Key = KeyToString(e.EntityKey),
  27:                          Values = StateEntryToXml(e)
  28:                      };
  29:     SaveLog(logEntries);
  30: }
  31:  
  32: string KeyToString(EntityKey key)
  33: {
  34:     if (key == null || key.IsTemporary)
  35:     {
  36:         return null;
  37:     }
  38:     if (key.EntityKeyValues.Length == 1)
  39:     {
  40:         return key.EntityKeyValues[0].Value.ToString();
  41:     }
  42:     else
  43:     {
  44:         return string.Join("; ",
  45:                    key.EntityKeyValues
  46:                       .Select(k => k.Key + "=" + k.Value)
  47:                       .ToArray());
  48:     }
  49: }
  50:  
  51: private string StateEntryToXml(ObjectStateEntry e)
  52: {
  53:     switch (e.State)
  54:     {
  55:         case EntityState.Added:
  56:             var count = e.CurrentValues.FieldCount;
  57:             return new XElement("Values",
  58:                        from i in Enumerable.Range(0, count)
  59:                        select new XElement("Value",
  60:                            new XAttribute("Name",
  61:                                e.CurrentValues.GetName(i)),
  62:                            e.CurrentValues.GetValue(i)))
  63:                    .ToString();
  64:         case EntityState.Deleted:
  65:             return null;
  66:         case EntityState.Modified:
  67:             return new XElement("Values",
  68:                        from v in e.GetModifiedProperties()
  69:                        let ord = e.OriginalValues.GetOrdinal(v)
  70:                        select new XElement("Value",
  71:                            new XAttribute("Name", v),
  72:                            new XAttribute("Old",
  73:                                e.OriginalValues.GetValue(ord)),
  74:                            e.CurrentValues.GetValue(ord)))
  75:                    .ToString();
  76:         default:
  77:             throw new InvalidOperationException();
  78:     }
  79: }

Таким же способом можно поддерживать Row-Level Security при обновлении данных с помощью EF.



История одного маппера

Однажды холодным зимним вечером я читал блог Brad Wilson, а именно вот эту статью и понял что нужно писать View-specific models для ASP.NET приложения. Основная проблема с написанием таких моделей заключается в том что приходится писать много “водопроводного кода” для маппинга сущностей базы, возвращаемых ORM на эти самые модели и наоборот. Причем Linq (если поддерживает ORM) позволяет описывать прямое преобразование, но не обратное.

Я начал искать object-to-object мапперы. Сразу нашел AutoMapper, он меня отпугнул монструозностью конфигурации и жутко неудобным (читай статическим) API использования. Кроме того отзывы о скорости работы этого маппера крайне негативные.

Следующим мне на глаза попался EmitMapper. Гораздо более приятный API для использования и довольно высокая скорость работы за счет кодогенерации. Но настройка и кастомизация выполняется очень многословно и непонятно.

В обоих проектах меня не устроил тяжелый API для маппинга. По сути маппинг из типа A в тип B - не более чем функция  A → B, или в нотации типов C# - Func<A,B>.

Я сел писать свой маппер. Естественно для скорости надо заниматься кодогенерацией, но писать свой кодогенератор в несколько килострок кода времени не хватит и проект будет заброшен. Но, к счастью, в .NET начиная с версии 3.5 есть кодогенератор и AST для него. Это классы наследники Expression из пространства имен System.Linq.Expressions, а компилятор тихо сидит в методе Expression<T>.Compile.

Таким образом задача упрощается до безобразия. Необходимо собрать expression tree и скомпилировать его. За два вечера я написал маппер, который поддерживает маппинг массивов, списков, сложных типов, конфигурацию с помощью expression tree и flattering.

Результаты сего труда я залил на Codeplex. Проект назвал Expression Mapper.

Скорость работы мапппера.

Пока писал маппер нашел бенчмарк на хабре. Решил прогнать свой маппер на таком бенчмарке. Результаты немного поразили:

Handwritten Mapper: 88 milliseconds
Emit Mapper: 157 milliseconds
Auto Mapper: 31969 milliseconds
Expression Mapper: 119 milliseconds

Недостатки.

Кроме того что проект еще сырой и требует доработки есть еще один серьезный недостаток. Нету возможности сделать маппер, который не создает новый объект, а изменяет поля в существующем.

Ссылка на страницу проекта - http://expressionmapper.codeplex.com/