Идентификация и аутентификация, управление доступом
Управление доступом в Java-среде
Java – это объектно-ориентированная система программирования, поэтому и управление доступом в ней спроектировано и реализовано в объектном стиле. По этой причине рассмотреть Java-среду для нас очень важно. Подробно о Java-технологии и безопасности Java-среды рассказано в статье А. Таранова и В. Цишевского "Java в три года" (Jet Info, 1998, 11-12). С разрешения авторов далее используются ее фрагменты.
Прежде всего, остановимся на эволюции модели безопасности Java. В JDK 1.0 была предложена концепция "песочницы" (sandbox) – замкнутой среды, в которой выполняются потенциально ненадежные программы ( апплеты, поступившие по сети). Программы, располагающиеся на локальном компьютере, считались абсолютно надежными, и им было доступно все, что доступно виртуальной Java-машине.
В число ограничений, налагаемых "песочницей", входит запрет на доступ к локальной файловой системе, на сетевое взаимодействие со всеми хостами, кроме источника апплета, и т.п. Независимо от уровня достигаемой при этом безопасности (а проблемы возникали и с разделением свой/чужой, и с определением источника апплета), наложенные ограничения следует признать слишком обременительными: возможности для содержательных действий у апплетов почти не остается.
Чтобы справиться с этой проблемой, в JDK 1.1 ввели деление источников (точнее, распространителей) апплетов на надежные и ненадежные (источник определялся по электронной подписи). Надежные апплеты приравнивались в правах к "родному" коду. Сделанное послабление решило проблемы тех, кому прав не хватало, но защита осталась неэшелонированной и, следовательно, неполной.
В JDK 1.2 сформировалась модель безопасности, используемая и в Java 2. От модели "песочницы" отказались. Оформились три основных понятия:
- источник программы ;
- право и множество прав ;
- политика безопасности.
Источник программы определяется парой (URL, распространители программы). Последние задаются набором цифровых сертификатов.
Право – это абстрактное понятие, за которым, как и положено в объектной среде, стоят классы и объекты. В большинстве случаев право определяется двумя цепочками символов – именем ресурса и действием. Например, в качестве ресурса может выступать файл, а в качестве действия – чтение. Важнейшим методом "правовых" объектов является implies(). Он проверяет, следует ли одно право (запрашиваемое) из другого (имеющегося).
Политика безопасности задает соответствие между источником и правами поступивших из него программ (формально можно считать, что каждому источнику соответствует своя "песочница"). В JDK 1.2 "родные" программы не имеют каких-либо привилегий в плане безопасности, и политика по отношению к ним может быть любой. В результате получился традиционный для современных ОС и СУБД механизм прав доступа со следующими особенностями:
- Java-программы выступают не от имени пользователя, их запустившего, а от имени источника программы. (Это весьма глубокая и прогрессивная трактовка, если ее правильно развить, см. следующий раздел);
- нет понятия владельца ресурсов, который мог бы менять права; последние задаются исключительно политикой безопасности (формально можно считать, что владельцем всего является тот, кто формирует политику);
- механизмы безопасности снабжены объектной оберткой.
Весьма важным понятием в модели безопасности JDK 1.2 является контекст выполнения. Когда виртуальная Java-машина проверяет права доступа объекта к системному ресурсу, она рассматривает не только текущий объект, но и предыдущие элементы стека вызовов. Доступ предоставляется только тогда, когда нужным правом обладают все объекты в стеке. Разработчики Java называют это реализацией принципа минимизации привилегий.
На первый взгляд, учет контекста представляется логичным. Нельзя допускать, чтобы вызов какого-либо метода расширял права доступа хотя бы по той причине, что доступ к системным ресурсам осуществляется не напрямую, а с помощью системных объектов, имеющих все права.
К сожалению, подобные доводы противоречат одному из основных принципов объектного подхода – принципу инкапсуляции. Если объект A обращается к объекту B, он не может и не должен знать, как реализован B и какими ресурсами он пользуется для своих целей. Если A имеет право вызывать какой-либо метод B с некоторыми значениями аргументов, B обязан обслужить вызов. В противном случае при формировании политики безопасности придется учитывать возможный граф вызовов объектов, что, конечно же, нереально.
Разработчики Java осознавали эту проблему. Чтобы справиться с ней, они ввели понятие привилегированного интервала программы. При выполнении такого интервала контекст игнорируется. Привилегированная программа отвечает за себя, не интересуясь предысторией. Аналогом привилегированных программ являются файлы с битами переустановки идентификатора пользователя/группы в ОС Unix, что лишний раз подтверждает традиционность подхода, реализованного в JDK 1.2. Известны угрозы безопасности, которые привносят подобные файлы. Теперь это не лучшее средство ОС Unix перекочевало в Java.
Рассмотрим дисциплину контроля прав доступа более формально.
Класс AccessController (встроенный менеджер безопасности) предоставляет единый метод для проверки заданного права в текущем контексте – checkPermission (Permission). Это лучше (по причине параметризуемости), чем множество методов вида checkXXX, присутствующих в SecurityManager – динамически изменяемом менеджере безопасности из ранних версий JDK.
Пусть текущий контекст выполнения состоит из N стековых фреймов (верхний соответствует методу, вызвавшему checkPermission(p)). Метод checkPermission реализует следующий алгоритм (см. Листинг 10.1).
i = N; while (i > 0) { if (метод, породивший i-й фрейм, не имеет проверяемого права) { throw AccessControlException } else if (i-й фрейм помечен как привилегированный) { return; } i = i – 1; }; // Выясним, есть ли проверяемое право у унаследованного контекста inheritedContext.checkPermission (p);Листинг 10.1. Алгоритм работы метода checkPermission класса AccessController.
Сначала в стеке ищется фрейм, не обладающий проверяемым правом. Проверка производится до тех пор, пока либо не будет исчерпан стек, либо не встретится "привилегированный" фрейм, созданный в результате обращения к методу doPrivileged(PrivilegedAction) класса AccessController. Если при порождении текущего потока выполнения был сохранен контекст inheritedContext, проверяется и он. При положительном результате проверки метод checkPermission(p) возвращает управление, при отрицательном возникает исключительная ситуация AccessControlException.
Выбранный подход имеет один недостаток – тяжеловесность реализации. В частности, при порождении нового потока управления с ним приходится ассоциировать зафиксированный "родительский" контекст и, соответственно, проверять последний в процессе контроля прав доступа.
Отметим, что этот подход не распространяется на распределенный случай (хотя бы потому, что контекст имеет лишь локальный смысл, как, впрочем, и политика безопасности).
В целом средства управления доступом в JDK 1.2 можно оценить как "наполовину объектные". Реализация оформлена в виде интерфейсов и классов, однако по-прежнему разграничивается доступ к необъектным сущностям – ресурсам в традиционном понимании. Не учитывается семантика доступа. Имеют место и другие отмеченные выше концептуальные проблемы.
Возможный подход к управлению доступом в распределенной объектной среде
Представляется, что в настоящее время проблема управления доступом существует в трех почти не связанных между собой проявлениях:
- традиционные модели (дискреционная и мандатная);
- модель "песочница" (предложенная для Java-среды и близкой ей системы Safe-Tcl);
- модель фильтрации (используемая в межсетевых экранах).
На наш взгляд, необходимо объединить существующие подходы на основе их развития и обобщения.
Формальная постановка задачи разграничения доступа может выглядеть следующим образом.
Рассматривается множество объектов (в смысле объектно-ориентированного программирования). Часть объектов может являться контейнерами, группирующими объекты-компоненты, задающими для них общий контекст, выполняющими общие функции и реализующими перебор компонентов. Контейнеры либо вложены друг в друга, либо не имеют общих компонентов.
С каждым объектом ассоциирован набор интерфейсов, снабженных дескрипторами (ДИ). К объекту можно обратиться только посредством ДИ. Разные интерфейсы могут предоставлять разные методы и быть доступными для разных объектов.
Каждый контейнер позволяет опросить набор ДИ объектов-компонентов, удовлетворяющих некоторому условию. Возвращаемый результат в общем случае зависит от вызывающего объекта.
Объекты изолированы друг от друга. Единственным видом межобъектного взаимодействия является вызов метода.
Предполагается, что используются надежные средства аутентификации и защиты коммуникаций. В плане разграничения доступа локальные и удаленные вызовы не различаются.
Предполагается также, что разрешение или запрет на доступ не зависят от возможного параллельного выполнения методов (синхронизация представляет отдельную проблему, которая здесь не рассматривается).
Разграничивается доступ к интерфейсам объектов, а также к методам объектов (с учетом значений фактических параметров вызова). Правила разграничения доступа (ПРД) задаются в виде предикатов над объектами.
Рассматривается задача разграничения доступа для выделенного контейнера CC, компонентами которого должны являться вызывающий и/или вызываемый объекты. ДИ этого контейнера полагается общеизвестным. Считается также, что между внешними по отношению к выделенному контейнеру объектами возможны любые вызовы.
Выполнение ПРД контролируется монитором обращений.
При вызове метода мы будем разделять действия, производимые вызывающим объектом (инициация вызова) и вызываемым методом (прием и завершение вызова).
При инициации вызова может производиться преобразование ДИ фактических параметров к виду, доступному вызываемому методу ( "трансляция интерфейса" ). Трансляция может иметь место, если вызываемый объект не входит в тот же контейнер, что и вызывающий.
Параметры методов могут быть входными и/или выходными. При приеме вызова возникает информационный поток из входных параметров в вызываемый объект. В момент завершения вызова возникает информационный поток из вызываемого объекта в выходные параметры. Эти потоки могут фигурировать в правилах разграничения доступа.
Структурируем множество всех ПРД, выделив четыре группы правил:
- политика безопасности контейнера;
- ограничения на вызываемый метод;
- ограничения на вызывающий метод;
- добровольно налагаемые ограничения.
Правила, общие для всех объектов, входящих в контейнер C, назовем политикой безопасности данного контейнера.
Пусть метод M1 объекта O1 в точке P1 своего выполнения должен вызвать метод M объекта O. Правила, которым должен удовлетворять M, можно разделить на четыре следующие подгруппы:
- правила, описывающие требования к формальным параметрам вызова;
- правила, описывающие требования к семантике M;
- реализационные правила, накладывающие ограничения на возможные реализации M;
- правила, накладывающие ограничения на вызываемый объект O.
Метод M объекта O, потенциально доступный для вызова, может предъявлять к вызывающему объекту следующие группы требований:
- правила, описывающие требования к фактическим параметрам вызова;
- правила, накладывающие ограничения на вызывающий объект.
Можно выделить три разновидности предикатов, соответствующих семантике и/или особенностям реализации методов:
- утверждения о фактических параметрах вызова метода M в точке P1;
- предикат, описывающий семантику метода M;
- предикат, описывающий особенности реализации метода M.
Перечисленные ограничения можно назвать добровольными, поскольку они соответствуют реальному поведению объектов и не связаны с какими-либо внешними требованиями.
Предложенная постановка задачи разграничения доступа соответствует современному этапу развития программирования, она позволяет выразить сколь угодно сложную политику безопасности, найти баланс между богатством выразительных возможностей и эффективностью работы монитора обращений.