🙌

アクセスコントロールSaaS Permit.io でWeb APIのアクセス制御を実装してみた

に公開

みなさん、こんにちは。

近年のOpenFGACedarなどのアクセスコントロールの統一した規格が登場してきており、注目を集めています。

今回はそんな中、イスラエルにある会社が提供している Permit.IO という SaaS 型のアクセスコントロールサービスを利用してみたので、その紹介をしたいと思います。

アクセスコントロール界隈(余談)

一言にアクセスコントロールと言っても、様々なシチュエーションとアプローチがあるかと思います。

古くは Unix の User/Group に対するファイルパーミッションから認証システムに付随するユーザーのアクセス制御機能、近年では認証基盤が提供している RBAC(Role-Based Access Control)や ABAC(Attribute-Based Access Control)などがあります。
中でも、Google の Zanzibar や Amazon の Cedar などのアプローチが注目されています。

下記の記事では、主要なポリシーエンジンの開発者同士がそれぞれの特徴を比較して対談しているので、興味がある方はぜひ読んでみてください。
これらはまだ若いエコシステムで、今後の進化が期待される分野です。

https://www.permit.io/blog/policy-engine-showdown-opa-vs-openfga-vs-cedar

やってみた

今回は、Permit.io の無料プランにサインアップし、チュートリアルを実施してみました。

Workspace の作成

まずは Permit.io にサインアップし、Workspace を作成します。
無料プランである程度利用できるので、気軽に試すことができます。

「Getting Started」がチュートリアルになっているので、こちらを順番に進めていきます。

alt text

Policy および Resource の作成

Policy はアクセスコントロールのルールを定義するもので、Resource はアクセス対象となるリソース自体を定義します。

Policy を作るためにはまず Resource を作成する必要があるので、Resource から作成していきます。

alt text

リソースは今回 Blog として作成しました。
ブログの CRUD 操作に対して、Web API でアクセス制御を行う想定です。

alt text

Resource を作成したら、自動でデフォルトの Policy を作成してくれます。

alt text

admin editor viewer の 3 つの Role が作成され、それぞれに対して CRUD 操作の権限が割り当てられています。

alt text

ユーザーの作成

次にユーザーを登録します。
一旦このユーザーにはadminロールを割り当てておきます。

サンプルアプリを動かす

次に、Permit.io が提供しているサンプルアプリを動かしてみます。
言語やプラットフォームのサポートもかなり充実しています。

alt text

Node.js では Express.js を利用した下記のようなサンプルコードが提供されていました。
固定ユーザーの WebAPI アクセスに対して、アクセス権限を Permit.io に問い合わせているだけのシンプルなコードです。

const { Permit } = require("permitio");

const express = require("express");
const app = express();
const port = 4000;

// This line initializes the SDK and connects your Node.js app
// to the Permit.io PDP container you've set up in the previous step.
const permit = new Permit({
  // in production, you might need to change this url to fit your deployment
  pdp: "https://cloudpdp.api.permit.io",
  // your api key
  token:
    "permit_key_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
});

// You can open http://localhost:4000 to invoke this http
// endpoint, and see the outcome of the permission check.
app.get("/", async (req, res) => {
  // This user was defined by you in the previous step and
  // is already assigned with a role in the permission system.
  const user = {
    id: "seiichi1101",
    firstName: "Seiichi",
    lastName: "Arai",
    email: "undefined",
  }; // in a real app, you would typically decode the user id from a JWT token

  // After we created this user in the previous step, we also synced the user's identifier
  // to permit.io servers with permit.write(permit.api.syncUser(user)). The user identifier
  // can be anything (email, db id, etc) but must be unique for each user. Now that the
  // user is synced, we can use its identifier to check permissions with 'permit.check()'.
  const permitted = await permit.check("seiichi1101", "read", "Blog");
  if (permitted) {
    res
      .status(200)
      .send(
        `${user.firstName} ${user.lastName} is PERMITTED to 'read' 'Blog' !`
      );
  } else {
    res
      .status(403)
      .send(
        `${user.firstName} ${user.lastName} is NOT PERMITTED to 'read' 'Blog' !`
      );
  }
});

app.listen(port, () => {
  console.log("Example app listening at http://localhost:" + port);
});

ページ下部の「Test Connection」で実際にアプリが Permit.io と通信できるか確認できます。

alt text

Hono アプリで試してみる

これだけだと面白くないので、Hono アプリに認証機能を追加して、Permit.io を利用してみます。

GitHub: https://github.com/seiichi1101/permitio-hono

メインのコードは下記の通り。
ベーシック認証を通過したユーザーに対して、アクセス権限を Permit.io に問い合わせています。

ここでのポイントは、HTTP のmethodpathをそれぞれ、actionresourceにマッピングしている点です。

