Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support opa stage, support opa with custom deny message, error #1262

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions plugins/wasm-go/extensions/opa/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,47 @@ package main

import (
"errors"
"net/http"
"strings"

"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/tidwall/gjson"
)

type OpaConfig struct {
policy string
timeout uint32

// the result json path, which must be a boolean value
resultPath string
// whether execute on request headers
skipHeader bool
// whether execute on request body
skipBody bool

// for some cases, we need to send custom deny message
denyCodePath string
denyMappingMessages map[string]string
denyMessageContenType string

// opa not 200
no200Message string
no200Code uint32
no200ContenType string

// after authz, allow add extra headers by result path
// eg: {"result.user_id": "x-user-real-id"}
// get result.user-realid from opa response, and add to request header x-user-realid
extratHeaders map[string]string

client wrapper.HttpClient
}

const (
defaultResultPath = "result"
)

func Client(json gjson.Result) (wrapper.HttpClient, error) {
serviceSource := strings.TrimSpace(json.Get("serviceSource").String())
serviceName := strings.TrimSpace(json.Get("serviceName").String())
Expand Down Expand Up @@ -80,3 +108,58 @@ func Client(json gjson.Result) (wrapper.HttpClient, error) {
}
return nil, errors.New("unknown service source: " + serviceSource)
}

func (config OpaConfig) rspCall(statusCode int, _ http.Header, responseBody []byte) {
if statusCode != http.StatusOK {
proxywasm.LogWarnf("opa policy failed , status code %d, responseBody %s", statusCode, responseBody)
if config.no200Message != "" {
proxywasm.SendHttpResponseWithDetail(
config.no200Code,
"opa.status_ne_200",
contentType(config.no200ContenType),
[]byte(config.no200Message),
-1,
)
return
} else {
proxywasm.SendHttpResponseWithDetail(uint32(statusCode), "opa.status_ne_200", nil, []byte("opa state not is 200"), -1)
}
return
}

ok := gjson.GetBytes(responseBody, config.resultPath).Bool()
if !ok {
proxywasm.LogDebugf("opa policy failed , raw opa response %s", responseBody)
if config.denyCodePath != "" {
denyCode := gjson.GetBytes(responseBody, config.denyCodePath).String()
denyMessage := config.denyMappingMessages[denyCode]
if denyMessage == "" {
denyMessage = "opa server not allowed"
}
proxywasm.SendHttpResponseWithDetail(
http.StatusUnauthorized,
"opa.server_not_allowed",
contentType(config.denyMessageContenType),
[]byte(denyMessage),
-1,
)
} else {
proxywasm.SendHttpResponseWithDetail(http.StatusUnauthorized, "opa.server_not_allowed", nil, []byte("opa server not allowed"), -1)
}
return
}
if len(config.extratHeaders) > 0 {
for k, v := range config.extratHeaders {
rv := gjson.GetBytes(responseBody, k).String()
if rv != "" {
proxywasm.LogDebugf("opa add header %s: %s", v, rv)
proxywasm.AddHttpRequestHeader(v, rv)
}
}
}
proxywasm.ResumeHttpRequest()
}

func contentType(ct string) [][2]string {
return [][2]string{{"Content-Type", ct}}
}
5 changes: 1 addition & 4 deletions plugins/wasm-go/extensions/opa/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a h1:luYRvxLTE1xYxrXYj7nmjd1U0HHh8pUPiKfdZ0MhCGE=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240318034951-d5306e367c43/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
Expand Down
120 changes: 84 additions & 36 deletions plugins/wasm-go/extensions/opa/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
Expand Down Expand Up @@ -58,6 +57,48 @@ func parseConfig(json gjson.Result, config *OpaConfig, log wrapper.Log) error {
return errors.New("timeout parse fail: " + err.Error())
}

config.resultPath = json.Get("resultPath").String()
if config.resultPath == "" {
config.resultPath = defaultResultPath
}

config.skipHeader = json.Get("skipHeader").Bool()
config.skipBody = json.Get("skipBody").Bool()

config.denyCodePath = json.Get("denyCodePath").String()
config.denyMappingMessages = make(map[string]string)
if config.denyCodePath != "" {
denyMappingMessages := json.Get("denyMappingMessages").Map()
for k, v := range denyMappingMessages {
config.denyMappingMessages[k] = v.String()
}
if len(config.denyMappingMessages) == 0 {
return errors.New("denyMappingMessages not allow empty when denyCodePath not empty")
}
config.denyMessageContenType = json.Get("denyMessageContenType").String()
if config.denyMessageContenType == "" {
return errors.New("denyMessageContenType not allow empty when denyCodePath not empty")
}
}

