2021年8月30日月曜日

表の操作ログを取得する - 追記

 カナダのInsum SolutionsがAPEX Instant Tipsというタイトルで、毎週金曜日にちょっとした小技を紹介しています。ちょうど最近"Instantly" add full table audit capability to your applicationsとして表の変更履歴を保存する方法を紹介していました。

SQL Developerを使用してジャーナル表(つまり履歴表)のDDLを生成しています。残念なことに、ライブデモでデータ・ディクショナリのインポートが成功しなかったため、ビデオを視聴しても実際の操作がわかりにくくなっています。

以下より、このビデオで触れられている、表の履歴を保存する3の方法を紹介してみます。ひとつはこのビデオの本題であるSQL Developerを使った方法、もうひとつはクイックSQLを使った方法、最後にこのビデオの視聴者からのコメントにあった、Flashback Data Archiveを使った方法です。

以下の準備作業が既に行われていることを前提とします。ワークスペース名や表EMP以外のジャーナル表を作成したい場合は、それぞれ作業内容を読み替えてください。

  1. Always FreeのAutonomous Transaction Processingのインスタンスがある
  2. APEXのワークスペースとしてAPEXDEVが作成されている
  3. サンプル・データセットのEMP/DEPTがインストールされている
こちらの記事にある事前準備が行われていれば、上記の条件はクリアしています。

SQL Developerを使ってジャーナル表を作成する


表EMPの履歴表(ジャーナル表)と表EMPの変更時に実行されるトリガーを生成します。

OCIのコンソールよりAutonomous Databaseのページを開き、DB接続をクリックします。ADBへの接続に使用するウォレットをダウンロードします。


開いたページにて、ウォレットのダウンロードをクリックします。


ダイアログが開くので、ダウンロードを実行します。Wallet_インスタンス名.zipというファイルがダウンロードされます。この例ではインスタンス名がAPEXDEVなので、ダウンロードされるファイル名はWallet_APEXDEV.zipになります。

パスワードの指定は必須なので適当に入力します。SQL Developerからの使用だけであれば、ウォレットに指定したパスワードが使われることはありません。


SQL Developerは以下のURLから入手できます。

https://www.oracle.com/sqldeveloper

SQL Developerを起動し(インストール作業の説明は割愛します - プラットフォームごとのInstallation Notesを参照してください)、Autonomous Databaseに接続します。

ユーザーはADMINまたはワークスペースのスキーマ(今回のワークスペース・スキーマはAPEXDEVなので、接続ユーザーとしてAPEXDEVも可)にて接続します。以下のスクリーンショットはユーザーADMINで接続しています。

接続名(Name)は任意の文字列です。以下ではadb-apexdev-adminとしています。ユーザー名としてADMIN、パスワードはADBのインスタンス作成時に指定したパスワードを指定しています。接続タイプとしてクラウド・ウォレットを選択し、構成ファイルダウンロード済みのウォレット・ファイルを指定します。Always Freeのインスタンスであれば、サービスインスタンス名_lowを選びます。以上の入力を行い、接続をクリックします。


データベースに接続されたら、データ・モデラーにジャーナル表を生成する対象である表EMPの定義を読み込みます。

ファイル・メニューより、Data Modelerインポートデータ・ディクショナリを呼び出します。

接続名を選択し、次へ進みます。

ジャーナル表を作成したい表を含むスキーマを選択します。今回の例では表EMPを含んでいるのはスキーマAPEXDEVなので、APEXDEVにチェックを入れています。次へ進みます。

ジャーナル表の生成対象となる表を選択します。今回の例ではEMPです。次へ進みます。

終了をクリックし、表EMPの定義をデータ・モデラーへインポートします。

インポートのログを保存するかどうか確認されます。今回は不要なので、閉じるをクリックします。

表EMPの定義がインポートされました。

続けて、たった今インポートした表EMPの定義をエクスポートします。エクスポートする際に、ジャーナル表とそれを操作するトリガーのDDLを生成します。

ファイル・メニューより、Data ModelerエクスポートDDLファイルを呼び出します。

ダイアログが開くので、生成をクリックします。

表のDDLスクリプトを含めるのタブを開き、対象となる表(この例ではAPEXDEV.EMP)にチェックを入れます。OKをクリックします。

