0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Autonomous AI DatabaseでDeep Data Securityを試す [ローカルユーザー]

0
Last updated at Posted at 2026-10-16

1. はじめに

同じテーブルを使っていても、「店舗担当者は自店舗の情報だけ」「本部担当者は全店舗の情報」といった参照範囲を分けたい場面があります。
さらに、自店舗のデータであっても、損失見込みや本部メモなどの項目は本部だけに公開したいことがあります。

今回は、Oracle AI Databaseの Deep Data Security を使い、このようなアクセス制御をDB側で設定してみました。
目的は次の3点です。

  • 行・列単位のアクセス制御がどのように動くかを理解する。
  • ローカル・エンド・ユーザー、データロール、データグラントの設定手順を整理する。
  • ユーザーごとに同じSQLを実行し、想定した参照範囲になるかを確認する。

題材には、架空の小売チェーンの店舗システム障害を使います。
AIエージェントやアプリケーションは介さず、DBへ直接SQLを発行して結果を比較しました。

本記事のDB名・店舗名・識別子は公開用のダミーまたは架空のサンプルです。
結果の件数・数値・NULLの動作は実測に基づきます。
個人情報、実環境の接続先、パスワード、ウォレット、鍵は掲載していません。

2. Deep Data Securityとは

Deep Data Securityは、認証されたエンドユーザーに応じて、DBオブジェクトの参照などを細かく制御する仕組みです。
誰が使っているかをDBが認識し、許可された行・列を返します。
今回使用したのはOracle AI Database 26aiです。
Oracle公式ドキュメント

Deep Data Securityは、ADB専用ではなく、Oracle AI Database本体に組み込まれた機能です。
オンプレミスでも提供されており、本記事ではADB上で利用しています。
Oracle公式FAQ

導入されたのは Oracle AI Database 26aiのRelease Update(RU)23.26.2(2026年4月) です。
機能の対外発表は2026年3月24日で、発表日と対応RUのリリース時期は異なります。
公式新機能一覧、2026年4月のリリース情報、Oracle公式発表

今回の設定に出てくる用語を先に整理します。

用語 今回の役割
スキーマ テーブルなどの格納先。
今回は STORE_DEMO
ローカル・エンド・ユーザー DB内で管理する利用者。
「店舗A担当」「本部担当」などを表す
データロール 利用者を権限の種類ごとにまとめるもの。
今回は店舗用と本部用
データグラント どのテーブルの、どの行・列を、どのデータロールに許可するかを記述する定義

設定の関係は「ユーザーにデータロールを割り当て、そのロールに対するデータグラントを定義する」と捉えると分かりやすいと思います。
エンドユーザー自身にテーブルを所有させる構成ではありません。

3. 検証環境

今回はADBだけでデータの保管とアクセス制御を行うため、構成図は省略します。
SQLを実行するクライアントは検証用で、業務アプリケーションは使用していません。

項目 内容
DBサービス Autonomous AI Database(ADB)
DB名(ダミー) DEMO_ADB
DBバージョン Oracle AI Database 26ai / 23.26.4.1.0
データ所有スキーマ STORE_DEMO
認証方法 ローカル・エンド・ユーザーのパスワード認証
外部認証基盤 IAM・Microsoft Entra IDとも使用しない
SQL実行クライアント Python / python-oracledb 26.0.1(Thinモード)
検証日 2026年10月16日

実際の接続ではmTLSを使用していますが、接続設定やウォレットの内容は本記事の対象外です。
あらかじめ、管理者と各エンドユーザーで接続できるSQLクライアント環境を準備します。

4. サンプルデータの構造

4.1 テーブルは3つ

テーブル 内容 件数 主キー
INCIDENTS 店舗ごとの障害情報 6件 incident_id
SYSTEMS システム台帳 3件 system_id
RUNBOOKS システム別の初動対応手順書 3件 runbook_id

INCIDENTS.system_id と RUNBOOKS.system_id は、どちらも SYSTEMS.system_id を参照します。
これにより、障害情報にシステム名や対応手順を結び付けられます。
今回は1システムにつき手順書を1件ずつ登録しました。

4.2 障害情報:INCIDENTS

