SolidJS で Supabase の Row Level Security を試してみた

以前、

- **[Flutter から Supabase の PostgreSQL with PostGIS を使ってみる](https://qiita.com/hmatsu47/items/c3f9cafb499aedaca1f1)**

で Flutter によるモバイルアプリから Supabase を使ってみましたが、今回は **[SolidJS](https://www.solidjs.com/)** から **Supabase の行レベルセキュリティ（Row Level Security）** を試してみました。

:::note warn
**2022/12/6 追記：**
GitHub リポジトリ側を supabase-js v2 にバージョンアップした関係で、リンク先のソースコードとは行番号や内容が噛み合わなくなっています。
（後日、この記事を supabase-js v2 に合わせて修正する予定）
:::

※前編にあたる記事は[こちら](https://zenn.dev/hmatsu47/articles/solidjs-suid-sample)です。

- **[SolidJS で Material-UI（SUID）を試してみた](https://zenn.dev/hmatsu47/articles/solidjs-suid-sample)**

https://zenn.dev/hmatsu47/articles/solidjs-suid-sample

**2022/6/1 追記：**
続き（補足）を書きました。

- **[SolidJS で Supabase の Row Level Security を試してみた…の続き（補足）](https://qiita.com/hmatsu47/items/774a3ab9441fe8eb96c7)**

https://qiita.com/hmatsu47/items/774a3ab9441fe8eb96c7

## 試したバージョン

- **[SolidJS](https://github.com/solidjs/solid)** : 1.4.3
- **[SUID](https://github.com/swordev/suid)** : 0.4.1
- **[supabase-js](https://github.com/supabase/supabase-js)** : 1.35.3

## 内容

**[Supabase](https://supabase.io)** のドキュメント、

- **[Quickstart: SolidJS](https://supabase.com/docs/guides/with-solidjs)**

https://supabase.com/docs/guides/with-solidjs

を **[SUID](https://suid.io/)** で Material-UI 化しつつ、カード表示による簡易投稿機能を実装し、

- 1 : ログインユーザ本人のみ登録・表示・編集・削除が可能
- 2 : ログインユーザ以外も表示が可能
- 3 : ログインユーザ以外も表示・編集が可能

の切り替えを Row Level Security で実現するものです。

コード全体は[こちら](https://github.com/hmatsu47/pgunconf-sample-app)のリポジトリに置いてあります。

- **[pgunconf-sample-app](https://github.com/hmatsu47/pgunconf-sample-app)**

https://github.com/hmatsu47/pgunconf-sample-app

※リポジトリのタイトルにあるとおり、2022/5/31 開催予定の **[第 33 回 PostgreSQL アンカンファレンス＠オンライン](https://pgunconf.connpass.com/event/243796/)** 向けに用意したサンプルです。デモの都合で GitHub ログイン機能も追加実装しています。

## 準備

前掲リポジトリの [`README.md`](https://github.com/hmatsu47/pgunconf-sample-app) に書いた内容と重複しますが、基本的には以下のとおりです。

- **[Quickstart: SolidJS](https://supabase.com/docs/guides/with-solidjs)** に記載の流れで準備を進める
- ただし、以下の点についてはリポジトリの内容で置き換える
  - アプリケーションの初期化（`npx degit`するときに TypeScript 向けの指定を行う）
  - DB のテーブル定義
  - アプリケーションコード
    - ~~SolidJS ではデフォルトで Vite を使う関係上、手順どおり進めた場合`supabaseClient.tsx`で`process.env.XXX`を使っても`.env`ファイルの内容は読めないので`import.meta.env.XXX`に変更する（Vue 3 と同じ）~~
      - ~~[Issue #6807](https://github.com/supabase/supabase/issues/6807) を立てて様子を見ているところ~~→修正されました

### Supabase で新規プロジェクトを作成

- **[Project set up](https://supabase.com/docs/guides/with-solidjs#project-set-up)**
  - API Key・URL を確認しておく（後で`.env`ファイルに記述）

### アプリケーション初期化

- **[Building the App](https://supabase.com/docs/guides/with-solidjs#building-the-app)**

```sh:初期化
npx degit solidjs/templates/ts pgunconf-sample-app
cd pgunconf-sample-app
npm i
npm install @supabase/supabase-js
npm install @suid/material
npm install @suid/icons-material
npm install @suid/types
```

（`@suid/types`は不要だが念のため）

### `.env`ファイルを`pgunconf-sample-app`直下に作成

```text:.env
VITE_SUPABASE_URL=【Supabase の Settings → API に表示されている Configuration の URL】
VITE_SUPABASE_ANON_KEY=【同ページ Project API keys の anon・public キー】
```

### Supabase の SQL Editor でテーブル・ストレージなどの定義を流し込む

流し込む内容は[こちら](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/db-create.sql)を参照してください。

- **[db-create.sql](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/db-create.sql)**

### ログイン用に外部のプロバイダ設定を行う（必要であれば）

Quickstart ではメールで送信した Magic Link をクリックするログイン方法を使っていますが、GitHub や Twitter など外部の認証プロバイダを利用したログインも可能です。併用もできます（各アカウントはメールアドレスをキーに「名寄せ」されます）。

- **[Login with GitHub](https://supabase.com/docs/guides/auth/auth-github)** （英語）

https://supabase.com/docs/guides/auth/auth-github

- **[GitHubでログイン](https://www.supabase.jp/docs/guides/auth/auth-github)** （日本語訳）

https://www.supabase.jp/docs/guides/auth/auth-github

※なお、GitHub ではアプリケーションの URL として http://localhost:3000 が登録可能ですが、Twitter などでは入力チェックでエラーになります。

### アプリケーションコードを実行

コードは[前掲リポジトリ](https://github.com/hmatsu47/pgunconf-sample-app)を参照してください。

- **[pgunconf-sample-app](https://github.com/hmatsu47/pgunconf-sample-app)**

```sh:開発環境で実行
npm run dev
```

## 解説

Supabase はデータベースとして（というよりプラットフォームとして）PostgreSQL を使用していますので、Row Level Security も PostgreSQL の機能で実現しています。

- **[Row Level Security](https://supabase.com/docs/guides/auth/row-level-security)**（英語）

https://supabase.com/docs/guides/auth/row-level-security 

- **[行単位セキュリティ](https://www.supabase.jp/docs/guides/auth/row-level-security)** （日本語訳）

https://www.supabase.jp/docs/guides/auth/row-level-security

※これ以降、リンクは主に日本語訳を示します。

### データベース側

#### テーブル定義

##### profiles テーブル（ユーザのプロフィール情報を保管）

**[db-create.sql](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/db-create.sql#L1)**（1 行目〜）

```sql:profilesテーブルの定義
create table profiles (
  id uuid references auth.users not null,
  updated_at timestamp with time zone,
  username text unique,
  avatar_url text,
  website text,

  primary key (id),
  unique(username),
  constraint username_length check (char_length(username) >= 3)
);
```

`auth.users`が認証ユーザのテーブルです。`id`列で外部キー制約を付けています。

##### articles テーブル（投稿情報を保管）

**[db-create.sql](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/db-create.sql#L47)**（47 行目〜）

```sql:articlesテーブルの定義
create table articles (
  id bigint generated by default as identity,
  updated_at timestamp with time zone,
  title text not null,
  note text,
  note_type int not null default 1,
  userid uuid not null,

  primary key (id),
  constraint title_length check (char_length(title) > 0)
);
（中略）
alter table articles add foreign key (userid) references profiles;
```

アプリケーションコードでクエリビルダを使う際にデータ行を結合して`SELECT`するために、`userid`列で外部キー制約を付けています。

#### 認証

Magic Link のクリックや SNS など外部のプロバイダによる認証を経由することで、Supabase のデータベース（PostgreSQL）では以下のヘルパー関数でユーザ判別が可能になります。

- **[ヘルパー関数](https://www.supabase.jp/docs/guides/auth/row-level-security#%E3%83%98%E3%83%AB%E3%83%91%E3%83%BC%E9%96%A2%E6%95%B0)**

> auth.uid()
> リクエストを行ったユーザーのIDを返します。
>
> auth.role()
> リクエストを行ったユーザーのロールを返します。ほとんどの場合、これはauthenticated（認証済み）またはanon（匿名）のいずれかです。
> 
> auth.email()
> リクエストを行ったユーザーのメール・アドレスを返します。

#### Row Level Security による認可

`enable row level security`によって対象テーブルの Row Level Security を有効にし、`create policy`で各操作の実行条件を指定するイメージです。

##### profiles テーブル

**[db-create.sql](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/db-create.sql#L13)**（13 行目〜）

```sql:profileテーブルのRLS
alter table profiles enable row level security;

create policy "Public profiles are viewable by everyone."
  on profiles for select
  using ( true );

create policy "Users can insert their own profile."
  on profiles for insert
  with check ( auth.uid() = id );

create policy "Users can update their own profile."
  on profiles for update
  using ( auth.uid() = id );
```

- 参照（`SELECT`）は制限なし（誰でも読める）
- 挿入（`INSERT`）時に実行ユーザと挿入する主キー列（`id`）の一致をチェック
- 更新（`UPDATE`）時に実行ユーザと更新対象行の主キー列（`id`）の一致をチェック

##### articles テーブル

**[db-create.sql](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/db-create.sql#L59)**（59 行目〜）

```sql:articlesテーブルのRLS
alter table articles enable row level security;

create policy "Users can view their own articles or disclosed articles."
  on articles for select
  using ( ( auth.uid() = articles.userid ) or ( note_type between 2 and 3 ) );

create policy "Users can insert their own articles."
  on articles for insert
  with check ( auth.uid() = articles.userid );

create policy "Users can update their own articles or free-updatable articles."
  on articles for update
  using ( ( auth.uid() = articles.userid ) or ( note_type = 3 ) );

create policy "Users can delete their own articles."
  on articles for delete
  using ( ( auth.uid() = articles.userid ) );
```

- 参照（`SELECT`）時に以下を`OR`条件でチェック
  - 実行ユーザと投稿者列（`userid`）の一致
  - 投稿の「他のユーザに許可する操作」が「`2`:読み取りのみ」か「`3`:読み取りと編集」
- 挿入（`INSERT`）時に実行ユーザと挿入する投稿者列（`userid`）の一致をチェック
- 更新（`UPDATE`）時に以下を`OR`条件でチェック
  - 実行ユーザと投稿者（`userid`）の一致
  - 投稿の「他のユーザに許可する操作」が「`3`:読み取りと編集」
  - 実はこれらだけでは不十分ですが（`userid`列値を投稿者以外に上書き更新できるため）、今回は内容を簡単にするためにこのラインにとどめています
    - **2022/5/29 追記：**[リポジトリ](https://github.com/hmatsu47/pgunconf-sample-app)は修正版になっています
      - [db-create.sql](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/db-create.sql)
      - [EditItem.tsx](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/src/EditItem.tsx)
      - [List.tsx](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/src/List.tsx)
    - **2022/6/1 追記：**[続き（補足）](https://qiita.com/hmatsu47/items/774a3ab9441fe8eb96c7)の記事を書きました。
      - **[SolidJS で Supabase の Row Level Security を試してみた…の続き（補足）](https://qiita.com/hmatsu47/items/774a3ab9441fe8eb96c7)**
- 削除（`DELETE`）時に実行ユーザと削除対象行の投稿者列（`userid`）の一致をチェック

### アプリケーション側

#### 接続クライアント生成

supabase-js の`createClient()`で生成します。

- **[supabaseClient.tsx](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/src/commons/supabaseClient.tsx)**

```tsx:接続クライアント生成
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

export const supabase = createClient(supabaseUrl!, supabaseAnonKey!);
```

#### 認証

##### マジックリンク送信

接続クライアントを使って`supabase.auth.signIn({ email: 【メールアドレス】 })`すると Magic Link 付きのメールを送信します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123899/23a8a3eb-c1c2-486a-c0e0-620039ea4566.png)

- **[Auth.tsx](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/src/Auth.tsx#L32)**（32 行目〜）

```tsx:マジックリンク送信
const { error } = await supabase.auth.signIn({ email: email() });
```

##### 外部プロバイダによる認証

接続クライアントを使って`supabase.auth.signIn({ email: 【メールアドレス】 })`すると Magic Link 付きのメールを送信します。

- **[Auth.tsx](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/src/Auth.tsx#L54)**（54 行目〜）

```tsx:外部プロバイダで認証
const { error } = await supabase.auth.signIn({ provider: provider });
```

GitHub を使う場合、`provider`として`github`（文字列）を渡します。

##### 認証後

接続クライアントの`supabase.auth.onAuthStateChange()`によってセッションを取得し、`Signal`（React の`State`に相当）にセットします。

- **[App.tsx](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/src/App.tsx#L27)**（27 行目〜）

```tsx:セッションをSignalにセット
createEffect(() => {
  setSession(supabase.auth.session());
（1行省略）
  // 認証状態が変化したら Session を更新
  supabase.auth.onAuthStateChange((_event, session) => {
    setSession(session);
（1行省略）
  })
})
```

#### 認可

こちらが参照（`select()`）のリファレンスです。左側メニューから更新・削除なども見ることができます。

- **[Fetch data: select()](https://supabase.com/docs/reference/javascript/select)**

https://supabase.com/docs/reference/javascript/select

以降、参照と挿入（`insert()`）・更新（`update()`）のみピックアップして例示します。

##### 参照（`select()`）

特にコードで制限をかけなくても、Row Level Security で許可されたテーブル・行だけ取得できます。

- **[List.tsx](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/src/List.tsx#L39)**（39 行目〜）

```tsx:RLSによる参照の制限
const { data, error, status } = await supabase
  .from("articles")
  .select(
    `
    id,
    updated_at,
    title,
    note,
    note_type,
    userid,
    profiles (
      username,
      avatar_url
    )
  `
  )
  .order("updated_at", { ascending: false });
```

例えば、こんな感じのユーザがいて、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123899/e5c7d3c9-5dd8-ddb7-2b50-9f58616f29ee.png)
こんな感じの投稿データが登録されている場合に、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123899/8352f1a2-3ffb-d14e-9f5e-f1636d11952c.png)
「hmatsu47」さんがログインした画面を見ると、`id`列が`18`の行 **以外** が表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123899/46d46a43-22e9-71dc-f0cf-5fa1364cebdf.png)

##### 挿入（`insert()`）・更新（`update()`）

- **[EditItem.tsx](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/src/EditItem.tsx#L116)**（116 行目〜）

```tsx:挿入・更新
const { data, error } = await (isInsert
  ? supabase.from("articles").insert(updates)
  : supabase
      .from("articles")
      .update(updates)
      .match({ id: props.article!.id }));
```

※ここでは`insert()`・`update()`を使っていますが、挿入時の主キー列を明示的に指定できる場合は`upsert()`も使えます（プロフィールの更新では`upsert()`を使っています）。

例えば先ほどの例で、

- [**`ViewItem.tsx`** の 98 〜 101 行目](https://github.com/hmatsu47/pgunconf-sample-app/blob/main/src/ViewItem.tsx#L98)をコメントアウトするか、ブラウザの開発者ツールで編集アイコン（ペン）の`disabled`を外して編集アイコンをクリックできるようにした上で、
- `id`が`19`の行（画面では上から 2 つ目）を「hmatsu47」さんが編集して、
- 「更新」ボタンを押す

と、Row Level Security で指定したチェックに引っかかるため、エラーが出て投稿は更新されません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123899/2e26da62-d925-7e32-7789-8135714a7b45.png)

## その他

ここで例示した以外にも[高度なポリシー](https://www.supabase.jp/docs/guides/auth/row-level-security#%E9%AB%98%E5%BA%A6%E3%81%AA%E3%83%9D%E3%83%AA%E3%82%B7%E3%83%BC)が設定できるので、必要に応じて試してみると良いでしょう。

- **[高度なポリシー](https://www.supabase.jp/docs/guides/auth/row-level-security#%E9%AB%98%E5%BA%A6%E3%81%AA%E3%83%9D%E3%83%AA%E3%82%B7%E3%83%BC)**

https://www.supabase.jp/docs/guides/auth/row-level-security#%E9%AB%98%E5%BA%A6%E3%81%AA%E3%83%9D%E3%83%AA%E3%82%B7%E3%83%BC