Упаковка связанных объектов в расширение

Полезное расширение QHB обычно включает несколько объектов SQL; например, для нового типа данных потребуются новые функции, новые операторы и, возможно, новые классы операторов индекса. Целесообразно собрать все эти объекты в один пакет, чтобы упростить управление базой данных. В QHB такой пакет называется расширением. Чтобы определить расширение, понадобится как минимум скрипт-файл, содержащий команды SQL для создания объектов расширения, и управляющий файл, задающий несколько основных свойств самого расширения. Если расширение содержит код на C/RUST, в него обычно также включается файл разделяемой библиотеки в котором был скомпилирован этот код. Как только вы предоставите все эти файлы, простая команда CREATE EXTENSION загрузит их в вашу базу данных.

При загрузке группы «слабосвязанных» объектов в вашу базу данных основное преимущество расширений перед обычным скриптом SQL заключается в том, что в первом случае QHB будет понимать, что объекты расширения связаны между собой. Все эти объекты можно удалить одной командой DROP EXTENSION (нет необходимости разрабатывать отдельный скрипт «uninstall» (деинсталляции)). Еще полезнее то, что утилита qhb_dump знает, что не должна выгружать отдельные компонентные объекты расширения — вместо этого она просто включит в дампы команду CREATE EXTENSION. Это значительно упрощает миграцию на новую версию расширения, которая может содержать дополнительные или другие объекты по сравнению со старой. Обратите внимание, однако, что при загрузке такого дампа в новую базу данных необходимо наличие управляющего файла, скрипта и других файлов расширения.

QHB не позволит вам удалить отдельный объект, содержащийся в расширении, кроме как при удалении всего расширения. Кроме того, хотя изменить определение компонентного объекта расширения и можно (например, с помощью команды CREATE OR REPLACE FUNCTION для функции), имейте в виду, что измененное определение не будет выгружено qhb_dump. Такое изменение обычно имеет смысл только в том случае, если вы одновременно вносите такое же изменение в файл скрипта расширения. (Но для таблиц, содержащих данные конфигурации, имеются специальные средства; см. подраздел Таблицы конфигурации расширений.) В производственных условиях для изменения компонентных объектов расширения обычно лучше создавать скрипт обновления расширения.

Скрипт расширения может устанавливать права доступа для объектов, являющихся частью расширения, с помощью команд GRANT и REVOKE. Окончательный набор прав для каждого объекта (если они установлены) будет сохранен в системном каталоге pg_init_privs. При использовании qhb_dump в дамп будет включена команда CREATE EXTENSION а затем набор операторов GRANT и REVOKE, необходимых, чтобы установить для объектов те же права, какие были на момент выгрузки.

В настоящее время QHB не поддерживает скрипты расширений, выполняющие операторы CREATE POLICY или SECURITY LABEL. Ожидается, что они будут установлены после создания расширения. В дампы, созданные qhb_dump, будут включены все политики RLS и метки безопасности на объектах расширения.

В механизме расширения также имеются средства поддержки для скриптов модификации упаковки, которые корректируют определения содержащихся в расширении объектов SQL. Например, если версия расширения 1.1 по сравнению с версией 1.0 добавляет одну функцию и изменяет тело другой, разработчик расширения может предоставить скрипт обновления, который вносит только эти два изменения. Затем, выполнив, команду ALTER EXTENSION UPDATE, можно применить эти изменения и отследить, какая версия расширения фактически установлена в заданной базе данных.

Виды объектов SQL, которые могут быть членами расширения, перечислены на справочной странице команды ALTER EXTENSION. В частности, объекты уровня кластера, такие как базы данных, роли и табличные пространства, не могут быть членами расширения, поскольку расширение известно только в пределах одной базе данных. (Хотя скрипту расширения и не запрещено создавать такие объекты, если он это сделает, они не будут отслеживаться как часть расширения.) Также обратите внимание, что хотя таблица может быть членом расширения, подконтрольные ей объекты, такие как индексы, не считаются непосредственными членами расширения. Другим важным моментом является то, что схемы могут принадлежать расширениям, но не наоборот: расширение как таковое имеет неполное имя и не существует «внутри» какой-либо схемы. Тем не менее компонентные объекты расширения будут принадлежать схемам, если это уместно для их типов. Самое расширение может иметь или не иметь оснований для того, чтобы владеть схемой (или схемами), в которой находятся его компонентные объекты.

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