列 型 内容
incident_id VARCHAR2 障害ID
store_id / store_name VARCHAR2 店舗番号・店舗名
assigned_end_user VARCHAR2 この障害を参照できる店舗担当ユーザー名
system_id VARCHAR2 対象システムのID
detected_at TIMESTAMP 検知日時
monitoring_severity VARCHAR2 監視上の重要度
observed_duration_minutes NUMBER 異常の継続時間(分)
affected_terminal_count / total_terminal_count NUMBER 影響端末数・全端末数
symptom / business_impact VARCHAR2 症状・業務影響
incident_status VARCHAR2 対応状況
estimated_loss_yen NUMBER 損失見込み。
本部だけに公開
internal_note VARCHAR2 本部メモ。
本部だけに公開

15列のうち、後ろの2列を機密情報として扱います。
登録した6件の概要は次のとおりです。
金額はあらかじめ登録した架空の値で、AIが計算したものではありません。

incident_id store_name assigned_end_user system_id estimated_loss_yen
INC-20261014-101-001 店舗A store_101 POS-CORE-DB 320000
INC-20261014-101-002 店舗A store_101 INVENTORY-SYNC 0
INC-20261014-102-001 店舗B store_102 PAYMENT-GW 42000
INC-20261014-102-002 店舗B store_102 POS-CORE-DB 3000
INC-20261014-103-001 店舗C store_103 POS-CORE-DB 280000
INC-20261014-103-002 店舗C store_103 INVENTORY-SYNC 7000

各店舗2件ずつ、合計6件です。
店舗Cの障害も登録しますが、今回は店舗C担当のユーザーを作りません。
店舗A・B担当には見えず、本部には見えることを試すためです。

estimated_loss_yen と internal_note はテーブル上で NOT NULL としています。
後述の検証で店舗ユーザーにNULLが返るのは、元データが未入力だからではありません。

4.3 システム台帳:SYSTEMS

5列で構成し、システムID・名称・サービス範囲・一次連絡先・エスカレーション先を登録しました。
連絡先も架空の値です。

system_id system_name
INVENTORY-SYNC 在庫・発注連携バッチ
PAYMENT-GW キャッシュレス決済ゲートウェイ
POS-CORE-DB 店舗POS共通データベース

4.4 手順書:RUNBOOKS

列 型 内容
runbook_id VARCHAR2 手順書ID
system_id VARCHAR2 対象システム
version VARCHAR2 手順書の版
priority_rules CLOB 優先度の判断基準
initial_checks CLOB 初動で確認する項目
escalation_rules CLOB 連絡・エスカレーションの基準

CLOBは長い文章を保存できるデータ型です。
今回は「影響端末数を確認する」「会計を継続できるかを確認する」といった短い手順を登録しました。
システム台帳と手順書は、店舗・本部とも全件を参照できる共通情報とします。

5. Deep Data Securityの設定手順

以下は、実際に構築した設定を公開用の名前に置き換えて整理したものです。
検証時に再作成したわけではありません。

管理者権限で「データの準備 → エンドユーザー → ロール → データグラント」の順に設定し、その後エンドユーザーで接続して動作を確認します。
これらのオブジェクトを作成・管理できる権限が必要です。
Oracle公式:前提条件と設定の概要

5.1 データ所有スキーマとテーブルを用意する

CREATE USER STORE_DEMO NO AUTHENTICATION
  DEFAULT TABLESPACE DATA QUOTA 20M ON DATA;

NO AUTHENTICATION は、このスキーマをパスワードで直接ログインする利用者にはしない設定です。
テーブルを所有するスキーマと、データを利用するエンドユーザーを分けています。

このスキーマに前述の3テーブルを作成し、サンプルデータを投入します。
テーブル作成・データ投入を含むSQLは、添付用の demo_setup_public.sql にまとめました。
パスワード部分は置換用の文字列だけを記載しています。

5.2 ローカル・エンド・ユーザーを作成する

-- <...> は置換用の文字列です。実行前に各ユーザーのパスワードを設定します。
CREATE END USER "store_101"
  IDENTIFIED BY "<PASSWORD_FOR_STORE_101>" SCHEMA STORE_DEMO;
CREATE END USER "store_102"
  IDENTIFIED BY "<PASSWORD_FOR_STORE_102>" SCHEMA STORE_DEMO;
