2015-08-25 2 views
0

У меня есть несколько сценариев, которые экспортируют один и тот же интерфейс, и они выполняются с использованием execfile() в изолированной области.Поделитесь ресурсами плагина с реализованными правилами разрешений

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

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

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

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

Пример:

class SharedResources: 
    # Here should be a shared resource manager that I tried to write 
    # but got stuck. That's why I ask this long and convoluted question! 
    # Some beginning: 
    def __init__ (self, owner): 
     self.owner = owner 

    def __call__ (self): 
     # Here we should return some object that will do 
     # required stuff. Read more for details. 
     pass 

class plugin (dict): 
    def __init__ (self, filename): 
     dict.__init__(self) 
     # Here some checks and filling with secure versions of __builtins__ etc. 
     # ... 
     self["__name__"] = "__main__" 
     self["__file__"] = filename 
     # Add a shared resources manager to this plugin 
     self["SharedResources"] = SharedResources(filename) 
     # And then: 
     execfile(filename, self, self) 

    # Expose the plug-in interface to outside world: 
    def __getattr__ (self, a): 
     return self[a] 
    def __setattr__ (self, a, v): 
     self[a] = v 
    def __delattr__ (self, a): 
     del self[a] 
    # Note: I didn't use self.__dict__ because this makes encapsulation easier. 
    # In future I won't use object itself at all but separate dict to do it. For now let it be 

---------------------------------------- 
# An example of two scripts that would use shared resource and be run with plugins["name"] = plugin("<filename>"): 
# Presented code is same in both scripts, what comes after will be different. 

def loadSomeResource(): 
    # Do it here... 
    return loadedresource 

# Then Load this resource if it's not already loaded in shared resources, if it isn't then add loaded resource to shared resources: 
shr = SharedResources() # This would be an instance allowing access to shared resources 
if not shr.has_key("Default Resources"): 
    shr.create("Default Resources") 
if not shr["Default Resources"].has_key("SomeResource"): 
    shr["Default Resources"].add("SomeResource", loadSomeResource()) 
resource = shr["Default Resources"]["SomeResource"] 
# And then we use normally resource variable that can be any object. 
# Here I Used category "Default Resources" to add and/or retrieve a resource named "SomeResource". 
# I want more categories so that plugins that deal with audio aren't mixed with plug-ins that deal with video for instance. But this is not strictly needed. 
# Here comes code specific for each plug-in that will use shared resource named "SomeResource" from category "Default Resources". 
... 
# And end of plugin script! 
---------------------------------------- 

# And then, in main program we load plug-ins: 
import os 
plugins = {} # Here we store all loaded plugins 
for x in os.listdir("plugins"): 
    plugins[x] = plugin(x) 

Пусть говорят, что наши скрипты хранятся в директории плагинов и оба используют некоторые WAVE файлы, загруженные в память. Плагин, который загружает сначала, загрузит WAVE и поместит его в ОЗУ. Другой плагин сможет получить доступ к уже загруженной WAVE, но не заменять или удалять его, тем самым возиться с другим плагином.

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

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

Я почти сделал это, а затем застрял, и моя голова пронизана понятиями, которые, когда реализованы, делают вещь, но только частично. Это ест меня, поэтому я больше не могу концентрироваться. Любое предложение более чем приветствуется!

Добавление:

Это то, что я использую в настоящее время без каких-либо безопасности включены:

# Dict that will hold a category of resources (should implement some security): 
class ResourceCategory (dict): 
    def __getattr__ (self, i): return self[i] 
    def __setattr__ (self, i, v): self[i] = v 
    def __delattr__ (self, i): del self[i] 

SharedResources = {} # Resource pool 

class ResourceManager: 
    def __init__ (self, owner): 
     self.owner = owner 

    def add (self, category, name, value): 
     if not SharedResources.has_key(category): 
      SharedResources[category] = ResourceCategory() 
     SharedResources[category][name] = value 

    def get (self, category, name): 
     return SharedResources[category][name] 

    def rem (self, category, name=None): 
     if name==None: del SharedResources[category] 
     else: del SharedResources[category][name] 

    def __call__ (self, category): 
     if not SharedResources.has_key(category): 
      SharedResources[category] = ResourceCategory() 
     return SharedResources[category] 

    __getattr__ = __getitem__ = __call__ 

    # When securing, this must not be left as this, it is unsecure, can provide a way back to SharedResources pool: 
    has_category = has_key = SharedResources.has_key 

Теперь плагин капсулы:

class plugin(dict): 
    def __init__ (self, path, owner): 
     dict.__init__() 
     self["__name__"] = "__main__" 
     # etc. etc. 
     # And when adding resource manager to the plugin, register it with this plugin as an owner 
     self["SharedResources"] = ResourceManager(owner) 
     # ... 
     execfile(path, self, self) 
     # ... 

Пример плагина сценария:

#----------------------------------- 
# Get a category we want. (Using __call__()) Note: If a category doesn't exist, it is created automatically. 
AudioResource = SharedResources("Audio") 
# Use an MP3 resource (let say a bytestring): 
if not AudioResource.has_key("Beep"): 
    f = open("./sounds/beep.mp3", "rb") 
    Audio.Beep = f.read() 
    f.close() 
# Take a reference out for fast access and nicer look: 
beep = Audio.Beep # BTW, immutables doesn't propagate as references by themselves, doesn't they? A copy will be returned, so the RAM space usage will increase instead. Immutables shall be wrapped in a composed data type. 

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

Я бы хотел, чтобы экземпляр ResourceManager() был ответственным за возвращение какой версии хранимых данных.

+0

Вы доверяете авторам плагинов не быть злыми? Если вы не можете доверять авторам, было показано, что безопасное использование eval/exec/execfile в большинстве случаев невозможно. См. Здесь: http://programmers.stackexchange.com/a/191628 и/или google для «python exec untrusted». Если вы доверяете авторам плагинов не пытаться обойти вашу песочницу, тогда вы можете создать систему, которая предотвратит случайное нарушение общих ресурсов и/или выполнение произвольных действий. –

+0

Я им не доверяю, но я могу игнорировать то, что делают люди на своем компьютере. Что касается совместного использования, я проверю все новые плагины, прежде чем предоставлять их для установки с сервера менеджером плагинов. И нет, невозможно сделать полностью безопасную песочницу. Существовал модуль rexec, который был как можно более безопасным, но он уже не разработан и теперь считается небезопасным. Но концепция в порядке, и ее можно улучшить, чтобы быть полностью безопасной. – Dalen

+0

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

ответ

1

Итак, мой общий подход был бы таким.

  1. Имейте центральный пул общих ресурсов. Доступ через этот пул будет доступен только для чтения. Оберните все данные в общий пул, чтобы никто «играя по правилам» не мог редактировать что-либо в нем.

  2. Каждый агент (плагин) сохраняет информацию о том, что он «владеет» в момент его загрузки. Он сохраняет ссылку на чтение и запись для себя и регистрирует ссылку на ресурс в централизованный пул только для чтения.

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

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

import collections as _col 
import sys 

if sys.version_info >= (3, 0): 
    immutable_scalar_types = (bytes, complex, float, int, str) 
else: 
    immutable_scalar_types = (basestring, complex, float, int, long) 

# calling this will circumvent any control an object has on its own attribute lookup 
getattribute = object.__getattribute__ 

# types that will be safe to return without wrapping them in a proxy 
immutable_safe = immutable_scalar_types 

def add_immutable_safe(cls): 
    # decorator for adding a new class to the immutable_safe collection 
    # Note: only ImmutableProxyContainer uses it in this initial 
    # implementation 
    global immutable_safe 
    immutable_safe += (cls,) 
    return cls 

def get_proxied(proxy): 
    # circumvent normal object attribute lookup 
    return getattribute(proxy, "_proxied") 

def set_proxied(proxy, proxied): 
    # circumvent normal object attribute setting 
    object.__setattr__(proxy, "_proxied", proxied) 

def immutable_proxy_for(value): 
    # Proxy for known container types, reject all others 
    if isinstance(value, _col.Sequence): 
     return ImmutableProxySequence(value) 
    elif isinstance(value, _col.Mapping): 
     return ImmutableProxyMapping(value) 
    elif isinstance(value, _col.Set): 
     return ImmutableProxySet(value) 
    else: 
     raise NotImplementedError(
      "Return type {} from an ImmutableProxyContainer not supported".format(
       type(value))) 