ここでアクティブなスクリプト・セットとしてjournal tablesが選択されています。この指定により、ジャーナル表とトリガーのDDLが生成されます。

DDLが生成され、画面に表示されます。表名の末尾に_JNが付加された表がジャーナル表、_JNtrgが付加されたトリガーがジャーナル表へ表EMPの変更を書き込むトリガーになります。

生成されたDDLスクリプトをファイルに保存するか、必要な部分をコピー&ペーストして保存します。

以下が生成されたDDLの例です。

この中でCREATE TABLE APEXDEV.EMP_JNのDDLとCREATE OR REPLACE TRIGGER APEXDEV.EMP_JNtrgがジャーナル表に関係します。


データベース・アクションSQLまたはSQLワークショップSQLコマンドから、CREATE TABLE文およびCREATE TRIGGER文を実行すると、ジャーナル表およびジャーナル表への変更の書き込みが行われるようになります。

CREATE TABLE文の実行です。

CREATE OR REPLACE TRIGGER文の実行です。


アクティブなスクリプト・セットとして指定されているjournal tablesは、ツール・メニューData Modeler設計ルールおよび変換表DDL変換にて処理内容が定義されています。


表のDDL変換スクリプトとしてjournal tablesが定義されています。そこに記載されているJavaScriptのコードによって、ジャーナル表のCREATE TABLE文やトリガーのCREATE TRIGGER文が生成されていることが確認できます。


_JNや_JNtrgといったポストフィックスを変更したい場合、および、ジャーナル表に追加されるカラムを変更したい、といった場合は生成されたDDLを編集するか、または、ここに記載されているJavaScriptを編集することによって対応できるでしょう。


クイックSQLを使って履歴表を作成する


クイックSQLの表ディレクティブとしてhistoryがあります。ただし、この機能はOracle APEX 21.1よりデサポートになっています。

https://docs.oracle.com/en/database/oracle/application-express/21.1/htmrn/index.html#HTMRN-GUID-4B10102D-0984-4C17-A1F2-6F8B1343D757

7.1.8 History Table in Quick SQL Desupported

Quick SQL no longer generates a history table nor a trigger to log table data changes.


どのような機能なのか、以下に紹介します。LiveSQLに含まれるQuick SQLでは使用できます。

表EMPの履歴を取るには、表ディレクティブとしてhistoryを指定します。表EMPを例にとると、クイックSQLの定義は以下になります。
# genpk: false
# semantics: default
emp /history
    empno num
    ename vc50
    job vc50
    mgr num
    hiredate date
    sal num
    comm num
    deptno num

表EMPの指定に続いて/historyを記述します。


生成されるSQLは以下になります。



変更履歴を保存する表はHISTORYひとつのみです。それぞれの表への変更と削除(挿入は除く)の操作は、表ごとに作成されたトリガーにより表HISTORYへ記載されます。

表ごとに履歴表(ジャーナル表)を作成していないため、表への変更(列の追加など)の対応がトリガーの変更のみで済むといった利点があります。ただし、保存された履歴データの利用は困難で、このような記録の仕方であれば統合監査により監査証跡を取得した方が、トリガーの定義も不要でパフォーマンス面でのデメリットも回避できるかと思います。


Flashback Data Archiveを構成する


マニュアルのOracle Flashbackの制限事項として記載がありますが、Autonomous DatabaseではFlashback Data Archiveを構成することはできません。そのため、Oracle Database 18c Express Editionを使用して動作を確認してみました。

2023年8月追記:
Flashback Time Travel(Flashback Data Archiveの新しい名称 - 機能は同じ)がAutonomous Databaseで利用できるようになりました。ただし、ユーザー・コンテキストの保存はできないようです。

Flashback Data Archiveについては、データベース開発ガイドのフラッシュバック・アーカイブの使用にて説明されています。これからの説明は主にConnor McDonaldさんによるYouTubeのビデオを参照しています。

Flashback Data Archive
Flashback Data Archive ... can it handle high volume of transactions?
https://www.youtube.com/watch?v=FpRAc-FEWbE
(5:14以降よりFDAの障害についていくつか説明があります)

以下はSYSで作業しています。