CREATE END USER "hq_ops"
  IDENTIFIED BY "<PASSWORD_FOR_HQ_OPS>" SCHEMA STORE_DEMO;

SCHEMA STORE_DEMO は、利用するスキーマを指定しています。
これだけで、そのスキーマの全データを読めるわけではありません。

ユーザー名は二重引用符で囲み、小文字で作成しています。
接続時も、この小文字の名前を使用します。
store_101 などは実在する個人を表さないデモ用の識別子です。

5.3 データロールとログイン用ロールを作成する

CREATE DATA ROLE DEMO_STORE_ROLE;
CREATE DATA ROLE DEMO_HQ_ROLE;

CREATE ROLE DEMO_DB_LOGIN_ROLE;
GRANT CREATE SESSION TO DEMO_DB_LOGIN_ROLE;
GRANT DEMO_DB_LOGIN_ROLE TO DEMO_STORE_ROLE;
GRANT DEMO_DB_LOGIN_ROLE TO DEMO_HQ_ROLE;

GRANT DATA ROLE DEMO_STORE_ROLE TO "store_101";
GRANT DATA ROLE DEMO_STORE_ROLE TO "store_102";
GRANT DATA ROLE DEMO_HQ_ROLE TO "hq_ops";

店舗担当者2名には同じ店舗用データロールを割り当てます。
どちらの店舗を見せるかは、次のデータグラントの条件で分けます。

DEMO_DB_LOGIN_ROLE は通常のロールで、DBに接続するための CREATE SESSION をまとめています。
データの参照範囲を決めるデータロールとは役割が異なります。

5.4 障害情報へのデータグラントを作成する

店舗用の定義は次のとおりです。

CREATE DATA GRANT STORE_DEMO.DEMO_STORE_INCIDENTS
  AS SELECT (ALL COLUMNS EXCEPT estimated_loss_yen, internal_note)
  ON STORE_DEMO.INCIDENTS
  WHERE assigned_end_user = ORA_END_USER_CONTEXT.username
  TO DEMO_STORE_ROLE;

設定のポイントは2つです。

  • ALL COLUMNS EXCEPT ...:損失見込みと本部メモの2列を除いて参照を許可する。
  • WHERE ...:担当ユーザー名が、現在認証されているエンドユーザー名と一致する行だけを許可する。

ORA_END_USER_CONTEXT.username から、現在のエンドユーザー名を取得します。
このため、店舗ごとにロールやグラントを増やさず、1つの店舗用ルールで今回の2名を扱えます。

本部用は行・列を限定しません。

CREATE DATA GRANT STORE_DEMO.DEMO_HQ_INCIDENTS
  AS SELECT ON STORE_DEMO.INCIDENTS
  TO DEMO_HQ_ROLE;

これらはSELECTの許可です。
更新や削除の許可は追加していません。
データグラントの構文や条件指定については、Oracle公式:Create Data Grantsを参照してください。

5.5 共通情報へのデータグラントを作成する

CREATE DATA GRANT STORE_DEMO.DEMO_STORE_SYSTEMS
  AS SELECT ON STORE_DEMO.SYSTEMS TO DEMO_STORE_ROLE;
CREATE DATA GRANT STORE_DEMO.DEMO_HQ_SYSTEMS
  AS SELECT ON STORE_DEMO.SYSTEMS TO DEMO_HQ_ROLE;

CREATE DATA GRANT STORE_DEMO.DEMO_STORE_RUNBOOKS
  AS SELECT ON STORE_DEMO.RUNBOOKS TO DEMO_STORE_ROLE;
CREATE DATA GRANT STORE_DEMO.DEMO_HQ_RUNBOOKS
  AS SELECT ON STORE_DEMO.RUNBOOKS TO DEMO_HQ_ROLE;

台帳・手順書には店舗による制限を付けません。
これで、データグラントは合計6個です。

5.6 今回の設定範囲

USE_DATA_GRANTS_ONLY は有効化していません。
実DBの DBA_DATA_GRANTS でも false であることを確認しました。
通常のオブジェクト権限によるアクセスまでデータグラントだけに制限する強制設定は、今回の対象外です。