Файлы расширений

Команда CREATE EXTENSION задействует для каждого расширения управляющий файл, который должен называться так же, как расширение, с суффиксом .control, и должен быть помещен в каталог SHAREDIR/extension. Также должен быть хотя бы один скрипт-файл SQL с именем, соответствующим шаблону расширение-- версия.sql (например foo--1.0.sql для версии 1.0 расширения foo). По умолчанию скрипт (или скрипты) также размещается в каталоге SHAREDIR/extension, но в управляющем файле для них можно указать и другой каталог.

Формат управляющего файла расширения тот же, что и у файла qhb.conf, а именно список назначений имя_параметра = значение, по одному на строку. Также допускаются пустые строки и комментарии, начинающиеся с #. Не забудьте взять в кавычки все значения, отличные от единственного слова или числа.

В управляющем файле может задавать следующие параметры:

directory (string)
Каталог, содержащий скрипт-файл(ы) SQL расширения. Если не задан абсолютный путь, имя относится к серверному каталогу SHAREDIR. Поведение по умолчанию равнозначно указанию directory = 'extension'.

default_version (string)
Версия расширения по умолчанию (та, которая будет установлена, если в CREATE EXTENSION не указана никакая версия). Хотя этот параметр можно опустить, но в отсутствие указания VERSION это приведет к сбою CREATE EXTENSION, поэтому обычно так делать не рекомендуется.

comment (string)
Комментарий (любая строка) о расширении. Комментарий применяется при изначальном создании расширения, но не при его обновлении (поскольку при этом могут замениться комментарии, добавленные пользователем). Кроме того, комментарий расширения можно задать, написав команду COMMENT в скрипт-файле.

encoding (string)
Кодировка набора символов, используемая в скрипт-файлах. Этот параметр следует указывать, если скрипт-файлы содержат символы, не относящиеся к ASCII. В противном случае предполагается, что в этих файлах используется кодировка базы данных.

module_pathname (string)
Значение этого параметра будет подставляться вместо каждого вхождения MODULE_PATHNAME в скрипт-файлах. Если этот параметр не задан, замена не производится. Как правило, для этого параметра устанавливается значение $libdir/имя_разделяемой_библиотеки, а затем в командах CREATE FUNCTION для функций на нативном языке используется MODULE_PATHNAME, поэтому скрипт- файлам не нужно жестко задавать имя разделяемой библиотеки.

requires (string)
Список имен расширений, от которых зависит это расширение, например requires = 'foo, bar'. Эти расширения должны быть установлены до того, как можно будет установить это расширение.

no_relocate (string)
Список имен расширение, от которых зависит это расширение и которым нужно запретить изменение своих схем посредством команды ALTER EXTENSION ... SET SCHEMA. Это необходимо, если скрипт этого расширения ссылается на имя требуемой схемы расширения (используя синтаксис @extschema:имя@) таким образом, что не может отслеживать переименования.

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

trusted (boolean)
Если этот параметр имеет значение true (это не значение по умолчанию), это позволяет некоторым обычными пользователям устанавливать расширение, для которого параметр superuser равен true. В частности, установка будет разрешена любому с правом CREATE в текущей базе данных. Когда пользователь, выполняющий CREATE EXTENSION, не является суперпользователем, но ему разрешена установка данного расширения посредством этого параметра, то скрипт установки или обновления запускается от имени инициализирующего суперпользователя, а не вызывающего пользователя. Этот параметр не имеет смысла, если superuser равен false. В целом, в этом параметре нельзя устанавливать true для расширений, которые могут открыть доступ к возможностям, иначе доступным только суперпользователям, например к файловой системе. Кроме того, если расширение помечается как доверенное, написание для него безопасных скриптов установки и обновления требует значительных дополнительных усилий; см. подраздел Примечания о безопасности расширений.

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

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

В дополнение к основному управляющему файлу расширение.control расширение может содержать добавочные управляющие файлы с именами вида расширение-- версия.control. Если они имеются, то должны находиться в каталоге скрипт-файлов. Добавочные управляющие файлы имеют тот же формат, что и основной управляющий файл. Любые параметры, установленные в добавочном управляющем файле, переопределяют параметры основного при установке этой версии расширения или обновлении до нее. Однако в добавочном управляющем файле нельзя установить параметры directory и default_version.