Flashback Data Archiveを作成します。すでに表領域USERSが作成済みで、そこにFlashback Data Archiveの領域を1Gバイト確保します。保持期間は1日に設定します。通常はもっと長い期間を設定するでしょう。
create flashback archive default longterm                          
tablespace users quota 1g retention 1 day;
スキーマAPEXDEVにすでに存在している表EMPにたいして、Flashback Data Archiveを有効にします。
alter table apexdev.emp flashback archive longterm;
Flashback Data Archiveのコンテキスト・レベルをALLに変更します。DBMS_FLASHBACK_ARCHIVE.SET_CONTEXT_LEVELを呼び出します。Oracle APEXでは、アプリケーション・コンテキストAPEX$SESSIONにAPP_ID、APP_SESSION、APP_USERといった値が保持されています。コンテキスト・レベルをALLにすると、APEX$SESSIONが保存の対象になります。(同様の情報はビューSYS_FBA_CONTEXT_AUDの列MODULEまたはCLIENT_IDENTIFIERからも参照できるため、この設定は必須ではありません)。
begin
    dbms_flashback_archive.set_context_level(
        level => 'ALL'
    );
end;
/
以上でFlashback Data Archiveによって、表EMPの変更履歴が保持されるようになりました。Flashback Version Queryを使って、変更履歴を確認してみます。

表EMPの変更履歴を一覧します。検索時に利用可能な行データ疑似列と、保存されているアプリケーション・コンテキストAPEX$SESSIONのAPP_IDおよびAPP_USERを出力する列に含めます。DBMS_FLASHBACK_ARCHIVE.GET_SYS_CONTEXTを使用しています。

SQLワークショップのSQLコマンドより実行します。

select empno, ename, job, mgr, sal, comm, deptno
, versions_starttime, versions_endtime, versions_xid, versions_operation
, dbms_flashback_archive.get_sys_context(versions_xid, 'APEX$SESSION', 'APP_ID') app_id
, dbms_flashback_archive.get_sys_context(versions_xid, 'APEX$SESSION', 'APP_USER') app_user
from emp
versions between timestamp
timestamp'2021-08-30 15:30:00' and systimestamp
where versions_xid is not null
order by versions_starttime asc;

列EMPNO、ENAMEといった列の値は、VERSIONS_STARTTIMEからVERSIONS_ENDTIMEまでの間維持されていたこと、この列はVERSIONS_OPERATIONでの操作により変更されたこと、アプリケーションIDが100であるAPEXアプリケーションより操作が行われたこと、APEXのユーザーADMINによって操作が行われたことなどを確認することができます。


ビューSYS_FBA_CONTEXT_AUDと列XIDでジョインをすると、CLIENT_IDENTIFIERやMODULEの情報も参照することができます。

select empno, ename, job, mgr, sal, comm, deptno
, a.client_identifier, a.module
from
(
select
empno, ename, job, mgr, sal, comm, deptno
, versions_xid
from emp
versions between timestamp
timestamp'2021-08-30 14:20:00' and systimestamp
) e, sys.sys_fba_context_aud a
where e.versions_xid = a.xid


最近のRU(Release Update)には、FDAのパフォーマンスを改善を行なう修正が入っているとのことなので、利用を検討される際には最新のRU を確認することをお勧めします。

表の変更履歴の取得にも色々な方法があり、それぞれ一長一短があります。要件に応じて方法を決めるのが良いかと思います。

以上になります。Oracle APEXのアプリケーション開発の参考になれば幸いです。

2021年8月27日金曜日

表の操作ログを取得する

 表の操作ログをトリガーで取得したい、と相談を受けました。ちょっと方法について考えてみました。

準備

SQLワークショップユーティリティサンプル・データセットからEMP/DEPTをインストールします。表EMPを操作するアプリケーションを作成することにより、動作の確認を行います。

SQLワークショップユーティリティクイックSQLより表EMPの操作ログを保存する表EMP_LOGを作成します。クイックSQLの定義は以下になります。

# semantics: default
emp_log
optime date /default sysdate
op vc8
oplog vc80
empno num
ename vc50
job vc50
mgr num
hiredate date
sal num
comm num
deptno num

SQLの生成SQLスクリプトを保存レビューおよび実行を順次実行して、表EMP_LOGを作成します。アプリケーションの作成は行いません。