@add_immutable_safe 
class ImmutableProxyContainer(object): 

    # the only names that are allowed to be looked up on an instance through 
    # normal attribute lookup 
    _allowed_getattr_fields =() 

    def __init__(self, proxied): 
     set_proxied(self, proxied) 

    def __setattr__(self, name, value): 
     # never allow attribute setting through normal mechanism 
     raise AttributeError(
      "Cannot set attributes on an ImmutableProxyContainer") 

    def __getattribute__(self, name): 
     # enforce attribute lookup policy 
     allowed_fields = getattribute(self, "_allowed_getattr_fields") 
     if name in allowed_fields: 
      return getattribute(self, name) 
     raise AttributeError(
      "Cannot get attribute {} on an ImmutableProxyContainer".format(name)) 

    def __repr__(self): 
     proxied = get_proxied(self) 
     return "{}({})".format(type(self).__name__, repr(proxied)) 

    def __len__(self): 
     # works for all currently supported subclasses 
     return len(get_proxied(self)) 

    def __hash__(self): 
     # will error out if proxied object is unhashable 
     proxied = getattribute(self, "_proxied") 
     return hash(proxied) 

    def __eq__(self, other): 
     proxied = get_proxied(self) 
     if isinstance(other, ImmutableProxyContainer): 
      other = get_proxied(other) 
     return proxied == other 


class ImmutableProxySequence(ImmutableProxyContainer, _col.Sequence): 

    _allowed_getattr_fields = ("count", "index") 

    def __getitem__(self, index): 
     proxied = get_proxied(self) 
     value = proxied[index] 
     if isinstance(value, immutable_safe): 
      return value 
     return immutable_proxy_for(value) 


class ImmutableProxyMapping(ImmutableProxyContainer, _col.Mapping): 

    _allowed_getattr_fields = ("get", "keys", "values", "items") 

    def __getitem__(self, key): 
     proxied = get_proxied(self) 
     value = proxied[key] 
     if isinstance(value, immutable_safe): 
      return value 
     return immutable_proxy_for(value) 

    def __iter__(self): 
     proxied = get_proxied(self) 
     for key in proxied: 
      if not isinstance(key, immutable_scalar_types): 
       # If mutable keys are used, returning them could be dangerous. 
       # If owner never puts a mutable key in, then integrity should 
       # be okay. tuples and frozensets should be okay as keys, but 
       # are not supported in this implementation for simplicity. 
       raise NotImplementedError(
        "keys of type {} not supported in " 
        "ImmutableProxyMapping".format(type(key))) 
      yield key 


class ImmutableProxySet(ImmutableProxyContainer, _col.Set): 

    _allowed_getattr_fields = ("isdisjoint", "_from_iterable") 

    def __contains__(self, value): 
     return value in get_proxied(self) 

    def __iter__(self): 
     proxied = get_proxied(self) 
     for value in proxied: 
      if isinstance(value, immutable_safe): 
       yield value 
      yield immutable_proxy_for(value) 

    @classmethod 
    def _from_iterable(cls, it): 
     return set(it) 

Примечание: это проверяется только на Python 3.4, но я пытался написать ее, чтобы быть совместимым как с Python 2 и 3.

Сделать корень из общих ресурсов словаря. Дайте ImmutableProxyMapping этого словаря плагинам.

private_shared_root = {} 
public_shared_root = ImmutableProxyMapping(private_shared_root) 

Создание API, где модули могут регистрировать новые ресурсы для public_shared_root, вероятно, на первый пришел-первый обслужен (если она уже есть, вы не можете зарегистрировать его). Предварительно заполните private_shared_root с любыми контейнерами, которые вам понадобятся, или любыми данными, которые вы хотите разделить со всеми плагинами, но вы знаете, что хотите быть доступными только для чтения.

Возможно, было удобно, если конвенцией для ключей в общем корневом сопоставлении были все строки, такие как пути к файловой системе (/home/dalen/local/python) или пунктирные пути, такие как объекты библиотеки python (os.path.expanduser). Таким образом, обнаружение столкновения является непосредственным и тривиальным/очевидным, если плагины пытаются добавить один и тот же ресурс в пул.

+0

Отлично! Теперь я добавил свое текущее решение (без защиты) к Q. Возможно, вы можете посоветовать лучший способ интегрировать свое решение в мое. Пусть останутся типы данных (int, float, bool, long, str, unicode, tuple, list, dict, set и read only file), для простоты. Поскольку в моем случае ресурсы - это только данные, нет драйверов доступа или подобных материалов. См. Если вы можете устранить два пула ресурсов, это будет хорошо. Иначе это не имеет большого значения. – Dalen

+0

И, как раз из любопытства, как бы вы начали взломать объект, который уже определил __getattribute __() и __setattr __()? Хм, не используя его класс для повторной инициализации, я не вижу другого способа в данный момент. – Dalen

+0

О, да, я полностью забыл, я использую Python 2.5 до 2.7, но не беспокойтесь, всего нескольких модификаций будет достаточно, чтобы использовать 3.4 в 2.x. Это не проблема. – Dalen