import { type Context, Hono } from "hono";
import { basicAuth } from "hono/basic-auth";
import { Permit } from "permitio";

const app = new Hono<{ Bindings: Env }>();
let permit: Permit | undefined;

type Env = {
  PERMITIO_TOKEN: string;
};
type Action = "read" | "create" | "delete" | "update";
type Resource = "Blog";
type Permission = {
  action: Action;
  resource: Resource;
};

const urlMapping = (method: string, path: string): Permission => {
  const mapping: { [key: string]: Permission } = {
    "get-/auth/blog": { action: "read", resource: "Blog" },
    "post-/auth/blog": { action: "create", resource: "Blog" },
    "delete-/auth/blog": { action: "delete", resource: "Blog" },
    "put-/auth/blog": { action: "update", resource: "Blog" },
  };
  const key = `${method}-${path}`;
  return mapping[key];
};

app.use(
  "/auth/*",
  basicAuth({
    username: "seiichi1101",
    password: "password",
  })
);

// Access Control Middleware
app.use("/auth/*", async (c, next) => {
  const path = c.req.path;
  const method = c.req.method.toLowerCase();
  const requiredPermissions = urlMapping(method, path);
  const authHeader = c.req.header("Authorization");
  const username = atob(authHeader?.substring(6) || "").split(":")[0];

  if (!permit) {
    permit = new Permit({
      pdp: "https://cloudpdp.api.permit.io",
      token: c.env.PERMITIO_TOKEN,
    });
  }

  const hasPermission = await permit.check(
    username,
    requiredPermissions.action,
    requiredPermissions.resource
  );

  if (!hasPermission) {
    return c.text("Forbidden", 403);
  }

  return next();
});

app.get("/auth/blog", (c) => {
  return c.text(
    `You are authorized to access: ${c.req.method} - ${c.req.path}`
  );
});

app.post("/auth/blog", (c) => {
  return c.text(
    `You are authorized to access: ${c.req.method} - ${c.req.path}`
  );
});

app.delete("/auth/blog", (c) => {
  return c.text(
    `You are authorized to access: ${c.req.method} - ${c.req.path}`
  );
});

app.put("/auth/blog", (c) => {
  return c.text(
    `You are authorized to access: ${c.req.method} - ${c.req.path}`
  );
});

export default app;

npm run devで起動し、curlでアクセスしてみます。

# read Blog
$ curl -X GET localhost:8787/auth/blog -u seiichi1101:password -w "\n"
You are authorized to access: GET - /auth/blog

# create Blog
$ curl -X POST localhost:8787/auth/blog -u seiichi1101:password -w "\n"
You are authorized to access: POST - /auth/blog

# delete Blog
$ curl -X DELETE localhost:8787/auth/blog -u seiichi1101:password -w "\n"
You are authorized to access: DELETE - /auth/blog

# update Blog
$ curl -X PUT localhost:8787/auth/blog -u seiichi1101:password -w "\n"
You are authorized to access: PUT - /auth/blog

すべての操作が許可されました。

Role を変更してみる

次に、Permit.io のコンソールからユーザーの Role をviewerに変更してみます。

その後、再度curlでアクセスしてみます。

# read Blog
$ curl -X GET localhost:8787/auth/blog -u seiichi1101:password -w "\n"
You are authorized to access: GET - /auth/blog

# create Blog
$ curl -X POST localhost:8787/auth/blog -u seiichi1101:password -w "\n"
Forbidden

# delete Blog
$ curl -X DELETE localhost:8787/auth/blog -u seiichi1101:password -w "\n"
Forbidden

# update Blog
$ curl -X PUT localhost:8787/auth/blog -u seiichi1101:password -w "\n"
Forbidden

GET 以外の操作が拒否されましたね。
Permit.io のポリシーで設定した通りに動作しています。

まとめ

いかがだったでしょうか。

Permit.io は SaaS 型のアクセスコントロールサービスであり、簡単にアクセス制御を実装できる点が魅力的でした。
コンソールもモダンな UI で使いやすく、ポリシーの管理も直感的に行えます。
また、様々な言語やフレームワークに対応しており、既存のアプリケーションにも容易に組み込むことができます。

今後もアクセスコントロールの分野は進化が期待されるので、引き続き注目していきたいと思います。

備忘録

試しながら気づいた点や疑問に思った点をメモしておきます。(今後のネタになればと)

PermitIO のサンプル集

https://docs.permit.io/category/learn-by-example

Prisma 連携

https://docs.permit.io/sdk/permit-prisma-extension/

ユーザーの同期

https://docs.permit.io/how-to/sync-users/

PDP(Policy Decision Point)のローカルホスティング

アプリケーションが自前でホストする PDP にアクセスすることが可能

PDP はコンテナとして提供されており、サイドカーで動作させることが可能

https://docs.permit.io/concepts/pdp/overview/

GitHubで編集を提案

Discussion