为什么使用自定义函数?
当内置运算或函数不能表达规则时,SPDL 条件可以调用已配置的函数。Speedle 向其 REST 端点发送 POST {"params":[...]},预期返回 {"result":...} 或 {"error":"..."}。参数顺序、类型和返回类型必须与条件一致。函数失败是评估错误,应用应拒绝访问。
启动本地示例
示例接受 100 至 50000 之间的数值。参数类型错误或列表为空时返回 HTTP 错误。只有回环 IP 可以使用明文,其他监听地址必须提供 TLS。
在单独终端运行:
go run ./website/tests/examples/custom-functionpackage main
import (
"encoding/json"
"flag"
"log"
"net"
"net/http"
"time"
)
type request struct {
Params []interface{} `json:"params"`
}
type response struct {
Result interface{} `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
func isValid(w http.ResponseWriter, r *http.Request) {
send := func(status int, value response) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(value)
}
if r.Method != http.MethodPost {
w.Header().Set("Allow", "POST")
send(405, response{Error: "POST required"})
return
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20))
decoder.DisallowUnknownFields()
var input request
if err := decoder.Decode(&input); err != nil || len(input.Params) == 0 {
send(400, response{Error: "params must contain at least one number"})
return
}
valid := true
for _, param := range input.Params {
value, ok := param.(float64)
if !ok {
send(400, response{Error: "all params must be numbers"})
return
}
if value < 100 || value > 50000 {
valid = false
}
}
send(200, response{Result: valid})
}
func main() {
listen := flag.String("listen", "127.0.0.1:23456", "Function listen address")
cert := flag.String("cert", "", "TLS server certificate")
key := flag.String("key", "", "TLS server key")
flag.Parse()
if (*cert == "") != (*key == "") {
log.Fatal("provide both --cert and --key")
}
mux := http.NewServeMux()
mux.HandleFunc("/func/isValid", isValid)
server := &http.Server{Addr: *listen, Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second}
if *cert != "" {
log.Fatal(server.ListenAndServeTLS(*cert, *key))
}
host, _, err := net.SplitHostPort(*listen)
if err != nil || !net.ParseIP(host).IsLoopback() {
log.Fatal("plaintext examples require a loopback IP; provide TLS certificates for other addresses")
}
log.Fatal(server.ListenAndServe())
}运行示例前
先按快速入门编译仓库根目录下的 ./bin/spctl、./bin/speedle-pms 和 ./bin/speedle-ads,需要 Go 1.26.10 或 Go 1.27.3(也可使用同一大版本的更新补丁版本)、Bash、curl 和 Python 3。下面各终端都从仓库根目录运行。为本页使用一个新的策略文件;仅在两个服务都停止后重建该文件。
初始化空策略文件:
printf '{}\n' > docs-policies.json终端一启动 PMS,并保持运行:
./bin/speedle-pms --store-type file --filestore-loc ./docs-policies.json --endpoint 127.0.0.1:6733 --grpc-endpoint 127.0.0.1:0 --insecure=true终端二启动 ADS,并保持运行:
./bin/speedle-ads --store-type file --filestore-loc ./docs-policies.json --endpoint 127.0.0.1:6734 --grpc-endpoint 127.0.0.1:0 --insecure=true在第三个终端运行后续命令。PMS/ADS 共享同一个文件,策略更新通过 watch 异步到达 ADS;先查询确认已更新,再依赖新策略做决策。--insecure=true 仅用于回环地址上的本地示例,生产连接见安全配置。示例显式指定 PMS 地址,不修改 ~/.spctlconfig。完成后在两个服务终端按 Ctrl-C。
注册并调用函数
以下定义关闭结果缓存,使每次决策反映本次请求。先创建服务,再创建策略。
cat > function.json <<'JSON'
{"name":"isValid","funcURL":"http://127.0.0.1:23456/func/isValid","resultCachable":false}
JSON
./bin/spctl --pms-endpoint http://127.0.0.1:6733/policy-mgmt/v1/ create function --json-file function.json
./bin/spctl --pms-endpoint http://127.0.0.1:6733/policy-mgmt/v1/ create service service1
./bin/spctl --pms-endpoint http://127.0.0.1:6733/policy-mgmt/v1/ create policy -c "grant user Ally access library if isValid(attr1)" --service-name service1以下两条请求分别返回 Allow(200 在范围内)、Deny(50 不在范围内):
curl --fail --silent --show-error -H 'Content-Type: application/json' \
-d '{"subject":{"principals":[{"type":"user","name":"Ally"}]},"serviceName":"service1","resource":"library","action":"access","attributes":[{"name":"attr1","type":"numeric","value":200}]}' \
http://127.0.0.1:6734/authz-check/v1/is-allowedcurl --fail --silent --show-error -H 'Content-Type: application/json' \
-d '{"subject":{"principals":[{"type":"user","name":"Ally"}]},"serviceName":"service1","resource":"library","action":"access","attributes":[{"name":"attr1","type":"numeric","value":50}]}' \
http://127.0.0.1:6734/authz-check/v1/is-allowedTLS 与缓存
TLS 端点先生成新的本地测试证书,然后运行 go run ./website/tests/examples/custom-function --cert ./tls/server.crt --key ./tls/server.key。注册时 funcURL 为 https://localhost:23456/func/isValid,ca 填写 tls/ca.crt 中的 PEM 证书内容,不是文件路径。生产证书必须覆盖实际主机。无需使用过期的硬编码证书或私钥。
resultCachable 默认为 false;启用缓存后,resultTTL 表示结果缓存秒数。只有当结果在相应时间和参数组合下仍然正确时才应缓存,例如账号状态等外部数据可能在 TTL 到期前变化。定义管理见 PMS 函数 API。浏览器 Playground 不支持调用外部自定义函数。