Скрипт-файлы SQL расширения могут содержать любые команды SQL, кроме команд управления транзакциями (BEGIN, COMMIT и т. д.) и команд, которые нельзя выполнить внутри блока транзакции (например VACUUM). Это связано с тем, что скрипт-файлы неявно выполняются внутри блока транзакции.

Скрипт-файлы SQL расширения также могут содержать строки, начинающиеся с \echo, которые будут игнорироваться (обрабатываться как комментарии) механизмом расширения. Это средство обычно используется для вывода ошибки, если скрипт-файл выполняется в psql, а не загружается командой CREATE EXTENSION (см. пример скрипта в подразделе Пример расширения). Без этого пользователи могут случайно загрузить содержимое расширения как «слабосвязанные» объекты, а не как собственно расширение — и получить состояние, которое несколько трудновато исправить.

Если скрипт расширения содержит строку @extowner@, эта строка заменяется именем (при необходимости заключенным в кавычки) пользователя, выполняющего команду CREATE EXTENSION or ALTER EXTENSION. Обычно эта функциональность используется расширениями, помеченными как доверенные, чтобы назначить владельцем выбранных объектов вызывающего пользователя, а не изначального суперпользователя. (Однако это следует делать с осторожностью. Например, назначение владельцем функции на нативном языке обычного пользователя, даст ему возможность повысить свои привилегии.)

Хотя скрипт-файлы могут содержать любые символы, разрешенные указанной кодировкой, управляющие файлы должны содержать только символы ASCII, поскольку QHB никак не может узнать кодировку управляющего файла. На практике вызывает сложности, только если вы хотите использовать символы не из ASCII в комментарии расширения. В этом случае рекомендуется не использовать параметр comment в управляющем файле, а вместо этого задать комментарий командой COMMENT ON EXTENSION в скрипт-файле.



Перемещаемость расширений

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

  • Полностью перемещаемое расширение можно переместить в другую схему в любое время, даже после его загрузки в базу данных. Это делается с помощью команды ALTER EXTENSION SET SCHEMA, которая автоматически переименовывает все компонентные объекты, перенося их в новую схему. Обычно это возможно только в том случае, если расширение не содержит внутренних предположений о том, в какой схеме находятся все его объекты. Кроме того, для начала все объекты расширения должны находиться в одной схеме (исключая объекты, которые не принадлежат никакой схеме, например процедурные языки). Отметьте расширение как полностью перемещаемое, задав relocatable = true в его управляющем файле.

  • Расширение может быть перемещаемым во время установки, но не после. Обычно это происходит, когда скрипт-файлу расширения нужно явно ссылаться на целевую схему, например, при настройке свойств search_path для функций SQL. Для такого расширения задайте relocatable = false в его управляющем файле и используйте псевдоимя @extschema@ для обращения к целевой схеме в скрипт-файле. Все вхождения этой строки будут заменены фактическим именем целевой схемы (при необходимости заключенным в кавычки) перед выполнением скрипта. Пользователь может задать целевую схему, используя параметр SCHEMA команды CREATE EXTENSION.

  • Если расширение вообще не поддерживает перемещение, установите в его управляющем файле relocatable = false, а также задайте в параметре schema имя предполагаемой целевой схемы. Это предотвратит использование параметра SCHEMA команды CREATE EXTENSION, если только в ней не указана та же схема, что и в управляющем файле. Этот выбор обычно необходим, если расширение содержит внутренние предположения об имени его схемы, которое нельзя заменить псевдоименем @extschema@. Механизм замещения @extschema@ доступен и в этом случае, хотя он будет иметь ограниченное применение, поскольку имя схемы определяется управляющим файлом.

Во всех случаях скрипт-файл будет выполняться с параметром search_path, изначально заданным для указания на целевую схему; то есть CREATE EXTENSION делает то же, что и:

SET LOCAL search_path TO @extschema@;

Это позволяет объектам, созданным скрипт-файлом, перейти в целевую схему. Скрипт файл может изменить search_path если пожелает, но обычно это нежелательно. search_path восстанавливает прежнее значение по завершении CREATE EXTENSION.

Целевая схема определяется параметром schema в управляющем файле, если он указан, либо параметром SCHEMA в команде CREATE EXTENSION, если он задан, либо текущей схемой создания объектов по умолчанию (первой в параметре search_path вызывающей функции). Когда используется параметр schema управляющего файла, целевая схема будет создана, если она еще не существует, но в двух других случаях она уже должна существовать.

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