そのため、以下の結果は 今回作成した3名のローカル・エンド・ユーザーに対する動作 として読みます。
「ADMINや別の通常DBユーザーも同じ制限を受ける」とまでは検証していません。

6. 検証方法

3名のエンドユーザーそれぞれで接続し直し、同じ9種類のSELECTを実行しました。
SQLへ利用者ごとの店舗条件を追加したり、取得後にプログラムで機密情報を伏せたりせず、DBが返す値を比較します。

ユーザー 期待する障害情報の範囲 機密2列 台帳・手順書
store_101 店舗Aの2件 NULL 各3件すべて
store_102 店舗Bの2件 NULL 各3件すべて
hq_ops 全店舗6件 実際の値 各3件すべて

元の値は管理者で取得して比較用に保存しました。
エンドユーザーの結果と、期待する行・列・値が一致するかを自動照合しています。
検証中は読み取り専用トランザクションを使用し、DDL・DML・権限変更は実行していません。

結果表の NULL は、DBが返したNULLを明示したものです。
SQLクライアントによっては空欄に見えます。
0円や空文字とは区別してください。

7. 検証結果

検証1:接続したエンドユーザーを確認する

確認したいこと

以降の結果が、意図したユーザーの接続で得られたものかを確認します。

期待する結果

end_user に、接続したユーザー名が表示される。

実行SQL

SELECT ORA_END_USER_CONTEXT.username AS end_user,
       SYS_CONTEXT('USERENV', 'SESSION_USER') AS session_user,
       SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') AS current_schema
FROM dual;

実行結果

end_user session_user current_schema
store_101 XS$NULL STORE_DEMO
store_102 XS$NULL STORE_DEMO
hq_ops XS$NULL STORE_DEMO

考察

今回の接続では SESSION_USER は全員 XS$NULL でした。
従来のDBユーザー名だけで本人を判断せず、エンドユーザーの確認には ORA_END_USER_CONTEXT.username を使うことがポイントです。

検証2:条件なしで検索しても、行・列の制限がかかるか

確認したいこと

WHERE句を付けずに障害情報を検索し、DB側だけで行・列が制限されるかを確認します。

期待する結果

店舗担当には自店舗2件と機密列のNULL、本部には全6件と機密列の実際の値が返る。

実行SQL

SELECT incident_id, store_id, store_name,
       estimated_loss_yen, internal_note
FROM STORE_DEMO.INCIDENTS
ORDER BY incident_id;

実行結果

store_101:

incident_id store_id store_name estimated_loss_yen internal_note
INC-20261014-101-001 101 店舗A NULL NULL
INC-20261014-101-002 101 店舗A NULL NULL

store_102:

incident_id store_id store_name estimated_loss_yen internal_note
INC-20261014-102-001 102 店舗B NULL NULL
INC-20261014-102-002 102 店舗B NULL NULL

hq_ops:

incident_id store_id store_name estimated_loss_yen internal_note
INC-20261014-101-001 101 店舗A 320000 値あり(架空の本部メモ)
INC-20261014-101-002 101 店舗A 0 値あり(架空の本部メモ)
INC-20261014-102-001 102 店舗B 42000 値あり(架空の本部メモ)
INC-20261014-102-002 102 店舗B 3000 値あり(架空の本部メモ)
INC-20261014-103-001 103 店舗C 280000 値あり(架空の本部メモ)
INC-20261014-103-002 103 店舗C 7000 値あり(架空の本部メモ)

本部メモは表示上「値あり」と要約しました。
実際の取得では元データの文章と一致しています。

考察

SQL側で店舗を絞らなくても、参照可能な行に制限されました。
また、機密列は「列自体が消える」のではなく、値がNULLとして返りました。
元データが0円の行も店舗担当にはNULLになるため、NULLを損失0円と解釈してはいけません。

検証3:他店舗を明示的に指定するとどうなるか

確認したいこと

店舗A・B担当が、自分の担当ではない店舗C(103)をSQLで指定した場合を確認します。

期待する結果

店舗担当2名は0件。
本部は店舗Cの2件を取得できる。

実行SQL

SELECT incident_id, store_id, store_name
FROM STORE_DEMO.INCIDENTS
WHERE store_id = '103'
ORDER BY incident_id;

実行結果

