はじめに
どうも僕です。
先日、セキュリティキャンプがありましたね。そこで面白そうなものがありました。
セキュリティ・キャンプ全国大会2023年のB7クラスで「 Policy as Code入門」の講義を担当させていただきます。今年はありがたいことに講義時間が去年に比べ倍になったので、コンテンツもがっつり増量しました
— mizutani (@m_mizutani) August 8, 2023
資料も公開しておくのでご興味ある方はご笑覧ください #seccamphttps://t.co/nyor8qsle8
講師の方が資料を公開してくれていましたね。面白そうだったので読んでいきます。
Rego とは
RegoとはOpen Policy Agent(OPA)
のポリシーの定義言語のこと
OPAはコードにおけるポリシーを決定するための汎用的なエンジンで、Regoはその中でポリシーを記述する際に使う。
使う場面
k8sやTerraformなどで使われることがあるみたいです。
使用する場面的にはAPIより環境全体に適用できるような場面に使用するのが、メリットを発揮できるのかなーといった感じです。
サンプル
今回はk8sのような環境ではなく素直にAPIとして実装します。
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()
}
<!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
関数で評価してます。
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
の時だけ許可します。
感想
正直、APIにRegoを使うのはどうなのか?という疑問があったが、DBに保存しない値を評価したい場合には使えるなと思います。
他にも、DBにはデータがあるけど、それを読み取らなくても良いデータ(例えばロールとか)を使用したい場合にもいいかなと思いました。
goだとソースコードに直接かけるんで、使う場面が来たら実装は楽だなと思います。