В целях безопасности во всех случаях в конец search_path автоматически добавляется схема pg_temp.

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

Если расширение ссылается на объекты, принадлежащие другому расширению, рекомендуется дополнять эти ссылки именами схем. Для этого напишите @extschema:имя@ в файле скрипта расширения, где имя — это имя другого расширения (которое должно быть в списке requires этого расширения). Эта строка будет заменена именем (при необходимости заключенным в кавычки) целевой схемы этого расширения. Хотя эта нотация избавляет от необходимости делать аппаратно запрограмированные предположения об именах схем в файле скрипта расширения, при ее использовании имя схемы другого расширения может встраиваться в установленные объекты этого расширения. (Как правило, это происходит, когда @extschema:имя@ используется внутри строкового литерала, такого как тело функции или параметр search_path. В остальных случаях ссылка на объект сокращается до OID во время синтаксического анализа и не требует последующего поиска.) Если имя схемы другого расширения встраивается таким способом, следует запретить перемещение другого расширения после установки вашего путем добавления имени другого расширения в список no_relocate вашего расширения.



Таблицы конфигурации расширений

Некоторые расширения включают таблицы конфигурации, содержащие данные, которые могут быть добавлены или изменены пользователем после установки расширения. Обычно, если таблица является частью расширения, ни определение таблицы, ни ее содержимое не будут выгружены qhb_dump. Но такое поведение для таблицы конфигурации нежелательно; любые изменения данных, сделанные пользователем, должны быть включены в дампы, иначе после выгрузки и восстановления расширение будет вести себя по-другому.

Чтобы решить эту проблему, скрипт-файл расширения может пометить созданную им таблицу или последовательность как отношение конфигурации, что заставит qhb_dump включить в дампы содержимое таблицы или последовательности (но не ее определение). Для этого после создания таблицы или последовательности нужно вызвать функцию pg_extension_config_dump(regclass, text), например

CREATE TABLE my_config (key text, value text);
CREATE SEQUENCE my_config_seq;

SELECT pg_catalog.pg_extension_config_dump('my_config', '');
SELECT pg_catalog.pg_extension_config_dump('my_config_seq', '');

Таким образом можно пометить любое количество таблиц или последовательностей. Также можно пометить последовательности, связанные со столбцами serial или bigserial.

Когда второй аргумент pg_extension_config_dump является пустой строкой, с помощью qhb_dump выгружается все содержимое таблицы. Обычно это правильно только в том случае, если при создании скриптом расширения данная таблица изначально пуста. Если же в таблице содержится смесь исходных данных и данных, добавленных пользователем, то второй аргумент pg_extension_config_dump предоставляет условие WHERE, которое выбирает данные, подлежащие выгрузке. Например, можно создать таблицу

CREATE TABLE my_config (key text, value text, standard_entry boolean);

SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entry');

а затем сделать так, чтобы поле standard_entry содержало true только в строках, созданных скриптом расширения.

Для последовательностей второй аргумент pg_extension_config_dump не имеет никакого значения.

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

Условие фильтра, связанное с таблицей конфигурации, можно изменить, снова вызвав pg_extension_config_dump. (Обычно это может пригодиться в скрипте обновления расширения). Единственный способ пометить таблицу как больше не являющуюся таблицей конфигурации — это отвязать ее от расширения с помощью команды ALTER EXTENSION ... DROP TABLE.

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

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



Обновления расширений

Одним из преимуществ механизма расширения является то, что он предоставляет удобные способы управления обновлениями команд SQL, которые определяют объекты расширения. Это делается путем назначения имени или номера каждой выпущенной версии скрипта установки расширения. Кроме того, если вы хотите, чтобы пользователи могли динамически обновлять свои базы данных от одной версии до другой, нужно предоставить скрипты обновления, которые вносят необходимые изменения для перехода с одной версии на другую. Скриптам обновления назначаются имена вида расширение--старая_версия--целевая_версия.sql (например, foo--1.0--1.1.sql содержит команды для изменения версии расширения foo с 1.0 на 1.1).

При наличии подходящего скрипта обновления команда ALTER EXTENSION UPDATE обновит установленное расширение до указанной новой версии. Скрипт обновления выполняется в той же среде, которую команда CREATE EXTENSION предоставляет для скриптов установки: в частности, search_path устанавливается таким же образом, а любые новые объекты, созданные скриптом, автоматически добавляются в расширение. Кроме того, если скрипт решит удалить компонентные объекты расширения, они автоматически от него отсоединятся.

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