テストに使用するアプリケーションを作成します。アプリケーション作成ウィザードを起動し、空のアプリケーションを作成します。名前表EMPの操作とします。アプリケーションの作成を実行します。


アプリケーションが作成されたら、フォームによる編集を行うページを作成します。ページの作成を実行します。


フォームを選択します。


フォーム付きレポートを選択します。


ページ名は任意です。ここではレポート・ページ名レポートEMPフォーム・ページ名フォームEMPとしています。フォーム・ページ・モードモーダル・ダイアログにします。フォームのページに作成されるページ・アイテムの名前にはページ番号が含まれるので、フォーム・ページ番号3にしてください。へ進みます。


ナビゲーションのプリファレンスとして、新規ナビゲーション・メニュー・エントリの作成を選択します。へ進みます。


データ・ソース表/ビューの名前EMP(表)を指定します。へ進みます。


フォームが扱う主キー型として、データベースで管理(ROWID)を選択し、作成をクリックします。


以上で表EMPの対話モード・レポートと編集フォームのページが作成されました。

同様の手順で対話グリッドのページを作成します。ページの作成で編集可能対話グリッドを選択します。ページ名グリッドEMPとし、レポート・ソース表/ビューの名前EMP(表)主キー型としてROWIDを選択します。作成をクリックします。


これで対話グリッドのページも作成されました。

これから、作成したアプリケーションを使った表EMPの操作を、ログとして記録する実装を行なっていきます。

フォームの操作ログの記録


Oracle APEXのアプリケーションにプロセスを追加して、操作のログを取得します。

フォームのページ(ページ番号3番)をページ・デザイナにて開きます。右ペインにプロセス・ビューを開き、プロセスの作成を実行します。作成するプロセスのコードは以下になります。

declare
l_msg varchar2(80);
begin
l_msg := :APP_USER || ':' || :APP_SESSION;
insert into emp_log(
op, oplog, empno
, ename, job, mgr, hiredate, sal, comm, deptno
)
values
(
:APEX$ROW_STATUS, l_msg, :P3_EMPNO
, :P3_ENAME, :P3_JOB, :P3_MGR, :P3_HIREDATE, :P3_SAL, :P3_COMM, :P3_DEPTNO
);
end;

プロセスのタイプとしてフォーム - 行の自動処理(DML)を選択します。フォーム・リージョンフォームEMPです。設定ターゲット・タイプとしてPL/SQL Codeを選択し、上記のコードを記載します。実際に編集している表の操作ではなく、別表に書き込むだけなので、(対象が元表である)失われた更新の防止OFF行のロックNoとします。


これでフォームを使った表EMPのINSERT、UPDATE、DELETEの操作が表EMP_LOGに記載されます。

テスト用アプリケーションを実行し表EMPの操作を行なった後、表EMP_LOGを確認してみます。SQLコマンドから以下のSQLを実行します。

select * from emp_log order by optime desc;



対話グリッドの操作ログの記録


対話グリッドのページ(ページ番号4番)をページ・デザイナにて開きます。右ペインにプロセス・ビューを開き、プロセスの作成を実行します。作成するプロセスのコードは以下になります。ほとんどフォームと同じです。

declare
l_msg varchar2(80);
begin
l_msg := :APP_USER || ':' || :APP_SESSION;
insert into emp_log(
op, oplog, empno
, ename, job, mgr, hiredate, sal, comm, deptno
)
values
(
:APEX$ROW_STATUS, l_msg, :EMPNO
, :ENAME, :JOB, :MGR, :HIREDATE, :SAL, :COMM, :DEPTNO
);
end;

プロセスのプロパティの設定についても、フォームのときと同じです。


これで、対話グリッドの操作ログを取得するプロセスも作成できました。

テスト用アプリケーションから表EMPを操作し、その操作ログを確認します。



トリガーによる記録



APEXのアプリケーションではなく、データベース・トリガーを使って操作ログを取得してみます。

INSERTトリガーは以下になります。

create or replace trigger tgr_emp_ins
before insert on emp
for each row
declare
l_msg varchar2(80);
begin
l_msg := sys_context('APEX$SESSION','APP_USER') || ':' || sys_context('APEX$SESSION','APP_SESSION');
insert into emp_log(
op, oplog, empno
, ename, job, mgr, hiredate, sal, comm, deptno
)
values
(
'INSERT', l_msg, :new.empno
, :new.ename, :new.job, :new.mgr, :new.hiredate, :new.sal, :new.comm, :new.deptno
);
end;

