LoginSignup
0
0

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

Posted at

はじめに

どうも僕です。

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

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

Rego とは

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

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

使う場面

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

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

サンプル

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

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()
}
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関数で評価してます。

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の部分で、useraliceでパスが/searchの時だけ許可します。

成功した時
スクリーンショット 2023-08-21 15.24.45.png

失敗した時
スクリーンショット 2023-08-21 15.25.46.png

感想

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

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0