権限は大丈夫なはずなのにPATCH時に401 Unauthorizedとなる理由と解決方法
TL;DR
RLS(Row Level Security)や列レベル権限でpassword
列を非公開にしていると、Prefer: return=representation
付きのPATCH/DELETEが401 Unauthorizedになることがあります。
原因は、更新後にPostgRESTがSELECT *
を自動実行し、password
列を読み込めず権限エラーになることです。
解決策は、URLに?select=
を付けて返却列を明示的に限定するだけで解消します。
前提
usersテーブル例
create table public.users(
name text primary key,
password text, -- 本番ではハッシュ推奨
money int,
last_access date
);
権限制御の要件
- GET(Read): 誰でもOK
- POST(Create): 誰でもOK
-
PATCH/DELETE(Update/Delete):
x-user-name
とx-user-password
ヘッダが行のname
とpassword
と一致している場合のみOK -
password
列は外部に返さない
RLS および列権限の例
-- RLS有効
alter table public.users enable row level security;
-- password列はSELECTも含め権限を与えない
revoke all on public.users from anon, authenticated;
grant select (name, money, last_access) on public.users to anon, authenticated;
-- UPDATEポリシー
create policy "update when header matches"
on users
for update
using (
name = (current_setting('request.headers', true)::json ->> 'x-user-name')
and
password = (current_setting('request.headers', true)::json ->> 'x-user-password')
);
-- DELETEポリシー
create policy "delete when header matches"
on users
for delete
using (
name = (current_setting('request.headers', true)::json ->> 'x-user-name')
and
password = (current_setting('request.headers', true)::json ->> 'x-user-password')
);
-- INSERT/SELECTも誰でもOK
create policy "public read" on users for select using (true);
create policy "public insert" on users for insert with check (true);
症状:PATCHが401 Unauthorizedになる
以下のような実装で、PATCHリクエスト時に401 Unauthorizedエラーが発生します。
def update_user_data(
name="aa",
password="margomasdfe",
update_fields={"money": 1010}
):
url = f"{SUPABASE_URL}/rest/v1/users?name=eq.{name}"
headers = {
"apikey": SUPABASE_API_KEY,
"Authorization": f"Bearer {SUPABASE_API_KEY}",
"Accept": "application/json",
"Content-Type": "application/json",
"Prefer": "return=representation",
"x-user-name": name,
"x-user-password": password,
}
data = json.dumps(update_fields).encode()
req = urllib.request.Request(url, data=data, headers=headers, method="PATCH")
urllib.request.urlopen(req) # -> HTTP Error 401: Unauthorized
原因
Prefer: return=representation
が付いている場合、
PostgRESTはPATCH成功後に自動でSELECT
(しかもSELECT *
)を実行し、レスポンスに返そうとします。
しかし、password
列のSELECT権限がないため、権限エラー(401 Unauthorized)が発生します。
※ 注意: UPDATEやDELETE自体は成功しているが、返却時のSELECTでエラーが発生します。
解決策:返却列を限定する
password
を除外し、URLに?select=name,money,last_access
のように付けるだけで解決できます。
import urllib.request, urllib.parse, json
def update_user_data_fixed(
name="user_name",
password="password",
update_fields={"money": 1010},
):
url = (
f"{SUPABASE_URL}/rest/v1/users"
f"?name=eq.{urllib.parse.quote(name)}"
"&select=name,money,last_access"
)
headers = {
"apikey": SUPABASE_API_KEY,
"Authorization": f"Bearer {SUPABASE_API_KEY}",
"Accept": "application/json",
"Content-Type": "application/json",
"Prefer": "return=representation",
"x-user-name": name,
"x-user-password": password,
}
req = urllib.request.Request(
url,
data=json.dumps(update_fields).encode(),
headers=headers,
method="PATCH",
)
with urllib.request.urlopen(req) as res:
return json.loads(res.read().decode())
print(update_user_data_fixed())
ポイント
- URLに
?select=name,money,last_access
と返却列を限定することで、PostgRESTがアップデート後のSELECTでpassword
列を読み込まなくなり、401 Unauthorizedエラーを回避できます。
まとめ
Supabase/PostgRESTでPrefer: return=representation
付きのPATCH/DELETEを実行する際、権限制御で非公開なカラムがある場合はURLに?select=
を追加して返却列を限定してください。
これにより、「更新はできるのに401 Unauthorized」が発生する問題を解消できます。