UPDATEトリガーです。

create or replace trigger tgr_emp_upd
before update on emp
for each row
declare
l_msg varchar2(80);
begin
l_msg := sys_context('APEX$SESSION','APP_USER') || ':' || sys_context('APEX$SESSION','APP_SESSION');
insert into emp_log(
op, oplog, empno
, ename, job, mgr, hiredate, sal, comm, deptno
)
values
(
'UPDATE', l_msg, :new.empno
, :new.ename, :new.job, :new.mgr, :new.hiredate, :new.sal, :new.comm, :new.deptno
);
end;

DELETEトリガーです。

create or replace trigger tgr_emp_del
before delete on emp
for each row
declare
l_msg varchar2(80);
begin
l_msg := sys_context('APEX$SESSION','APP_USER') || ':' || sys_context('APEX$SESSION','APP_SESSION');
insert into emp_log(
op, oplog, empno
, ename, job, mgr, hiredate, sal, comm, deptno
)
values
(
'DELETE', l_msg, :old.empno
, :old.ename, :old.job, :old.mgr, :old.hiredate, :old.sal, :old.comm, :old.deptno
);
end;

上記のトリガーを設定した後、テスト用アプリケーションから表EMPを操作してみます。

その後、表EMP_LOGの内容を確認します。トリガーのログでは操作をINSERT, UPDATE, DELETEとして記載しています。Oracle APEXのプロセスではC, U, Dとしています。1つの操作について、それぞれのログが記載されていることが確認できます。



統合監査による記録


プロセスやトリガーを設定せずに、統合監査のポリシーを定義して表EMPで行われた操作を記録することができます。

ユーザーADMINにてデータベース・アクションに接続し、以下のSQLにて統合監査ポリシーapex_emp_opの作成と有効化を行います。

create audit policy apex_emp_op
actions
all on apexdev.emp
when '1=1'
evaluate per statement;
audit policy apex_emp_op;


これで表EMPへの操作が記録されます。APEXのプロセスやトリガーの実装とは異なり、SELECT文の実行も監査証跡の取得対象にすることが可能です。

テスト用のアプリケーションを使って表EMPの操作を行なった後、ビューUNIFIED_AUDIT_TRAILを検索し、監査証跡を確認します。以下のSELECT文を実行します。

select
action_name, sql_text, sql_binds, current_user, client_identifier
from unified_audit_trail
where 1=1
and object_schema = 'APEXDEV'
and object_name = 'EMP'
and unified_audit_policies = 'APEX_EMP_OP'
order by event_timestamp desc;


表EMPの監査証跡が取得されていることが確認できます。

以上で表EMPの操作ログを取得する方法の紹介は終了です。

自律トランザクションについて


Oracle APEXのプロセスまたはトリガーによる操作ログの取得では、エラーが発生することにより操作のログが取得されない場合が起こり得ます。ログの書き込みを自律トランザクション (新しくトランザクションを開始し、ログを記載した時点で終了する) にて実行することにより、それぞれの障害を分離することができます。

Oracle APEXのプロセスとしての操作ログを取得する場合は、以下のようなログを表に書き込むプロシージャを作成し、そのプロシージャが自律トランザクションで動作するように記述します。APEXのページ・アイテムはデータを文字列として扱っていることを考慮して、引数の型をすべてVARCHAR2にしています。

create or replace procedure log_emp_op(
p_row_status in varchar2
, p_app_user in varchar2
, p_app_session in varchar2
, p_empno in varchar2
, p_ename in varchar2
, p_job in varchar2
, p_mgr in varchar2
, p_hiredate in varchar2
, p_sal in varchar2
, p_comm in varchar2
, p_deptno in varchar2
)
is
l_msg varchar2(80);
pragma autonomous_transaction;
begin
l_msg := p_app_user || ':' || p_app_session;
insert into emp_log(
op, oplog, empno
, ename, job, mgr, hiredate, sal, comm, deptno
)
values
(
p_row_status, l_msg, p_empno
, p_ename, p_job, p_mgr, p_hiredate, p_sal, p_comm, p_deptno
);
commit;
end log_emp_op;