店舗担当2名はともに0件。
本部には次の2件が返りました。

incident_id store_id store_name
INC-20261014-103-001 103 店舗C
INC-20261014-103-002 103 店舗C

考察

SQLに他店舗の条件を書いても、許可された範囲は広がりませんでした。
0件という結果だけから、その店舗に障害が存在しないとは判断できない点にも注意が必要です。

検証4:件数や合計金額の集計にも制御が反映されるか

確認したいこと

行を一覧表示するだけでなく、件数・金額を集計した場合の結果を確認します。

期待する結果

店舗担当には自店舗の集計だけが返り、機密列から金額を取得できない。
本部には全店舗の集計が返る。

実行SQL

SELECT store_id, store_name,
       COUNT(*) AS incident_count,
       COUNT(estimated_loss_yen) AS visible_loss_count,
       SUM(estimated_loss_yen) AS loss_sum_yen,
       COUNT(internal_note) AS visible_note_count
FROM STORE_DEMO.INCIDENTS
GROUP BY store_id, store_name
ORDER BY store_id;

実行結果

実行ユーザー store_id store_name incident_count visible_loss_count loss_sum_yen visible_note_count
store_101 101 店舗A 2 0 NULL 0
store_102 102 店舗B 2 0 NULL 0
hq_ops 101 店舗A 2 2 320000 2
hq_ops 102 店舗B 2 2 45000 2
hq_ops 103 店舗C 2 2 287000 2

考察

COUNT(*) は返された行を数えますが、COUNT(列名) はその列がNULLでない行を数えます。
店舗側で2件・0件という違いが出たのはこのためです。
SUM もNULLとなり、金額は得られませんでした。
本部の合計は652,000円です。

検証5:機密列を検索条件に指定するとどうなるか

確認したいこと

損失見込みを表示するだけでなく、「10万円以上」という検索条件に使った場合を確認します。

期待する結果

店舗担当には機密金額を条件にした該当行が返らない。
本部には条件に合う2件が返る。

実行SQL

SELECT incident_id, store_id, estimated_loss_yen
FROM STORE_DEMO.INCIDENTS
WHERE estimated_loss_yen >= 100000
ORDER BY incident_id;

実行結果

店舗担当2名はともに0件。
本部には次の2件が返りました。

incident_id store_id estimated_loss_yen
INC-20261014-101-001 101 320000
INC-20261014-103-001 103 280000

考察

今回の比較条件では、店舗担当が機密金額で該当障害を抽出することもできませんでした。
機密列を検索条件に使うと、見えている自店舗の行であっても0件になり得ます。
この1例だけで、あらゆる検索条件や推測による情報取得を検証できたわけではありません。

検証6:複数テーブルを結合しても制御が維持されるか

確認したいこと

障害情報に台帳と手順書をJOIN(結合)し、システム名や手順書IDを付けて取得します。

期待する結果

店舗担当は自店舗の障害に対応する2件だけを取得し、金額はNULL。
本部は全6件と実際の金額を取得する。

実行SQL

SELECT i.incident_id, i.store_id, s.system_id,
       s.system_name, r.runbook_id, i.estimated_loss_yen
FROM STORE_DEMO.INCIDENTS i
JOIN STORE_DEMO.SYSTEMS s ON s.system_id = i.system_id
JOIN STORE_DEMO.RUNBOOKS r ON r.system_id = i.system_id
ORDER BY i.incident_id, r.runbook_id;

実行結果

ユーザー 取得行数 損失見込み
store_101 2 すべてNULL
store_102 2 すべてNULL
hq_ops 6 元の値と一致

店舗A担当の取得結果を例として示します。

incident_id store_id system_id system_name runbook_id estimated_loss_yen
INC-20261014-101-001 101 POS-CORE-DB 店舗POS共通データベース RB-DEMO-POS-01 NULL
INC-20261014-101-002 101 INVENTORY-SYNC 在庫・発注連携バッチ RB-DEMO-INV-01 NULL

考察

共通テーブルと組み合わせても、障害情報の行制限や機密列の制限は維持されました。
今回のデータでは1システムに手順書が1件なので、JOIN後も障害と同じ行数です。
手順書が複数あれば、結合結果の行数自体は増える可能性があります。

