一覧SD-06-2 · 認可とテナント分離

マルチテナントのアクセス制御

一つの入口だけでなく、検索から保存までテナント境界を一貫させる方法を学びます。

説明

テナント分離では、一覧画面だけでなく、件数、詳細取得、更新などすべての経路へ同じ境界を適用します。要求のtenant値を信用するのではなく、認証主体の業務上の所属から対象を絞ります。

同じ題名reportを持つredとblueの文書を使います。一覧、件数、詳細、更新の条件を比較し、別テナントのIDを指定しても情報や変更が届かないことを確認します。

一つの入口だけでなく、検索から保存までテナント境界を一貫させる方法を学びます。

1所属と同名文書を読む

aliceはred、bobはblueに所属します。reportという題名は両テナントに存在します。

from sd06 import * db = database() print(db.execute("SELECT * FROM memberships認証主体の所属を取得する ORDER BY subject").fetchall()) print(db.execute("SELECT id,tenant,title同名文書の配置を確認する FROM docs ORDER BY id").fetchall())

同じ検索語でも、主体の所属に応じて返るIDが変わる資料です。

2一覧検索を所属で制限する

検索語reportを同じにし、主体だけを変更します。

print(listing(db,"alice")red所属の一覧を検索する) print(listing(db,"bob")blue所属で同じ検索をする)

aliceには7、bobには8です。titleの条件だけではなく、所属のJOINが検索範囲を決めています。

3件数にも同じ条件を適用する

全体件数をそのまま返すと、見せないテナントの存在や増減を漏らす場合があります。

print(db.execute("SELECT COUNT(*) FROM docs WHERE全体の件数を比較用に読む title='report'").fetchone()) print(db.execute("SELECT COUNT(*) FROM docs d JOIN memberships m ON m.tenant=d.tenant WHERE m.subject=?主体の所属で集計を制限する AND d.title=?",("alice","report")).fetchone())

全体は2、alice向けは1です。返す行だけでなく、件数や集計も同じ範囲へ制限します。

4詳細IDの指定にも所属を適用する

一覧では見えないID8を直接指定します。

print(detail(db,"alice",7)) print(detail(db,"alice",8)一覧にない他テナントIDを直接指定する)

7は取得でき、8は空です。一覧を絞っても詳細がIDだけの検索なら境界を越えるため、詳細にも所属条件を含めます。

5更新時も対象行を制限する

aliceが文書8を変更しようとします。

print(tenant_edit(db,"alice",8他テナントの更新を要求する,"leaked-change")) print(db.execute("SELECT body FROM docs WHERE id=8").fetchone())

更新0でblue-secretを保ちます。取得時だけでなく、更新するSQLにも所属を含めています。

6自テナントの対象は変更できる

同じaliceが文書7を変更します。

print(tenant_edit(db,"alice",7所属内の対象を更新する,"red-revised")) print(detail(db,"alice",7)) print(detail(db,"bob",8)別テナントの保存値を照合する)

7は更新され、bobの8は元のままです。この例のtenant_editは所属内の編集を許す規則で、さらに操作別権限が必要な業務では条件を追加します。

7所属を解除した後の全入口を確認する

aliceの所属を削除し、同じ一覧・詳細・更新を要求します。

db.execute("DELETE FROM memberships主体の所属を解除する WHERE subject='alice'") print(listing(db,"alice")一覧への反映を確認する,detail(db,"alice",7),tenant_edit(db,"alice",7更新にも反映する,"after-revoke")) print(db.execute("SELECT body FROM docs WHERE id=7").fetchone())

一覧と詳細は空、更新0でred-revisedを保ちます。所属の変更がすべての入口へ反映されました。

まとめ

所属は認証主体の業務情報から取得します。

一覧・件数・詳細・更新へ同じテナント境界を適用します。

所属の失効後に、すべての入口と保存状態を再確認します。

操作履歴と解説を振り返れます。

Python 3.15 / SQLite / 固定の主体・資源・時刻

1
from sd06 import * db = database() print(db.execute("SELECT * FROM memberships ORDER BY subject").fetchall()) print(db.execute("SELECT id,tenant,title FROM docs ORDER BY id").fetchall())
[('alice', 'red'), ('bob', 'blue')]
[(7, 'red', 'report'), (8, 'blue', 'report'), (9, 'red', 'memo')]
2
print(listing(db,"alice")) print(listing(db,"bob"))
[(7, 'report')]
[(8, 'report')]
3
print(db.execute("SELECT COUNT(*) FROM docs WHERE title='report'").fetchone()) print(db.execute("SELECT COUNT(*) FROM docs d JOIN memberships m ON m.tenant=d.tenant WHERE m.subject=? AND d.title=?",("alice","report")).fetchone())
(2,)
(1,)
4
print(detail(db,"alice",7)) print(detail(db,"alice",8))
[(7, 'red-secret')]
[]
5
print(tenant_edit(db,"alice",8,"leaked-change")) print(db.execute("SELECT body FROM docs WHERE id=8").fetchone())
0
('blue-secret',)
6
print(tenant_edit(db,"alice",7,"red-revised")) print(detail(db,"alice",7)) print(detail(db,"bob",8))
1
[(7, 'red-revised')]
[(8, 'blue-secret')]
7
db.execute("DELETE FROM memberships WHERE subject='alice'") print(listing(db,"alice"),detail(db,"alice",7),tenant_edit(db,"alice",7,"after-revoke")) print(db.execute("SELECT body FROM docs WHERE id=7").fetchone())
[] [] 0
('red-revised',)

演習完了