Skip to content
Merged
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
31 changes: 24 additions & 7 deletions envoyauth/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,24 @@ var errInvalidPath = errors.New("invalid parsed path")

// RequestToInput - Converts a CheckRequest in either protobuf 2 or 3 to an input map
func RequestToInput(req any, logger logging.Logger, protoSet *protoregistry.Files, skipRequestBodyParse bool) (map[string]any, error) {
var err error
var input map[string]any

var rawBody []byte
var path, body string
var headers map[string]string
var version ast.Value
// we anticipate sending at most 6 items to the request, so if we hint the size of the map,
// we are less likely to have to grow the map at runtime which can introduce more allocations.
input := make(map[string]any, 6)
var (
err error

rawBody []byte
path, body string
headers map[string]string
version ast.Value

// set the easily retrieved attributes of the source peer, per
// https://www.envoyproxy.io/docs/envoy/v1.34.0/api-v3/service/auth/v3/attribute_context.proto#envoy-v3-api-msg-service-auth-v3-attributecontext-peer
//
// All of these parameters are available in the full `input` proto representation, but we will pull out the source.principal field here so that
// users can easily find it and reference it in policies.
sourcePrincipal string
)

// NOTE: The path/body/headers blocks look silly, but they allow us to retrieve
// the parts of the incoming request we care about, without having to convert
Expand All @@ -56,6 +67,7 @@ func RequestToInput(req any, logger logging.Logger, protoSet *protoregistry.File
body = req.GetAttributes().GetRequest().GetHttp().GetBody()
headers = req.GetAttributes().GetRequest().GetHttp().GetHeaders()
rawBody = req.GetAttributes().GetRequest().GetHttp().GetRawBody()
sourcePrincipal = req.GetAttributes().GetSource().GetPrincipal()
version = v3Info
case *ext_authz_v2.CheckRequest:
var bs []byte
Expand All @@ -68,6 +80,7 @@ func RequestToInput(req any, logger logging.Logger, protoSet *protoregistry.File
path = req.GetAttributes().GetRequest().GetHttp().GetPath()
body = req.GetAttributes().GetRequest().GetHttp().GetBody()
headers = req.GetAttributes().GetRequest().GetHttp().GetHeaders()
sourcePrincipal = req.GetAttributes().GetSource().GetPrincipal()
version = v2Info
}

Expand All @@ -91,6 +104,10 @@ func RequestToInput(req any, logger logging.Logger, protoSet *protoregistry.File
input["truncated_body"] = isBodyTruncated
}

if sourcePrincipal != "" {
input["source_principal"] = sourcePrincipal
}

return input, nil
}

Expand Down
59 changes: 59 additions & 0 deletions envoyauth/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -664,3 +664,62 @@ func TestParsedPathAndQuery(t *testing.T) {
}
}
}

func TestSourcePeerAttributes(t *testing.T) {
var tests = []struct {
input string
expectedSourcePrincipal any
}{
{
input: `{
"attributes": {
"request": {
"http": {
"headers": {
"content-type": "application/grpc"
},
"method": "POST",
"path": "/com.book.BookService/GetBooksViaAuthor",
"protocol": "HTTP/2",
"raw_body": "AAAAAAA="
}
}
}
}`,
expectedSourcePrincipal: nil,
},
{
input: `{
"attributes": {
"source": {
"service": "",
"principal": "spiffe://test-domain/path",
"certificate": ""
},
"request": {
"http": {
"headers": {
"content-type": "application/grpc"
},
"method": "POST",
"path": "/com.book.BookService/GetBooksViaAuthor",
"protocol": "HTTP/2",
"raw_body": "AAAAAAA="
}
}
}
}`,
expectedSourcePrincipal: "spiffe://test-domain/path",
},
}

for i, tt := range tests {
parsed, err := RequestToInput(createCheckRequest(tt.input), nil, nil, false)
if err != nil {
t.Errorf("Unexpected error in test %d: %s", i, err.Error())
}
if parsed["source_principal"] != tt.expectedSourcePrincipal {
t.Errorf("mismatched source principal in test %d: expected %v, got %v", i, tt.expectedSourcePrincipal, parsed["source_principal"])
}
}
}
86 changes: 86 additions & 0 deletions internal/internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,73 @@ func TestCheckDenyWithLogger(t *testing.T) {
}
}

func TestCheckAllowWithSpiffeDecision(t *testing.T) {
request := `{
"attributes": {
"source": {
"principal": "spiffe://test-domain/test"
},
"request": {
"http": {
"id": "13359530607844510314",
"headers": {
"content-type": "application/json"
},
"method": "GET",
"body": "{\"firstname\": \"foo\", \"lastname\": \"bar\", \"dept\": {\"it\": \"eng\"}}",
}
}
}
}`

var req ext_authz.CheckRequest
if err := util.Unmarshal([]byte(request), &req); err != nil {
panic(err)
}

server := testAuthzServerWithSpiffeDecision(nil, withCustomLogger(&testPlugin{}))
ctx := context.Background()
output, err := server.Check(ctx, &req)
if err != nil {
t.Fatal(err)
}
if output.Status.Code != int32(code.Code_OK) {
t.Fatal("Expected request to be allowed but got:", output)
}
}

func TestCheckDenyWithSpiffeDecision(t *testing.T) {
request := `{
"attributes": {
"request": {
"http": {
"id": "13359530607844510314",
"headers": {
"content-type": "application/json"
},
"method": "GET",
"body": "{\"firstname\": \"foo\", \"lastname\": \"bar\", \"dept\": {\"it\": \"eng\"}}",
}
}
}
}`

var req ext_authz.CheckRequest
if err := util.Unmarshal([]byte(request), &req); err != nil {
panic(err)
}

server := testAuthzServerWithSpiffeDecision(nil, withCustomLogger(&testPlugin{}))
ctx := context.Background()
output, err := server.Check(ctx, &req)
if err != nil {
t.Fatal(err)
}
if output.Status.Code != int32(code.Code_PERMISSION_DENIED) {
t.Fatal("Expected request to be denied but got:", output)
}
}

func TestCheckAllowWithLoggerNDBCache(t *testing.T) {
// test server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -2126,6 +2193,25 @@ func testAuthzServer(customConfig *Config, customPluginFuncs ...customPluginFunc
return testAuthzServerWithModule(module, "envoy/authz/result", customConfig, customPluginFuncs...)
}

func testAuthzServerWithSpiffeDecision(customConfig *Config, customPluginFuncs ...customPluginFunc) *envoyExtAuthzGrpcServer {
module := `
package envoy.authz

default allow = false

missingId := true if not input.source_principal

allow if {
not missingId
input.source_principal == "spiffe://test-domain/test"
}

result.allowed = allow
`

return testAuthzServerWithModule(module, "envoy/authz/result", customConfig, customPluginFuncs...)
}

func testAuthzServerWithModule(module string, path string, customConfig *Config, customPluginFuncs ...customPluginFunc) *envoyExtAuthzGrpcServer {
m, err := getPluginManager(module, customPluginFuncs...)
if err != nil {
Expand Down