検証7:共通のシステム台帳を全員が参照できるか

確認したいこと

制限すべき情報だけでなく、全員に許可した共通情報も正しく読めるかを確認します。

期待する結果

3名ともシステム台帳の全3件・全5列を参照できる。

実行SQL

SELECT * FROM STORE_DEMO.SYSTEMS
ORDER BY system_id;

実行結果

全員が同じ3件を取得し、全5列が元データと一致しました。
以下はIDと名称の抜粋です。

system_id system_name
INVENTORY-SYNC 在庫・発注連携バッチ
PAYMENT-GW キャッシュレス決済ゲートウェイ
POS-CORE-DB 店舗POS共通データベース

考察

店舗用ロールは、すべてのテーブルが自店舗限定になるわけではありません。
テーブルごとのデータグラントに従い、共通情報は全件を取得できました。

検証8:共通の手順書と長い本文を参照できるか

確認したいこと

手順書の基本情報に加え、CLOB列に保存した本文も取得できるかを確認します。

期待する結果

3名とも全3件・全6列を参照でき、本文も欠けずに取得できる。

実行SQL

SELECT * FROM STORE_DEMO.RUNBOOKS
ORDER BY runbook_id;

実行結果

全員が同じ3件を取得しました。
CLOB本文を含め、全6列が元データと一致しました。
以下は基本情報の抜粋です。

runbook_id system_id version
RB-DEMO-INV-01 INVENTORY-SYNC 1.0-demo
RB-DEMO-PAY-01 PAYMENT-GW 1.0-demo
RB-DEMO-POS-01 POS-CORE-DB 1.0-demo

考察

長い文章を格納するCLOB列も、共通情報として問題なく取得できました。
この記事では表の読みやすさのため本文を省略していますが、検証では本文を含むすべての列を照合しています。

検証9:SELECT *で全列を指定するとどうなるか

確認したいこと

列名を列挙せず、すべての列を表す * を使った場合を確認します。

期待する結果

店舗担当は自店舗2件のみ。
15列は返るが、機密2列の値はNULL。
本部は全6件・全15列の実際の値を取得する。

実行SQL

SELECT * FROM STORE_DEMO.INCIDENTS
ORDER BY incident_id;

実行結果

ユーザー 取得行数 取得列数 機密2列
store_101 2 15 すべてNULL
store_102 2 15 すべてNULL
hq_ops 6 15 元の値と一致

店舗側のその他13列も、許可された行の元データと一致しました。

考察

列名を省略しても、機密列の値を読むことはできませんでした。
一方で、列名や列数まで隠れるわけではありません。
アプリケーション側では、返されたNULLを考慮して表示や計算を設計する必要があります。

8. 総まとめ

今回の検証では、3名に対して同じ9種類のSELECTを実行し、27回のSELECT、29項目の自動照合がすべて成功しました。
照合数は、SQLの本数とは別に、行・列・値の一致などを確認した項目数です。

確認項目 結果
エンドユーザーの識別 接続したユーザー名を確認できた
自店舗の行だけを参照 店舗担当は各2件、本部は全6件
機密列の保護 店舗担当にはNULL、本部には実際の値
他店舗を指定した検索 店舗担当は0件、本部は対象店舗の2件
集計・機密列の条件指定 今回試したSQLでも想定した制御結果
JOIN・SELECT * 行・列の制限が維持された
共通の台帳・手順書 全員が全件・全列を参照できた

設定の流れは、データを用意し、エンドユーザーを作り、データロールを割り当て、データグラントで参照範囲を定義するという順序で整理できました。
設定後に管理者だけで確認するのではなく、実際のエンドユーザーで接続して比較することが大切です。

また、機密列はNULLとして返るため、「値がない」「0円」「権限がなくて見えない」を扱い分ける必要があることも分かりました。
今回のように元データを把握できる小さなサンプルで試すと、アクセス制御の影響を確認しやすくなります。

今回確認したのはローカル・エンド・ユーザー3名のSELECTです。
更新・削除、外部認証連携、接続プールでのユーザー切替、通常のDBユーザーへの強制適用は検証対象に含めていません。
次の段階では、利用する接続方式や必要な操作に合わせて確認範囲を広げたいと思います。

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?