Regoを使ってAPIのアクセス制限をしてみる

# はじめに
どうも僕です。

先日、セキュリティキャンプがありましたね。そこで面白そうなものがありました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">セキュリティ・キャンプ全国大会2023年のB7クラスで「 Policy as Code入門」の講義を担当させていただきます。今年はありがたいことに講義時間が去年に比べ倍になったので、コンテンツもがっつり増量しました<br><br>資料も公開しておくのでご興味ある方はご笑覧ください <a href="https://twitter.com/hashtag/seccamp?src=hash&amp;ref_src=twsrc%5Etfw">#seccamp</a><a href="https://t.co/nyor8qsle8">https://t.co/nyor8qsle8</a></p>&mdash; mizutani (@m_mizutani) <a href="https://twitter.com/m_mizutani/status/1688735994387079169?ref_src=twsrc%5Etfw">August 8, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

講師の方が資料を公開してくれていましたね。面白そうだったので読んでいきます。

# Rego とは

Regoとは`Open Policy Agent(OPA)`のポリシーの定義言語のこと

OPAはコードにおける**ポリシー**を決定するための汎用的なエンジンで、Regoはその中でポリシーを記述する際に使う。

## 使う場面
k8sやTerraformなどで使われることがあるみたいです。

使用する場面的にはAPIより環境全体に適用できるような場面に使用するのが、メリットを発揮できるのかなーといった感じです。


# サンプル
今回はk8sのような環境ではなく素直にAPIとして実装します。

```go:main.go
package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/open-policy-agent/opa/rego"
)

func main() {
	r := gin.Default()
	r.LoadHTMLGlob("*.html")
	r.GET("/", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.html", gin.H{"content": "Hello, World"})
	})
	r.GET("/search", authorize, func(c *gin.Context) {
		c.JSON(200, gin.H{"result": "success"})
	})
	r.Run(":8080")
}
func authorize(c *gin.Context) {
	input := map[string]interface{}{
		"user": c.GetHeader("X-User"),
		"path": c.FullPath(),
	}

	r := rego.New(
		rego.Query("allows"),
		rego.Package("api.authz"),
		rego.Module("example.rego",
			`package api.authz

             default allow = false

             allows {
                 input.user == "alice"
                 input.path == "/search"
             }`),
	)

	ctx := context.Background()
	query, err := r.PrepareForEval(ctx)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to prepare rego query"})
		c.Abort()
		return
	}

	rs, err := query.Eval(ctx, rego.EvalInput(input))
	if err != nil || len(rs) == 0 || !rs[0].Expressions[0].Value.(bool) {
		c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
		c.Abort()
		return
	}
	fmt.Println(rs)
	c.Next()
}
```

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
    <input type="text" id="usernameInput" placeholder="Username...">
    <button id="searchBtn">Search</button>
    <div id="result"></div>

    <script>
document.getElementById('searchBtn').addEventListener('click', function() {
    var username = document.getElementById('usernameInput').value;

    fetch('/search', {
        headers: {
            'X-User': username
        }
    })
    .then(response => {
        // HTTPステータスコードが非2xxの場合
        if (!response.ok) {
            // レスポンスボディを解析してエラーメッセージを抽出
            return response.json().then(err => {
                throw new Error(err.error);
            });
        }
        return response.json();  // 正常なレスポンスをJSONとして解析
    })
    .then(data => {
        document.getElementById('result').innerText = data.result;
    })
    .catch(err => {
        console.log(err);
        document.getElementById('result').innerText = err.message;
    });
});
    </script>
</body>
</html>
```

HTML側で入力した名前をgoの`authorize`関数で評価してます。

```go:rego
r := rego.New(
		rego.Query("allows"),
		rego.Package("api.authz"),
		rego.Module("example.rego",
			`package api.authz

             default allow = false

             allows {
                 input.user == "alice"
                 input.path == "/search"
             }`),
	)
```

ここがRegoの部分で、`user`が`alice`でパスが`/search`の時だけ許可します。

成功した時
![スクリーンショット 2023-08-21 15.24.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/450534/fd430a8c-876f-0d38-8dfb-78ded3bfc874.png)

失敗した時
![スクリーンショット 2023-08-21 15.25.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/450534/f187295d-db9f-e14c-abc9-454f203ebf2f.png)


# 感想

正直、**API**にRegoを使うのはどうなのか？という疑問があったが、DBに保存しない値を評価したい場合には使えるなと思います。

他にも、DBにはデータがあるけど、それを読み取らなくても良いデータ（例えばロールとか）を使用したい場合にもいいかなと思いました。


goだとソースコードに直接かけるんで、使う場面が来たら実装は楽だなと思います。