Чтобы достигнуть запрошенной версии, команда ALTER EXTENSION может выполнять последовательности скрипт-файлов обновления. Например, если доступны только foo--1.0--1.1.sql и foo--1.1--2.0.sql, ALTER EXTENSION будет применять их последовательно, если будет запрошено обновление до версии 2.0, а в данный момент установлена версия 1.0.

QHB не делает никаких предположений о свойствах имен версий: например, она не знает, следует ли 1.1 за 1.0. Она просто сопоставляет доступные имена версий и следует по пути, который требует применения наименьшего количества скриптов обновления. (Имя версии может быть фактически любой строкой, которая не содержит -- или не начинается и не заканчивается -.)

Иногда бывает полезно предоставить скрипты «понижения версии», например foo- 1.1--1.0.sql, чтобы получить возможность отменить изменения, внесенные версией 1.1. Если вы их предоставляете, остерегайтесь вероятности неожиданного применения такого скрипта, если окажется, что он обеспечивает более короткий путь. Рискованная ситуация возникает тогда, когда имеется скрипт обновления «короткого пути», который перепрыгивает через несколько версий, и скрипт понижения версии до начальной точки первого скрипта. Возможно, для понижения версии с последующим обновлением по короткому пути потребуется меньше шагов, чем для последовательного повышения версии. Если скрипт понижения версии удалит какие-либо незаменимые объекты, это приведет к нежелательным результатам.

Чтобы проверить наличие неожиданных путей обновления, используйте эту команду:

SELECT * FROM pg_extension_update_paths('имя_расширения');

Она показывает каждую пару различных известных имен версий для указанного расширения, а также последовательность пути обновления, которая будет принята для перехода от исходной версии до целевой, или NULL, если доступного пути обновления нет. Путь выводится в текстовом виде с разделителями --. Если вы предпочитаете формат массива, можно использовать regexp_split_to_array(path,'--').



Установка расширений с помощью скриптов обновления

Расширение, существующее уже некоторое время, вероятно, будет иметь несколько версий, для которых автору потребуется написать скрипт обновления. Например, если вы выпустили расширение foo в версиях 1.0, 1.1 и 1.2, для них понадобятся скрипты обновления foo--1.0--1.1.sql и foo--1.1--.2.sql. Прежде нужно было также создавать новые скрипт-файлы foo--.1.sql и foo--1.2.sql, которые напрямую собирали более новые версии расширений, иначе те можно было установить только путем установки 1.0 с последующим обновлением. Это было утомительно и требовало многократных повторений, но теперь в этом нет необходимости, поскольку команда CREATE EXTENSION способна автоматически следовать цепочкам обновлений. Например, если доступны только скрипт-файлы foo--1.0.sql, foo--1.0--1.1.sql и foo--1.1--1.2.sql, то запрос на установку версии 1.2 выполняется с помощью последовательного запуска этих трех скриптов. Процесс будет проходить так же, как если бы вы сначала установили версию 1.0, а затем обновили ее до 1.2. (Как и в случае с ALTER EXTENSION UPDATE, если доступно несколько путей, предпочтение отдается кратчайший путь.) Размещение скрипт-файлов расширения по такому образцу может снизить трудозатраты на выпуск небольших обновлений.

Если вы используете добавочные (ориентированные на версию) управляющие файлы для расширения, поддерживаемого по этому образцу, имейте в виду, что управляющий файл нужен каждой версии, даже если у нее нет отдельного скрипта установки, поскольку этот управляющий файл будет определять, как именно выполняется неявное обновление до эту версии. Например, если в файле foo--1.0.control задается requires = 'bar', а в других управляющих файлах foo этого не происходит, зависимость расширения от bar будет сброшена при обновлении с версии 1.0 до другой.



Примечания о безопасности расширений

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

Расширение, у которого свойство superuser имеет значение true, также должно быть защищено от угроз безопасности, связанных с действиями, которые происходят в скриптах установки и обновления этого расширения. Злонамеренному пользователю не составит особого труда создать объекты типа троянского коня, которые впоследствии скомпрометируют выполнение небрежно написанного скрипта, позволив этому пользователю получить права суперпользователя.

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

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


Примечания о безопасности функций в расширениях

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