操作ログを取得するプロセスのソースは以下に置き換わります。

log_emp_op(
p_row_status => :APEX$ROW_STATUS
, p_app_user => :APP_USER
, p_app_session => :APP_SESSION
, p_empno => :P3_EMPNO
, p_ename => :P3_ENAME
, p_job => :P3_JOB
, p_mgr => :P3_MGR
, p_hiredate => :P3_HIREDATE
, p_sal => :P3_SAL
, p_comm => :P3_COMM
, p_deptno => :P3_DEPTNO
);

対話グリッドのプロセスのソースは以下になります。

log_emp_op(
p_row_status => :APEX$ROW_STATUS
, p_app_user => :APP_USER
, p_app_session => :APP_SESSION
, p_empno => :EMPNO
, p_ename => :ENAME
, p_job => :JOB
, p_mgr => :MGR
, p_hiredate => :HIREDATE
, p_sal => :SAL
, p_comm => :COMM
, p_deptno => :DEPTNO
);

トリガーを自律トランザクションにするには、pragma autonomous_transactionの指定とcommit文を追加します。INSERTトリガーの例です。他のトリガーも同様に変更します。

create or replace trigger tgr_emp_ins
before insert on emp
for each row
declare
l_msg varchar2(80);
pragma autonomous_transaction;
begin
l_msg := sys_context('APEX$SESSION','APP_USER') || ':' || sys_context('APEX$SESSION','APP_SESSION');
insert into emp_log(
op, oplog, empno
, ename, job, mgr, hiredate, sal, comm, deptno
)
values
(
'INSERT', l_msg, :new.empno
, :new.ename, :new.job, :new.mgr, :new.hiredate, :new.sal, :new.comm, :new.deptno
);
commit;
end;


今回作成したアプリケーションのエクスポートは以下に置きました。トリガーのソースもサポート・オブジェクトとして含んでいます。
https://github.com/ujnak/apexapps/blob/master/exports/log-emp-op.sql

Oracle APEXのアプリケーション作成の参考になれば幸いです。

カスタム認証スキームの保護について

 以前の記事でカスタム認証を動作させるために、表のSELECT権限をPUBLICに与えました。そのときの記事の本題ではないとはいえ、あまり良くない方法ではあります。そこで、カスタムの認証スキームの保護について、ユーザー情報を直接参照することをを禁止した上で、認証スキームのファンクションを実行できる実装をいくつか行ってみます。

確認に使用する環境はAlways FreeのAutonomous Transaction Processingになります。特別な機能は使用しないので、その他の環境でも同様に実装可能でしょう。APEXのワークスペースとしてAPEXDEVが作成済みで、それを使用して確認を行います。


認証スキームのためのスキーマの作成


最初に認証スキームに使うデータを保持するスキーマを作成します。認証情報を保持するスキーマを、APEXのワークスペースのスキーマや、ユーザー・データを保持するスキーマと分離することにより、認証に使用する情報をそれらのスキーマから直接参照できないようにします。

ユーザーADMINにてデータベース・アクションに接続し、データベース・ユーザーを開きます。


ユーザーの作成を呼び出します。


ユーザー名CUSTAUTHとします。パスワードを指定し、WebアクセスONにします。表領域の割り当て制限 DATAとして25Mを割り当てます。今回の実装例では認証データは微々たるものなので25Mバイトを割り与えていますが、もっと多く(最大はUNLIMITED)割り与えてもよいでしょう。ユーザーの作成を実行します。


ユーザーCUSTAUTHが作成されました。認証スキームが使用する表やファンクションなど、認証に関するすべてのデータベース・オブジェクトは、このCUSTAUTHのスキーマに作成します。


認証スキームに使用するオブジェクトの作成



こちらの資料(Oracle APEX勉強会 - 認証と認可の実装を学ぶ)で紹介した認証スキームを実装します。

ユーザーCUSTAUTHデータベース・アクションに接続し、開発SQLを開き表MY_USERSを作成します。以下のCREATE TABLE文を実行します。
CREATE TABLE "MY_USERS"
(
    "ID" VARCHAR2(32),
	"USER_NAME" VARCHAR2(30) NOT NULL ENABLE,
    "PASSWORD" VARCHAR2(256),
  PRIMARY KEY("ID") USING INDEX ENABLE
);