config.no200Message = json.Get("no200Message").String()
if config.no200Message != "" {
config.no200Code = uint32(json.Get("no200Code").Int())
config.no200ContenType = json.Get("no200ContenType").String()
if config.no200ContenType == "" {
return errors.New("no200ContenType not allow empty when no200Message not empty")
}
if config.no200Code == 0 {
return errors.New("no200Code not allow empty when no200Message not empty")
}
}

config.extratHeaders = make(map[string]string)
extratHeaders := json.Get("extratHeaders").Map()
for k, v := range extratHeaders {
config.extratHeaders[k] = v.String()
}

var uint32Duration uint32

if duration.Milliseconds() > int64(^uint32(0)) {
Expand All @@ -76,59 +117,66 @@ func parseConfig(json gjson.Result, config *OpaConfig, log wrapper.Log) error {
return nil
}

const (
OPACtxKeyHeaders = "headers"
OPACtxKeyMethod = "method"
OPACtxKeyScheme = "scheme"
OPACtxKeyPath = "path"
OPACtxKeyQuery = "query"
)

func setCtx(ctx wrapper.HttpContext) {
p, _ := url.Parse(ctx.Path())
headers, _ := proxywasm.GetHttpRequestHeaders()
ctx.SetContext(OPACtxKeyMethod, ctx.Method())
ctx.SetContext(OPACtxKeyScheme, ctx.Scheme())
ctx.SetContext(OPACtxKeyPath, p.Path)
ctx.SetContext(OPACtxKeyQuery, p.RawQuery)
ctx.SetContext(OPACtxKeyQuery, p.RawQuery)
ctx.SetContext(OPACtxKeyHeaders, headers)
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config OpaConfig, log wrapper.Log) types.Action {
if config.skipHeader {
if !config.skipBody {
setCtx(ctx)
}
if len(config.extratHeaders) > 0 {
return types.HeaderStopIteration
}
return types.ActionContinue
}
setCtx(ctx)
return opaCall(ctx, config, nil, log)
}

func onHttpRequestBody(ctx wrapper.HttpContext, config OpaConfig, body []byte, log wrapper.Log) types.Action {
if config.skipBody {
return types.ActionContinue
}
return opaCall(ctx, config, body, log)
}

func opaCall(ctx wrapper.HttpContext, config OpaConfig, body []byte, log wrapper.Log) types.Action {
request := make(map[string]interface{}, 6)
headers, _ := proxywasm.GetHttpRequestHeaders()
request["headers"] = ctx.GetContext(OPACtxKeyHeaders)
request["method"] = ctx.GetContext(OPACtxKeyMethod)
request["scheme"] = ctx.GetContext(OPACtxKeyScheme)
request["path"] = ctx.GetContext(OPACtxKeyPath)
request["query"] = ctx.GetContext(OPACtxKeyQuery)

request["method"] = ctx.Method()
request["scheme"] = ctx.Scheme()
request["path"] = ctx.Path()
request["headers"] = headers
if len(body) != 0 {
request["body"] = body
}
parse, _ := url.Parse(ctx.Path())
query, _ := url.ParseQuery(parse.RawQuery)
request["query"] = query

data, _ := json.Marshal(Metadata{Input: map[string]interface{}{"request": request}})
if err := config.client.Post(fmt.Sprintf("/v1/data/%s/allow", config.policy),
[][2]string{{"Content-Type", "application/json"}},
data, rspCall, config.timeout); err != nil {
opaUrl := fmt.Sprintf("/v1/data/%s/allow", config.policy)
if config.resultPath != "" {
opaUrl = fmt.Sprintf("/v1/data/%s", config.policy)
}
if err := config.client.Post(opaUrl, [][2]string{{"Content-Type", "application/json"}}, data, config.rspCall, config.timeout); err != nil {
log.Errorf("client opa fail %v", err)
return types.ActionPause
}
return types.ActionPause
}

func rspCall(statusCode int, _ http.Header, responseBody []byte) {
if statusCode != http.StatusOK {
proxywasm.SendHttpResponseWithDetail(uint32(statusCode), "opa.status_ne_200", nil, []byte("opa state not is 200"), -1)
return
}
var rsp map[string]interface{}
if err := json.Unmarshal(responseBody, &rsp); err != nil {
proxywasm.SendHttpResponseWithDetail(http.StatusInternalServerError, "opa.bad_response_body", nil, []byte(fmt.Sprintf("opa parse rsp fail %+v", err)), -1)
return
}

result, ok := rsp["result"].(bool)
if !ok {
proxywasm.SendHttpResponseWithDetail(http.StatusInternalServerError, "opa.conversion_fail", nil, []byte("rsp type conversion fail"), -1)
return
}

if !result {
proxywasm.SendHttpResponseWithDetail(http.StatusUnauthorized, "opa.server_not_allowed", nil, []byte("opa server not allowed"), -1)
return
}
proxywasm.ResumeHttpRequest()
}
Loading