Справочная страница команды CREATE FUNCTION содержит совет относительно безопасного написания функций с характеристикой SECURITY DEFINER. Имеет смысл применять эти методики и для всех функций, предоставляемых расширением, так как эти функции может вызвать пользователь с расширенными правами.

Если вы не можете настроить search_path так, чтобы он содержал только безопасные схемы, считайте, что каждое неполное имя может быть преобразовано в объект, определенный злонамеренным пользователем. Избегайте конструкций, явно зависящих от search_path; например, IN и CASE выражение WHEN всегда выбирают оператор по пути поиска. Вместо них используйте OPERATOR(схема.=) ANY и CASE WHEN выражение.

Обычно расширение общего назначения не должно рассчитывать на то, что оно будет установлено в безопасной схеме, что означает, что даже уточненные схемой ссылки на его собственные объекты не вполне безопасны. Например, если в расширении есть функция myschema.myfunc(bigint), то вызовы вроде myschema.myfunc(42) могут быть перехвачены вредоносной функцией myschema.myfunc(integer). Позаботьтесь о том, чтобы типы данных функции и параметров оператора точно соответствовали объявленным типам их аргументов, и при необходимости используйте явные приведения.


Примечания о безопасности скриптов в расширениях

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

Команды DDL, например CREATE FUNCTION и CREATE OPERATOR CLASS, в целом безопасны, но избегайте любых команд, содержащих расширения общего назначения. Например, требуют проверки команда CREATE VIEW, а также выражение DEFAULT в команде CREATE FUNCTION.

Иногда скрипту расширения может понадобиться выполнить SQL общего назначения, например, чтобы внести в каталог изменения, невозможные через DDL. Обязательно выполняйте такие команды с безопасным search_path; не полагайтесь на защищенность пути, установленного при выполнении CREATE/ALTER EXTENSION. Наилучшим выходом будет временно задать в search_path значение pg_catalog, pg_temp и везде, где потребуется, добавить явные указания схемы, в которую устанавливается расширение. (Этот прием также может быть полезен при создании представлений.) Примеры можно найти в модулях share/extension в дистрибутиве исходного кода QHB.

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



Пример расширения

Вот полный пример расширения, созданного исключительно средствами SQL, составного типа с двумя элементами, который может хранить в своих слотах, названных «k» и «v», значения любого типа. Для хранения все нетекстовые значения автоматически преобразуются в текст.

Скриптовый файл pair--1.0.sql выглядит так:

-- выразит недовольство, если скрипт получен из psql, а не посредством команды CREATE EXTENSION
\echo Use "CREATE EXTENSION pair" to load this file. \quit
-- (Используйте "CREATE EXTENSION pair", чтобы загрузить этот файл )

CREATE TYPE pair AS ( k text, v text );

CREATE OR REPLACE FUNCTION pair(text, text)
RETURNS pair LANGUAGE SQL AS 'SELECT ROW($1, $2)::@extschema@.pair;';

CREATE OPERATOR ~> (LEFTARG = text, RIGHTARG = text, FUNCTION = pair);

-- "SET search_path" легче корректировать, но полные имена работают лучше.
CREATE OR REPLACE FUNCTION lower(pair)
RETURNS pair LANGUAGE SQL
AS 'SELECT ROW(lower($1.k), lower($1.v))::@extschema@.pair;'
SET search_path = pg_temp;

CREATE OR REPLACE FUNCTION pair_concat(pair, pair)
RETURNS pair LANGUAGE SQL
AS 'SELECT ROW($1.k OPERATOR(pg_catalog.||) $2.k,
               $1.v OPERATOR(pg_catalog.||) $2.v)::@extschema@.pair;';

Управляющий файл pair.control выглядит так:

# pair extension
comment = 'A key/value pair data type' /* Тип данных для пары ключ/значение */
default_version = '1.0'
# cannot be relocatable because of use of @extschema@
/* не может быть перемещаемым, так как использует @extschema@ */
relocatable = false

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

EXTENSION = pair
DATA = pair--1.0.sql

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

Этот сборочный файл опирается на инфраструктуру PGXS, которая описывается в следующем подразделе. Команда make install установит управляющие файлы и скрипт-файлы в правильный каталог, который определит pg_config.

Как только файлы будут установлены, используйте команду CREATE EXTENSION, чтобы загрузить объекты в ту или иную базу данных.