ユーザーの登録を行います。ユーザー名はTESTUSER、パスワードはmypass7777です。以下のPL/SQLコードを実行します。
declare
    l_id my_users.id%type;
begin
    l_id := rawtohex(sys_guid());
    insert into my_users(id, user_name) values(l_id,'TESTUSER');
    update my_users set password = rawtohex(standard_hash('mypass7777'||id||user_name, 'SHA512'))
    where id = l_id;
end;
/

認証スキームとなるファンクションmy_authenticationを作成します。
create or replace function my_authentication (
  p_username in varchar2, 
  p_password in varchar2 ) 
return boolean
is
  l_user my_users.user_name%type := upper(p_username);
  l_pwd my_users.password%type;
  l_id my_users.id%type;
  l_hash my_users.password%type;
begin
    select id, password
      into l_id, l_pwd
      from my_users
     where user_name = l_user;
select rawtohex(standard_hash(p_password||l_id||l_user, 'SHA512'))
     into l_hash
     from dual;
     return l_pwd = l_hash;
exception
when NO_DATA_FOUND then
    return false; 
end;
/

作成したファンクションをユーザーAPEXDEVから呼び出せるように、実行権限を与えます。
grant execute on my_authentication to apexdev;



以上で認証スキームとして使用するファンクションや、それが利用する表が作成されました。サインインに使用するユーザーTESTUSERも登録済みです。


テスト用アプリケーションの作成


テストに使用するアプリケーションを作成します。アプリケーション作成ウィザードを実行し、アプリケーションの名前認証スキームの確認として、アプリケーションの作成を実行します。何の機能も含まないアプリケーションが作成されます。


アプリケーションが作成されたら、共有コンポーネント認証スキームを開き、作成を実行します。


スキームの作成として、ギャラリからの事前構成済スキームに基づくを選択します。に進みます。


名前CUSTAUTHとし、タイプとしてカスタムを選択します。認証ファンクション名としてcustauth.my_authenticationを指定します。認証スキームの作成を実行します。


作成された認証スキームはすぐにカレントの認証スキームになります。ページを実行して、サインインの確認を行います。


ユーザー名testuser、パスワードmypass7777でサインインに成功します。それ以外ではサインインはできません。


以上でテスト用のアプリケーションも作成できました。


保護の確認


PL/SQLによるプロシージャ、ファンクションおよびパッケージは特別な指定がない限り、デフォルトでは定義者権限にて実行されます。定義者権限について、マニュアルでは以下のように説明されています。

定義者権限プロシージャのユーザーに必要なのは、そのプロシージャを実行する権限のみで、そのプロシージャでアクセスする基礎となるオブジェクトに対する権限は不要です。これは、定義者権限プロシージャは、その実行者に関係なく、プロシージャを所有するユーザーのセキュリティ・ドメインの下で動作するためです。

今回の例に当てはめて説明します。

作成したファンクションmy_authenticationはスキーマCUSTAUTHに作成されているため、CUSTAUTHの権限で実行されます。ファンクションmy_authenticationの実行権限があれば、ファンクション内でアクセスしている表などのオブジェクトのアクセス権限は不要です。それらはCUSTAUTHが持つの権限でアクセスされるためです。

ユーザーAPEXDEVは表CUSTAUTH.MY_USERSへのアクセス権限は持ちませんが、ファンクションmy_authenticationの実行権限は与えられています。そのため、受け取ったユーザー名、パスワードが正しいかどうかファンクションmy_authenticationを呼び出して検証することはできます。しかし、登録済みのユーザー名の一覧やハッシュ化されたパスワードを直接参照することはできません。

オラクルでは定義者権限がデフォルトの設定になります。

実際にSQLワークショップSQLコマンドより以下のSQLを実行すると、ORA-00942: 表またはビューが存在しません。が発生します。
select * from custauth.my_users;


テスト用アプリケーションではファンクションmy_authenticationが呼び出され、ユーザーTESTUSERにて認証が成功できていることは確認済みです。実際にどのようなアクセスが発生しているのか、表CUSTAUTH.MY_USERSの監査証跡を取得して確認してみます。

ユーザーADMINにてデータベース・アクションに接続し、以下のSQLを実行します。
create audit policy apex_custauth
actions 
    select on custauth.my_users
