権限は大丈夫なはずなのにPATCH時に401 Unauthorizedとなる理由と解決方法

# 権限は大丈夫なはずなのにPATCH時に401 Unauthorizedとなる理由と解決方法

## TL;DR
RLS（Row Level Security）や列レベル権限で`password`列を非公開にしていると、`Prefer: return=representation`付きのPATCH/DELETEが**401 Unauthorized**になることがあります。  
原因は、更新後にPostgRESTが`SELECT *`を自動実行し、`password`列を読み込めず権限エラーになることです。  
解決策は、URLに`?select=`を付けて返却列を明示的に限定するだけで解消します。

## 前提
### usersテーブル例
```sql
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 および列権限の例
```sql
-- 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エラーが発生します。

```python
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`のように付けるだけで解決できます。

```python
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」が発生する問題を解消できます。