PHP: полный мануал с примерами, функциями и ООП
Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Дерево руководства
Глава 73 · После подключения, SQL-запросов, транзакций и административного API Firebird
Firebird: доступ к каталогу, роли, отзыв прав и изменение отдельных столбцов
Четыре сценария с реальными данными: читающая учётная запись, явная роль, отзыв SELECT и изменение только количества.
Войти в базу и получить разрешение — разные шаги
Каталог читает витрина, остатки меняет кладовщик, а названия товаров редактирует другой сотрудник. Каждому нужен свой набор возможностей. Создание логина ещё не разрешает читать таблицу; выдача роли ещё не означает, что обычная роль включена в текущем подключении. Здесь настроим эти переходы и увидим реальные Cable — 3, Clamp — 0, а после разрешённого изменения Cable — 9.
- Учётная запись и пароль → сервер узнаёт пользователя
- Выбранная прикладная база → проверяются её SQL-привилегии
- Необязательная активная роль добавляет выданные ей права
- SELECT читает разрешённые данные; UPDATE проверяется отдельно
- Только успешная запись + commit меняет товар; отказ сохраняется как отказ
Удобная аналогия — пропуск в здание и ключи от кабинетов. Проверка пароля соответствует пропуску, GRANT SELECT — разрешению читать определённый каталог, роль — именованному набору ключей. Граница аналогии: права SQL могут приходить несколькими путями одновременно, а роль не является отдельным пользователем с паролем. Снятие одного разрешения не отменяет такое же разрешение, полученное через другую роль или PUBLIC.
Сначала нужны подключение и простые SELECT/UPDATE, commit и rollback и снятие ошибки до следующего запроса. Текущая тема — что разрешено уже вошедшему пользователю. Создание, изменение пароля и удаление самой учётной записи рассматриваются в соседней главе административного API.
| Уровень | Что отвечает на вопрос |
|---|---|
| Вход по TCP | Приняты ли имя и пароль этой серверной средой? |
| SELECT на LAB_ITEMS | Разрешено ли читать каталог в этой прикладной базе? |
| UPDATE(QTY) | Можно ли менять именно столбец количества? Это ещё не ограничение по строкам. |
| Роль LAB_READER | Какой общий набор прав доступен выбранному пользователю и активен ли он сейчас? |
| Авторизация сайта | Имеет ли посетитель сайта право выполнять действие? Один SQL-логин приложения сам по себе не различает посетителей. |
Проверка проводится настоящими подключениями к отдельному серверу на 127.0.0.1 с собственной security database. Embedded-доступ без сервера не доказывает правильность пароля: в Firebird embedded аутентификация устроена иначе и зависит от доступа к файлу. Обычная учётная запись не является владельцем учебной FDB, SYSDBA или RDB$ADMIN, поэтому её отказам можно придать проверяемый смысл. Пароли не попадают в публичный отчёт. Случайные имена собственных объектов сохраняются только как свидетельство их создания и удаления; таблица результата показывает назначение пользователя в задаче и реальные данные.
Прочитать GRANT, роль и REVOKE по частям
SQL-разрешение выдаёт владелец объекта или другой уполномоченный администратор. Команда GRANT — это изменение метаданных выбранной прикладной базы. Она не меняет пароль, не создаёт HTTP-сессию и не исполняет SELECT за получателя.
GRANT SELECT ON TABLE LAB_ITEMS TO USER SHOP_READER;
COMMIT;| Элемент | Значение |
|---|---|
| GRANT SELECT | Разрешить чтение. UPDATE, INSERT и DELETE этим не выдаются. |
| ON TABLE LAB_ITEMS | Права относятся к конкретной таблице текущей базы. |
| TO USER SHOP_READER | Получатель — именно пользователь, а не одноимённая роль. |
| COMMIT | Подтвердить изменение; незавершённую административную транзакцию нельзя принимать за окончательный результат для других подключений. |
SHOP_READER здесь понятное имя в коротком фрагменте. Самостоятельный проект создаёт уникальную собственную учётную запись. Имена таблиц, ролей и пользователей являются SQL-идентификаторами: вместо них нельзя поставить обычный placeholder ?. Пример берёт их только из своего проверенного набора и не склеивает GRANT из произвольного ввода посетителя. Значения в пользовательских запросах передаются параметрами: порядок параметров и границы SQL.
GRANT SELECT ON TABLE LAB_ITEMS TO ROLE LAB_READER;
GRANT LAB_READER TO USER SHOP_READER;
COMMIT;Здесь две разные связи: таблица отдаёт SELECT роли, а пользователь получает право выбрать эту роль. Первое GRANT не назначает роль человеку. Второе не выдаёт автоматически новые права самой роли. Для обычной роли её имя указывается при подключении; в PHP это седьмой аргумент ibase_connect после charset, buffers и dialect. Firebird4+ также поддерживает DEFAULT-роли, включаемые автоматически: они не используются в наших опытах, чтобы два состояния были видны.
REVOKE SELECT ON TABLE LAB_ITEMS FROM USER SHOP_READER;
COMMIT;REVOKE снимает указанное разрешение соответствующего источника выдачи. Оно не удаляет пользователя, не возвращает изменённые ранее данные и не гарантирует разрыв его старого соединения. Если SELECT всё ещё доступен, проверяют другие прямые выдачи, активные и DEFAULT-роли, PUBLIC и полномочия администратора. Для чистой проверки нашего примера остаётся единственный путь доступа; состояние старого соединения наблюдается отдельно, затем создаётся новое.
| Операция | Граница |
|---|---|
| GRANT SELECT | На уровне таблицы позволяет читать её строки и столбцы; WHERE в отдельном запросе не превращается в правило доступа. |
| GRANT UPDATE(QTY) | Разрешает менять указанный столбец, а не только выбранный товар. |
| WITH GRANT OPTION | Позволяет получателю передавать выданные объектные права дальше. В примерах не выдаётся. |
| WITH ADMIN OPTION для роли | Позволяет передавать членство в роли. Это не то же самое, что право UPDATE. |
| REVOKE GRANT OPTION FOR / ADMIN OPTION FOR | Снимает соответствующее право делегирования; не следует считать такой отзыв удалением самого SELECT или членства. |
| PUBLIC | Особый получатель, охватывающий пользователей; право, выданное PUBLIC, не снимается обычным REVOKE у одного пользователя. |
Разрешить витрине чтение каталога, сохранив запрет изменения
Сервис витрины должен показывать товары и остатки, но не менять их случайным UPDATE. Сначала проверим, что обычный пользователь действительно вошёл по TCP. Без прав он ещё не читает LAB_ITEMS. Затем администратор выдаёт только SELECT: витрина показывает Cable — 3 и Clamp — 0, а попытка записать 9 отклоняется.
- Пользователь проходит вход → CURRENT_USER совпадает с созданной учётной записью
- SELECT LAB_ITEMS без прав → отказ -551
- Администратор: GRANT SELECT + commit
- Новое пользовательское соединение читает конкретные два товара
- UPDATE не выдан → отказ -551; Cable остаётся 3
Минимальная команда и её части
GRANT SELECT ON TABLE LAB_ITEMS TO USER SHOP_READER;
COMMIT;GRANT здесь выполняет администратор в прикладной базе. SELECT — одно разрешение, LAB_ITEMS — объект, SHOP_READER — получатель. Действие проверяется отдельным пользовательским подключением. $identity содержит результат CURRENT_USER, а $catalogue — настоящие строки выбранных ID, TITLE и QTY. Для отказа используется строгое сравнение с false и сразу сохраняется код; успешный вход не подменяет успех SQL-запроса.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Обычный пользователь вошёл | да |
| SELECT до GRANT разрешён | нет |
| Код первого отказа | -551 |
| После GRANT: товар 1 | Cable — 3 |
| После GRANT: товар 2 | Clamp — 0 |
| UPDATE при одном SELECT разрешён | нет |
| Код отказа UPDATE | -551 |
| После отказа Cable сохранил количество | 3 |
Полная программа, файлы лаборатории и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$owned=[];$removed=[];$db=$service=null;
try {
$config=f160Config();$service=f160Service();$runtime=f160Runtime($service);
$name=f160CreateDatabase($service,$owned,'reader');
$user=f160NewUser($owned,'reader');$password=f160Password();
f160AddUser($service,$user,$password);
$userDb=null;
try {
$userDb=f160ConnectUser($name,$user,$password);
$identity=f160Read($userDb,'SELECT CURRENT_USER AS LOGIN_NAME FROM RDB$DATABASE');
$deniedRead=f160Capture(static fn()=>ibase_query($userDb,'SELECT ID,TITLE,QTY FROM LAB_ITEMS ORDER BY ID'));
f160Close($userDb);
$db=f160Connect($name);
f160Command($db,'GRANT SELECT ON TABLE LAB_ITEMS TO USER '.f160UserIdentifier($user));
f160Commit($db);f160Close($db);
$userDb=f160ConnectUser($name,$user,$password);
$catalogue=f160Read($userDb,'SELECT ID,TITLE,QTY FROM LAB_ITEMS ORDER BY ID');
$deniedWrite=f160Capture(static fn()=>ibase_query($userDb,'UPDATE LAB_ITEMS SET QTY=9 WHERE ID=1'));
$after=f160Read($userDb,'SELECT ID,TITLE,QTY FROM LAB_ITEMS ORDER BY ID');
f160Close($userDb);
} finally {
if(is_resource($userDb))f160Close($userDb);
}
$expected=[['ID'=>1,'TITLE'=>'Cable','QTY'=>3],['ID'=>2,'TITLE'=>'Clamp','QTY'=>0]];
$rows=[['Обычный пользователь вошёл',$identity[0]['LOGIN_NAME']===$user],
['SELECT до GRANT разрешён',$deniedRead['value']!==false],['Код первого отказа',$deniedRead['code']],
['После GRANT: товар 1',$catalogue[0]['TITLE'].' — '.$catalogue[0]['QTY']],
['После GRANT: товар 2',$catalogue[1]['TITLE'].' — '.$catalogue[1]['QTY']],
['UPDATE при одном SELECT разрешён',$deniedWrite['value']!==false],['Код отказа UPDATE',$deniedWrite['code']],
['После отказа Cable сохранил количество',$after[0]['QTY']]];
$checks=['ordinary_login_verified'=>$identity===[['LOGIN_NAME'=>$user]],
'select_initially_refused'=>$deniedRead['value']===false && $deniedRead['code']===-551 && $deniedRead['warning'] && $deniedRead['message_present'],
'granted_select_reads_catalogue'=>$catalogue===$expected,
'update_still_refused'=>$deniedWrite['value']===false && $deniedWrite['code']===-551 && $deniedWrite['warning'] && $deniedWrite['message_present'],
'catalogue_unchanged'=>$after===$expected];
$observations=['ordinary_user_identity_matches'=>$identity[0]['LOGIN_NAME']===$user,'initial_select'=>$deniedRead,
'catalogue'=>$catalogue,'update_refusal'=>$deniedWrite,'after'=>$after,
'permission_path'=>'direct SELECT only; no role or PUBLIC grant'];
} finally {
if(is_resource($db)){f160Close($db);}
if(is_resource($service)){f160Detach($service);}
$removed=f160Cleanup($owned);
}
f160Report('fb160-select-permission','Разрешить витрине чтение каталога, сохранив запрет изменения',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Обычный пользователь вошёл — да
SELECT до GRANT разрешён — нет
Код первого отказа — -551
После GRANT: товар 1 — Cable — 3
После GRANT: товар 2 — Clamp — 0
UPDATE при одном SELECT разрешён — нет
Код отказа UPDATE — -551
После отказа Cable сохранил количество — 3
Распакуйте все файлы архива в отдельный каталог. README.txt описывает отдельный TCP-сервер с собственной security database; embedded-подключение не заменяет этот опыт. example.php создаёт одну собственную FDB с каталогом Cable/3 и Clamp/0, одного обычного пользователя и необходимые выдачи прав. lab.php отделяет административные операции от пользовательского SQL, проверяет владельца и удаляет только зарегистрированные учебные объекты. result.html содержит реальные товары и отказы, result.json — наблюдения, metrics.json — проверки и завершение уборки. Пароли не печатаются.
Открыть таблицу фактического результата · Открыть наблюдения JSON.
Ограничения и выбор другого решения
Пользователь не является владельцем таблицы или SYSDBA: иначе его успех не доказывал бы влияние GRANT. SELECT позволяет читать таблицу, но не разрешает UPDATE, INSERT или DELETE. Это не перевод всей FDB в read-only и не запрет записи другим пользователям. Пароль не выводится в отчёте. В приложении также проверяют посетителя сайта: если разные посетители работают под одним SQL-логином, база видит один и тот же набор прав приложения.
Человек видит пользователя в списке учётных записей, но не понимает, где находятся разрешения на таблицы. Список пользователей отвечает на другую задачу, чем GRANT внутри прикладной базы. Исходная задача на Stack Overflow. Разделяем вход на сервер и SQL-права конкретной базы. Вход проверяется обычной учётной записью, затем отдельно выдаётся SELECT на каталог. SYSDBA используется только для подготовки и выдачи прав, а не для доказательства их ограничения.
Проверьте понимание
Что изменится, если вместо SELECT выдать ALL, чтобы быстро убрать ошибку доступа?
Разбор
Пользователь получит также права изменения и другие объектные разрешения из ALL. Ошибка исчезнет ценой потери нужного ограничения. Для читающей витрины оставьте SELECT; для нового требования добавьте конкретное право и отдельную проверку запрещённого действия.
Включить выданную роль в конкретном подключении
Нескольким читателям каталога удобно выдавать один набор прав через роль. Но наличие записи «пользователь состоит в роли» и её активация — разные вещи. Один и тот же пользователь сначала получает отказ SELECT без роли, затем указывает LAB_READER и видит Cable — 3, Clamp — 0. Удалять товары читающая роль по-прежнему не позволяет.
- CREATE ROLE LAB_READER создаёт именованный набор
- GRANT SELECT роли → GRANT роли пользователю → commit
- Вход без role: CURRENT_ROLE = NONE, SELECT запрещён
- Вход с LAB_READER: CURRENT_ROLE = LAB_READER, каталог читается
- DELETE не входит в набор роли; товары остаются на месте
Минимальная команда и её части
CREATE ROLE LAB_READER;
COMMIT;
GRANT SELECT ON TABLE LAB_ITEMS TO ROLE LAB_READER;
GRANT LAB_READER TO USER SHOP_READER;
COMMIT;Роль создаётся в прикладной базе и не имеет собственного пароля. Для подключения PHP использует форму ibase_connect($dsn, $user, $password, "UTF8", 0, 3, "LAB_READER"). 0 оставляет выбор буферов серверу, 3 — SQL-диалект, последний показанный аргумент — роль. f160ConnectUser в проекте передаёт эти параметры явно. CURRENT_ROLE берётся запросом из самого соединения: строка в конфигурации ещё не доказывает, что нужная роль действительно активна.
$db = ibase_connect($dsn, $user, $password, "UTF8", 0, 3, "LAB_READER");
if ($db === false) {
throw new RuntimeException("Не удалось открыть подключение с ролью");
}Здесь $dsn, $user и $password уже содержат настройки подключения. Последний показанный строковый аргумент выбирает роль. В полной программе эти значения создаются и проверяются в отдельной лаборатории; секреты не нужно вставлять в HTML-страницу или общедоступный исходник.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| CURRENT_ROLE без указания роли | NONE |
| SELECT без роли разрешён | нет |
| Код отказа без роли | -551 |
| CURRENT_ROLE при явном выборе | LAB_READER |
| Каталог с ролью | Cable — 3; Clamp — 0 |
| DELETE для читающей роли разрешён | нет |
| После отказа число товаров | 2 |
Полная программа, файлы лаборатории и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$owned=[];$removed=[];$db=$service=null;
try {
$config=f160Config();$service=f160Service();$runtime=f160Runtime($service);
$name=f160CreateDatabase($service,$owned,'role');
$user=f160NewUser($owned,'role');$password=f160Password();
f160AddUser($service,$user,$password);
$role='LAB_READER';$roleSQL=f160RoleIdentifier($role);
$db=f160Connect($name);
f160Command($db,'CREATE ROLE '.$roleSQL);
f160Commit($db);
f160Command($db,'GRANT SELECT ON TABLE LAB_ITEMS TO ROLE '.$roleSQL);
f160Command($db,'GRANT '.$roleSQL.' TO USER '.f160UserIdentifier($user));
f160Commit($db);f160Close($db);
$userDb=null;
try {
$userDb=f160ConnectUser($name,$user,$password);
$withoutRole=f160Read($userDb,'SELECT CURRENT_ROLE AS ROLE_NAME FROM RDB$DATABASE')[0]['ROLE_NAME'];
$denied=f160Capture(static fn()=>ibase_query($userDb,'SELECT ID,TITLE,QTY FROM LAB_ITEMS ORDER BY ID'));
f160Close($userDb);
$userDb=f160ConnectUser($name,$user,$password,$role);
$withRole=f160Read($userDb,'SELECT CURRENT_ROLE AS ROLE_NAME FROM RDB$DATABASE')[0]['ROLE_NAME'];
$catalogue=f160Read($userDb,'SELECT ID,TITLE,QTY FROM LAB_ITEMS ORDER BY ID');
$deniedDelete=f160Capture(static fn()=>ibase_query($userDb,'DELETE FROM LAB_ITEMS WHERE ID=1'));
$after=f160Read($userDb,'SELECT ID,TITLE,QTY FROM LAB_ITEMS ORDER BY ID');
f160Close($userDb);
} finally {
if(is_resource($userDb))f160Close($userDb);
}
$expected=[['ID'=>1,'TITLE'=>'Cable','QTY'=>3],['ID'=>2,'TITLE'=>'Clamp','QTY'=>0]];
$rows=[['CURRENT_ROLE без указания роли',$withoutRole],['SELECT без роли разрешён',$denied['value']!==false],
['Код отказа без роли',$denied['code']],['CURRENT_ROLE при явном выборе',$withRole],
['Каталог с ролью',$catalogue[0]['TITLE'].' — '.$catalogue[0]['QTY'].'; '.$catalogue[1]['TITLE'].' — '.$catalogue[1]['QTY']],
['DELETE для читающей роли разрешён',$deniedDelete['value']!==false],['После отказа число товаров',count($after)]];
$checks=['no_explicit_role'=>$withoutRole==='NONE',
'role_not_automatically_applied'=>$denied['value']===false && $denied['code']===-551 && $denied['warning'] && $denied['message_present'],
'explicit_role_active'=>$withRole===$role,
'role_select_reads_rows'=>$catalogue===$expected,
'delete_not_granted'=>$deniedDelete['value']===false && $deniedDelete['code']===-551 && $deniedDelete['warning'] && $deniedDelete['message_present'],
'rows_preserved'=>$after===$expected];
$observations=['role_without_option'=>$withoutRole,'select_without_role'=>$denied,'role_with_option'=>$withRole,
'catalogue'=>$catalogue,'delete_refusal'=>$deniedDelete,'after'=>$after,'role_is_default'=>false];
} finally {
if(is_resource($db)){f160Close($db);}
if(is_resource($service)){f160Detach($service);}
$removed=f160Cleanup($owned);
}
f160Report('fb160-role-login','Включить выданную роль в конкретном подключении',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
CURRENT_ROLE без указания роли — NONE
SELECT без роли разрешён — нет
Код отказа без роли — -551
CURRENT_ROLE при явном выборе — LAB_READER
Каталог с ролью — Cable — 3; Clamp — 0
DELETE для читающей роли разрешён — нет
После отказа число товаров — 2
Распакуйте все файлы архива в отдельный каталог. README.txt описывает отдельный TCP-сервер с собственной security database; embedded-подключение не заменяет этот опыт. example.php создаёт одну собственную FDB с каталогом Cable/3 и Clamp/0, одного обычного пользователя и необходимые выдачи прав. lab.php отделяет административные операции от пользовательского SQL, проверяет владельца и удаляет только зарегистрированные учебные объекты. result.html содержит реальные товары и отказы, result.json — наблюдения, metrics.json — проверки и завершение уборки. Пароли не печатаются.
Открыть таблицу фактического результата · Открыть наблюдения JSON.
Ограничения и выбор другого решения
Это обычная роль, выданная без DEFAULT. В Firebird4+ DEFAULT-роли могут включаться автоматически; CURRENT_ROLE показывает явно выбранную роль и не перечисляет все действующие DEFAULT-роли. Поэтому NONE не означает автоматически отсутствие любых прав. Прямые выдачи пользователю и PUBLIC также продолжают действовать. Сохраняйте точное имя роли; цитированные SQL-идентификаторы чувствительны к регистру. В лаборатории роль не совпадает с именем пользователя.
Практическая проблема: пользователю выдали роль и её права, но чтение таблицы всё ещё запрещено. В обсуждении оказались важны явная активация роли при подключении и точное имя роли. Исходная задача на Stack Overflow. Показываем два входа одного пользователя: без обычной роли и с ней. Читаем CURRENT_ROLE и настоящие товары. Современные DEFAULT-роли объясняются отдельно; правило старого Firebird не переносится на них автоматически.
Проверьте понимание
Почему не стоит исправлять отказ роли выдачей SELECT непосредственно пользователю, не проверив CURRENT_ROLE?
Разбор
Прямое разрешение может скрыть ошибку параметра подключения: запрос начнёт работать, но предполагаемая роль всё ещё не будет активна. Сначала проверьте grant роли пользователю, commit и фактический CURRENT_ROLE. Выдавайте прямые права только если это выбранная модель доступа, а не обход непонятной ошибки.
Отозвать SELECT и проверить новое подключение отдельно от старого
Временный читатель закончил работу, и его SELECT нужно отозвать. Но что именно проверять после REVOKE? В этом профиле старое открытое соединение продолжает читать Cable — 3 и Clamp — 0 даже новым запросом. После его явного закрытия новый вход остаётся возможен, а новый SELECT получает -551. Каталог не удаляется и не откатывается: администратор видит те же товары.
- Выдать единственный прямой SELECT и прочитать Cable — 3 / Clamp — 0
- Администратор: REVOKE SELECT + commit
- Старое открытое соединение: наблюдаем новый запрос, который здесь ещё читает
- Освободить результат → закрыть старое соединение → обнулить ссылку
- Новый вход с тем же логином: SELECT запрещён -551
- Администратор подтверждает, что строки каталога остались
Минимальная команда и её части
REVOKE SELECT ON TABLE LAB_ITEMS FROM USER SHOP_READER;
COMMIT;REVOKE перечисляет право и объект, FROM USER указывает получателя. Команду здесь выполняет тот же администратор, который выдал SELECT. $before содержит строки до отзыва; $oldRows — результат нового запроса на прежнем открытом соединении. Ресурс этого результата освобождается и удаляется из массива диагностики до закрытия подключения. Затем f160Close явно закрывает старое соединение, и только после этого f160ConnectUser открывает новое с теми же настройками. В $freshCall сохраняется конкретный отказ SELECT, а не отказ входа.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| До REVOKE | Cable — 3; Clamp — 0 |
| Старое соединение после REVOKE ещё читает | да |
| Строки старого соединения | Cable — 3; Clamp — 0 |
| Старое соединение закрыто перед новым | да |
| Новое соединение может выполнить SELECT | нет |
| Код нового отказа | -551 |
| Сам каталог не удалён | Cable — 3; Clamp — 0 |
Полная программа, файлы лаборатории и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$owned=[];$removed=[];$db=$service=null;
try {
$config=f160Config();$service=f160Service();$runtime=f160Runtime($service);
$name=f160CreateDatabase($service,$owned,'revoke');
$user=f160NewUser($owned,'revoke');$password=f160Password();
f160AddUser($service,$user,$password);
$db=f160Connect($name);
f160Command($db,'GRANT SELECT ON TABLE LAB_ITEMS TO USER '.f160UserIdentifier($user));
f160Commit($db);f160Close($db);
$userDb=null;$oldResult=null;
try {
$userDb=f160ConnectUser($name,$user,$password);
$before=f160Read($userDb,'SELECT ID,TITLE,QTY FROM LAB_ITEMS ORDER BY ID');
$db=f160Connect($name);
f160Command($db,'REVOKE SELECT ON TABLE LAB_ITEMS FROM USER '.f160UserIdentifier($user));
f160Commit($db);f160Close($db);
$oldCall=f160Capture(static fn()=>ibase_query($userDb,'SELECT ID,TITLE,QTY FROM LAB_ITEMS ORDER BY ID'));
$oldResult=$oldCall['value'];unset($oldCall['value']);
$oldCall['allowed']=is_resource($oldResult);
try {
$oldRows=is_resource($oldResult)?f160Rows($oldResult):[];
} finally {
if(is_resource($oldResult))ibase_free_result($oldResult);
$oldResult=null;
}
f160Close($userDb);
$oldConnectionClosed=$userDb===null;
$userDb=f160ConnectUser($name,$user,$password);
$freshCall=f160Capture(static fn()=>ibase_query($userDb,'SELECT ID,TITLE,QTY FROM LAB_ITEMS ORDER BY ID'));
f160Close($userDb);
} finally {
if(is_resource($oldResult))ibase_free_result($oldResult);
$oldResult=null;
if(is_resource($userDb))f160Close($userDb);
}
$db=f160Connect($name);
$after=f160Read($db,'SELECT ID,TITLE,QTY FROM LAB_ITEMS ORDER BY ID');
f160Close($db);
$expected=[['ID'=>1,'TITLE'=>'Cable','QTY'=>3],['ID'=>2,'TITLE'=>'Clamp','QTY'=>0]];
$rows=[['До REVOKE',$before[0]['TITLE'].' — '.$before[0]['QTY'].'; '.$before[1]['TITLE'].' — '.$before[1]['QTY']],
['Старое соединение после REVOKE ещё читает',$oldCall['allowed']],
['Строки старого соединения',$oldRows[0]['TITLE'].' — '.$oldRows[0]['QTY'].'; '.$oldRows[1]['TITLE'].' — '.$oldRows[1]['QTY']],
['Старое соединение закрыто перед новым',$oldConnectionClosed],
['Новое соединение может выполнить SELECT',$freshCall['value']!==false],['Код нового отказа',$freshCall['code']],
['Сам каталог не удалён',$after[0]['TITLE'].' — '.$after[0]['QTY'].'; '.$after[1]['TITLE'].' — '.$after[1]['QTY']]];
$checks=['granted_reader_initial_rows'=>$before===$expected,
'old_connection_observed_allowed'=>$oldCall['allowed'] && !$oldCall['warning'] && $oldCall['code']===0 && $oldRows===$expected,
'old_connection_closed'=>$oldConnectionClosed,
'fresh_connection_select_refused'=>$freshCall['value']===false && $freshCall['code']===-551 && $freshCall['warning'] && $freshCall['message_present'],
'catalogue_still_exists'=>$after===$expected];
$observations=['before'=>$before,'old_session_after_revoke'=>$oldCall,'old_session_rows'=>$oldRows,
'old_connection_closed'=>$oldConnectionClosed,'fresh_session_after_revoke'=>$freshCall,'admin_readback'=>$after,
'old_query_kind'=>'new ibase_query on the original still-open connection; not a reused prepared statement',
'permission_path'=>'one direct SELECT grant; no role or PUBLIC alternative'];
} finally {
if(is_resource($db)){f160Close($db);}
if(is_resource($service)){f160Detach($service);}
$removed=f160Cleanup($owned);
}
f160Report('fb160-revoke-access','Отозвать SELECT и проверить новое подключение отдельно от старого',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
До REVOKE — Cable — 3; Clamp — 0
Старое соединение после REVOKE ещё читает — да
Строки старого соединения — Cable — 3; Clamp — 0
Старое соединение закрыто перед новым — да
Новое соединение может выполнить SELECT — нет
Код нового отказа — -551
Сам каталог не удалён — Cable — 3; Clamp — 0
Распакуйте все файлы архива в отдельный каталог. README.txt описывает отдельный TCP-сервер с собственной security database; embedded-подключение не заменяет этот опыт. example.php создаёт одну собственную FDB с каталогом Cable/3 и Clamp/0, одного обычного пользователя и необходимые выдачи прав. lab.php отделяет административные операции от пользовательского SQL, проверяет владельца и удаляет только зарегистрированные учебные объекты. result.html содержит реальные товары и отказы, result.json — наблюдения, metrics.json — проверки и завершение уборки. Пароли не печатаются.
Открыть таблицу фактического результата · Открыть наблюдения JSON.
Ограничения и выбор другого решения
Продолжающееся чтение старого соединения — наблюдение нашего Firebird 5.0.4 / interbase 3.0.1, а не обещание одинакового результата для любого сервера, операции и подготовленного statement. Этот пример специально выполняет новый ibase_query, не переиспользует подготовленный запрос. Не считайте REVOKE немедленным отключением всех сессий или мгновенной блокировкой уже выполняемых операций. В рабочем приложении отзыв полномочий согласуют с жизненным циклом соединений и другими источниками прав. ibase_connect с одинаковыми аргументами может вернуть уже открытый ресурс, поэтому второй вызов без закрытия первого не доказывает новый вход. Новое подключение после REVOKE всё ещё проходит аутентификацию: удаление аккаунта и отзыв SQL-разрешения являются разными действиями.
Проверьте понимание
После REVOKE SELECT запрос продолжает работать. Какие две проверки нужно сделать до вывода «REVOKE сломан»?
Разбор
Сначала проверьте, действительно ли административное изменение подтверждено и не осталось другого SELECT через роль, PUBLIC или другого grantor. Затем отделите старое открытое соединение от нового: освободите statement/results, закройте старое подключение и проверьте разрешение в новом. Простое повторение ibase_connect с теми же аргументами при живом первом ресурсе может вернуть прежнее соединение. Для рабочего отзыва доступа также учитывайте активные операции и правила завершения соединений.
Разрешить изменение количества и запретить изменение названия
Кладовщик фиксирует количество товара, но не должен переименовывать каталог. Выдадим SELECT и UPDATE только для QTY. Разрешённая запись меняет Cable с 3 на 9 и подтверждается commit. Попытка записать название Changed получает отказ, а повторный SELECT показывает прежнее название Cable и уже подтверждённое количество 9.
- GRANT SELECT открывает чтение каталога
- GRANT UPDATE(QTY) разрешает менять только количество
- Пользователь: UPDATE QTY = 9 WHERE ID = 1 + commit
- Попытка UPDATE TITLE запрещена с -551
- Новый SELECT подтверждает Cable — 9 и Clamp — 0
Минимальная команда и её части
GRANT SELECT ON TABLE LAB_ITEMS TO USER STOCK_EDITOR;
GRANT UPDATE(QTY) ON TABLE LAB_ITEMS TO USER STOCK_EDITOR;
COMMIT;Скобки (QTY) относятся к привилегии UPDATE и перечисляют разрешённые столбцы. SELECT выдан отдельно: он нужен пользователю для чтения каталога и используемых данных. В самом UPDATE вопросительные знаки представляют значения: сначала 9, затем ID = 1. После успеха программа делает commit; запрещённое изменение названия проверяется следующим отдельным запросом и не отменяет предыдущий подтверждённый остаток.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| До изменения | Cable — 3 |
| После UPDATE(QTY) + commit | Cable — 9 |
| Изменение TITLE разрешено | нет |
| Код отказа TITLE | -551 |
| Название после отказа | Cable |
| Количество после отказа | 9 |
| Другой товар | Clamp — 0 |
Полная программа, файлы лаборатории и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$owned=[];$removed=[];$db=$service=null;
try {
$config=f160Config();$service=f160Service();$runtime=f160Runtime($service);
$name=f160CreateDatabase($service,$owned,'columns');
$user=f160NewUser($owned,'columns');$password=f160Password();
f160AddUser($service,$user,$password);
$db=f160Connect($name);
$userSQL=f160UserIdentifier($user);
f160Command($db,'GRANT SELECT ON TABLE LAB_ITEMS TO USER '.$userSQL);
f160Command($db,'GRANT UPDATE(QTY) ON TABLE LAB_ITEMS TO USER '.$userSQL);
f160Commit($db);f160Close($db);
$userDb=null;
try {
$userDb=f160ConnectUser($name,$user,$password);
$before=f160Read($userDb,'SELECT ID,TITLE,QTY FROM LAB_ITEMS ORDER BY ID');
f160Command($userDb,'UPDATE LAB_ITEMS SET QTY=? WHERE ID=?',[9,1]);
f160Commit($userDb);
$afterQuantity=f160Read($userDb,'SELECT ID,TITLE,QTY FROM LAB_ITEMS ORDER BY ID');
$deniedTitle=f160Capture(static fn()=>ibase_query($userDb,'UPDATE LAB_ITEMS SET TITLE=? WHERE ID=?','Changed',1));
$afterRefusal=f160Read($userDb,'SELECT ID,TITLE,QTY FROM LAB_ITEMS ORDER BY ID');
f160Close($userDb);
} finally {
if(is_resource($userDb))f160Close($userDb);
}
$initial=[['ID'=>1,'TITLE'=>'Cable','QTY'=>3],['ID'=>2,'TITLE'=>'Clamp','QTY'=>0]];
$expected=[['ID'=>1,'TITLE'=>'Cable','QTY'=>9],['ID'=>2,'TITLE'=>'Clamp','QTY'=>0]];
$rows=[['До изменения',$before[0]['TITLE'].' — '.$before[0]['QTY']],
['После UPDATE(QTY) + commit',$afterQuantity[0]['TITLE'].' — '.$afterQuantity[0]['QTY']],
['Изменение TITLE разрешено',$deniedTitle['value']!==false],['Код отказа TITLE',$deniedTitle['code']],
['Название после отказа',$afterRefusal[0]['TITLE']],['Количество после отказа',$afterRefusal[0]['QTY']],
['Другой товар',$afterRefusal[1]['TITLE'].' — '.$afterRefusal[1]['QTY']]];
$checks=['initial_catalogue'=>$before===$initial,
'quantity_committed'=>$afterQuantity===$expected,
'title_update_refused'=>$deniedTitle['value']===false && $deniedTitle['code']===-551 && $deniedTitle['warning'] && $deniedTitle['message_present'],
'title_and_committed_quantity_preserved'=>$afterRefusal===$expected];
$observations=['before'=>$before,'after_quantity_commit'=>$afterQuantity,'title_refusal'=>$deniedTitle,'after_refusal'=>$afterRefusal,
'granted_columns'=>['QTY'],'select_granted'=>true,'row_level_restriction_granted'=>false];
} finally {
if(is_resource($db)){f160Close($db);}
if(is_resource($service)){f160Detach($service);}
$removed=f160Cleanup($owned);
}
f160Report('fb160-column-update','Разрешить изменение количества и запретить изменение названия',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
До изменения — Cable — 3
После UPDATE(QTY) + commit — Cable — 9
Изменение TITLE разрешено — нет
Код отказа TITLE — -551
Название после отказа — Cable
Количество после отказа — 9
Другой товар — Clamp — 0
Распакуйте все файлы архива в отдельный каталог. README.txt описывает отдельный TCP-сервер с собственной security database; embedded-подключение не заменяет этот опыт. example.php создаёт одну собственную FDB с каталогом Cable/3 и Clamp/0, одного обычного пользователя и необходимые выдачи прав. lab.php отделяет административные операции от пользовательского SQL, проверяет владельца и удаляет только зарегистрированные учебные объекты. result.html содержит реальные товары и отказы, result.json — наблюдения, metrics.json — проверки и завершение уборки. Пароли не печатаются.
Открыть таблицу фактического результата · Открыть наблюдения JSON.
Ограничения и выбор другого решения
UPDATE(QTY) не добавляет правило «только строка ID1». В примере WHERE ID=1 выбирает товар, но пользователь с таким правом может сформировать другой разрешённый UPDATE количества. Поэтому ограничение сотрудников по складам или организациям требует отдельного проектирования прав на представления/процедуры и авторизации приложения. Также широкое UPDATE, выданное напрямую, через роль или PUBLIC, сделает узкую проверку бессмысленной: разрешения складываются.
Приложению нужно разрешить чтение, оставив другим приложениям возможность записи. Файловый режим, режим всей FDB и права одного пользователя легко перепутать. Исходная задача на Stack Overflow. Полномочия ограничиваются конкретным SQL-пользователем: SELECT отдельно от UPDATE(QTY). Файл базы не переводится в read-only. Пример чужого ответа, выдающий все права, не принят за решение читающего доступа.
Проверьте понимание
Пользователь должен менять и QTY, и название TITLE. Достаточно ли добавить TITLE в SQL UPDATE приложения?
Разбор
Нет: запрос должен соответствовать правам. При осознанном расширении полномочий администратор выдаёт UPDATE(QTY,TITLE) либо отдельный UPDATE(TITLE), подтверждает изменение и проверяет новым пользовательским подключением. Если названия менять не нужно, интерфейс приложения не должен пытаться обходить отказ полномочиями SYSDBA.
Ограничение столбцов не заменяет ограничение строк
Разрешение UPDATE(QTY) решает задачу «менять количество, не менять название». Оно не решает задачу «менять только товары своего склада». Например, разрешённый пользователь может написать другой WHERE либо убрать его; если ему выдан UPDATE(QTY) на таблицу, это право остаётся для соответствующего столбца доступных строк таблицы.
- SELECT/UPDATE определяют доступное действие
- UPDATE(QTY) ограничивает изменяемые столбцы
- WHERE ID=1 в программе выбирает текущий товар
- Для обязательной изоляции строк нужен спроектированный интерфейс базы/приложения
- Прямое право на исходную таблицу может обойти ограниченное представление
Один из инструментов для отдельного интерфейса данных — VIEW, именованный сохранённый SELECT. Можно открыть пользователю чтение представления с нужными столбцами/условием, не выдавая SELECT исходной таблицы. Если представление допускает запись, WITH CHECK OPTION проверяет, что новая версия строки остаётся в его условии. Возможность обновления, права владельца представления и другие способы доступа нужно проверять отдельно; не всякое представление автоматически изменяемо.
Это следующий этап проектирования, а не скрытая гарантия нашего column-update. В нём учебный пользователь имеет SELECT таблицы и UPDATE(QTY), а отказ TITLE подтверждён отдельной попыткой. Для многопользовательского сайта одного общего SQL-логина недостаточно, чтобы база узнала посетителя: приложение должно проверять его полномочия и корректно связывать их с условиями запросов или выбранным интерфейсом базы.
Проверять выданное действие под настоящим пользователем
При отказе доступа сначала назовите точное действие: вход, SELECT таблицы, UPDATE столбца или активация роли. Затем проверьте текущую базу, CURRENT_USER, CURRENT_ROLE, источник выдачи и commit административного изменения. В этих программах -551 означает полученный отказ SQL-привилегии; это не отказ пароля и не диагноз любого сбоя сервера. Код и наличие сообщения снимаются сразу, до следующего вызова драйвера. Сырые сообщения сервера могут содержать пути и внутренние имена, поэтому публичный интерфейс показывает понятное действие и безопасную диагностику.
Набор прав может приходить напрямую, через роли или PUBLIC. При отзыве одного grant учтите остальные пути. Изучите полный синтаксис GRANT и правила REVOKE и grantor перед сложным делегированием. Обычный получатель WITH GRANT OPTION может передавать объектное право, а WITH ADMIN OPTION у роли относится к членству; в наших минимальных пользователях делегирование не включено.
Следующий практический шаг — спроектировать интерфейс данных для своей задачи: читающая витрина, изменение остатков либо доступ через роль. Связанные темы: передача значений без склеивания SQL, границы commit и rollback, ошибка, код и безопасное завершение.
Источники и обсуждения
- Firebird5: выдача SQL-привилегий
- Firebird5: отзыв привилегий
- Firebird5: роли и активация при подключении
- Firebird5: аутентификация и полномочия
- Firebird5: представления и CHECK OPTION
- PHP: параметры и повторное использование ibase_connect
- interbase3.0.1: передача роли и повторное использование подключения
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