when '1=1'
evaluate per statement 
;
audit policy apex_custauth;

テスト用のアプリケーションを実行し(サインイン済みの場合は一旦サインアウトし)、サインインを行います。サインインを行なったのち、ビューUNIFIED_AUDIT_TRAILを確認します。
select
    sql_text, sql_binds, current_user, client_identifier
from unified_audit_trail 
where 1=1
and object_schema = 'CUSTAUTH' 
and unified_audit_policies = 'APEX_CUSTAUTH'
order by event_timestamp desc;

current_userCUSTAUTHとなっていることより、認証スキームのファンクションmy_authenticationが、その認証スキームを実行しているユーザーAPEXDEVではなく、ファンクションmy_authenticationが定義されているユーザーCUSTAUTHにて実行されていることが確認できます。

大抵の場合、PL/SQLのプロシージャ、ファンクションおよびパッケージは定義者権限による実行が適切です。


実行者権限でのファンクションの実行


PL/SQLのファンクションは呼び出したユーザーの権限で実行させることも可能です。その場合はコードにauthid current_userを含めます。ファンクションmy_authenticationを実行者権限で動作するように改変します。

ユーザーCUSTAUTHでデータベース・アクションに接続し、SQLより以下を実行します。authid current_userの記述を追加しています。

create or replace function my_authentication (
  p_username in varchar2, 
  p_password in varchar2 ) 
return boolean
authid current_user
is
  l_user custauth.my_users.user_name%type := upper(p_username);
  l_pwd  custauth.my_users.password%type;
  l_id   custauth.my_users.id%type;
  l_hash custauth.my_users.password%type;
begin
    select id, password
      into l_id, l_pwd
      from custauth.my_users
     where user_name = l_user;
select rawtohex(standard_hash(p_password||l_id||l_user, 'SHA512'))
     into l_hash
     from dual;
     return l_pwd = l_hash;
exception
when NO_DATA_FOUND then
    return false; 
end;
/

authid current_userの追加以外に、表MY_USERSにスキーマCUSTAUTHの指定も含めています。実行者権限での実行で単に表MY_USERSが指定されている場合は、実行者のスキーマに含まれている表を参照します。(この場合、表APEXDEV.MY_USERSを参照しようとします)

ファンクションmy_authenticationはユーザーAPEXDEVとして実行されるため、表MY_USERSへのSELECT権限が必要です。以下のGRANT文を実行します。
grant select on my_users to apexdev;

テスト用アプリケーションを実行し、ユーザーTESTUSERでサインインします。正常にサインインされるはずです。

ユーザーADMINにてデータベース・アクションに接続し、ビューUNIFIED_AUDIT_TRAILより、認証を行うために実行されたSQLを確認します。current_userAPEXDEVであることが確認できます。



コード・ベース・アクセス制御の設定


表MY_USERSへのSELECT権限をユーザーAPEXDEVに与えると、登録されているすべてのユーザーの情報を、APEXDEVから参照することができるようになります。これは望ましくないため、ファンクションmy_authenticationを通したときだけ、表MY_USERSの参照を可能にします。

ユーザーCUSTAUTHにロールを作成する権限を与えます。
grant create role to custauth ;

ユーザーCUSTAUTHでデータベース・アクションに接続します。SQLから以下を実行します。ユーザーAPEXDEVより表MY_USERSのSELECT権限を削除します。表MY_USERSのSELECT権限を含んだロールCUSTAUTH_MY_USERSを作成し、そのロールをファンクションMY_AUTHENTICATIONへ割り与えています。
revoke select on my_users from apexdev;
create role custauth_my_users;
grant select on my_users to custauth_my_users;
grant custauth_my_users to function my_authentication;

以上の設定により、テスト用アプリケーションのサインインは問題なく行えるようになります。ユーザーtestuser、パスワードmypass7777にてサインインします。


アプリケーションの画面が開きます。


この状態でSQLコマンドより表CUSTAUTH.MY_USERSを検索すると、ORA-00942: 表またはビューが存在しません。が発生します。これは期待している動作です。


以上で、認証に使用する情報を保護した上で、認証スキームを実装する方法の紹介は終了です。

Oracle APEXのアプリケーション作成の参考になれば幸いです。