Поделиться ресурсами плагина с реализованными правилами разрешений
У меня есть несколько сценариев, которые экспортируют один и тот же интерфейс, и они выполняются с использованием 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 и поместит его в RAM. Другой плагин сможет получить доступ к уже загруженному 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() отвечал, кому возвращать какую версию хранимых данных.
1 ответ
Итак, мой общий подход был бы следующим.
Иметь центральный общий пул ресурсов. Доступ через этот пул будет доступен только для чтения всем. Оберните все данные в общий пул, чтобы никто, "играя по правилам", не мог что-либо редактировать в нем.
Каждый агент (плагин) хранит информацию о том, что ему "принадлежит" во время его загрузки. Он сохраняет для себя ссылку на чтение / запись и регистрирует ссылку на ресурс в централизованном пуле только для чтения.
Когда плагин загружен, он получает ссылку на центральный пул только для чтения, с которым он может регистрировать новые ресурсы.
Таким образом, для решения только собственной структуры данных Python (а не экземпляров пользовательских классов) достаточно закрытой системы реализаций, доступных только для чтения, заключается в следующем. Обратите внимание, что уловки, которые используются для их блокировки, - это те же уловки, которые кто-то может использовать для обхода замков, поэтому песочница очень слаба, если кто-то с небольшим знанием Python активно пытается ее взломать.
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
). Таким образом, обнаружение столкновений происходит сразу и тривиально / очевидно, если плагины пытаются добавить один и тот же ресурс в пул.