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

Well known attributes #428

Merged
merged 6 commits into from
Oct 9, 2023
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
20 changes: 12 additions & 8 deletions pkg/service/auth_pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -533,8 +533,9 @@ func (pipeline *AuthPipeline) GetResolvedIdentity() (interface{}, interface{}) {
}

type authorizationJSON struct {
Context *envoy_auth.AttributeContext `json:"context"`
AuthData map[string]interface{} `json:"auth"`
// Deprecated: Use WellKnownAttributes instead.
Context *envoy_auth.AttributeContext `json:"context"`
*WellKnownAttributes `json:""`
}

func (pipeline *AuthPipeline) GetAuthorizationJSON() string {
Expand Down Expand Up @@ -573,12 +574,7 @@ func (pipeline *AuthPipeline) GetAuthorizationJSON() string {
authData["callbacks"] = callbacks
}

authJSON, _ := gojson.Marshal(&authorizationJSON{
Context: pipeline.GetRequest().Attributes,
AuthData: authData,
})

return string(authJSON)
return NewAuthorizationJSON(pipeline.GetRequest(), authData)
}

func (pipeline *AuthPipeline) customizeDenyWith(authResult auth.AuthResult, denyWith *evaluators.DenyWithValues) auth.AuthResult {
Expand Down Expand Up @@ -609,3 +605,11 @@ func (pipeline *AuthPipeline) customizeDenyWith(authResult auth.AuthResult, deny

return authResult
}

func NewAuthorizationJSON(request *envoy_auth.CheckRequest, authPipeline map[string]any) string {
authJSON, _ := gojson.Marshal(&authorizationJSON{
Context: request.Attributes,
WellKnownAttributes: NewWellKnownAttributes(request.Attributes, authPipeline),
})
return string(authJSON)
}
21 changes: 19 additions & 2 deletions pkg/service/auth_pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,10 @@ func TestAuthPipelineGetAuthorizationJSON(t *testing.T) {
}, &requestMock)

requestJSON, _ := gojson.Marshal(requestMock.GetAttributes())
expectedJSON := fmt.Sprintf(`{"context":%s,"auth":{"authorization":{},"identity":null,"metadata":{},"response":{}}}`, requestJSON)
assert.Equal(t, pipeline.GetAuthorizationJSON(), expectedJSON)
expectedWellKnownAttributes := `"request":{"host":"my-api","method":"GET","path":"/operation","url_path":"/operation","headers":{"authorization":"Bearer n3ex87bye9238ry8"}},"source":{},"destination":{},"auth":{}`
expectedJSON := fmt.Sprintf(`{"context":%s,%s}`, requestJSON, expectedWellKnownAttributes)

assert.Equal(t, expectedJSON, pipeline.GetAuthorizationJSON())
}

func TestEvaluateWithCustomDenyOptions(t *testing.T) {
Expand Down Expand Up @@ -576,3 +578,18 @@ func BenchmarkAuthPipeline(b *testing.B) {
assert.DeepEqual(b, r.Message, "")
assert.DeepEqual(b, r.Code, rpc.OK)
}

func TestNewAuthorizationJSON(t *testing.T) {
request := &envoy_auth.CheckRequest{}
_ = gojson.Unmarshal([]byte(rawRequest), &request)

authPipeline := map[string]any{
"identity": "leeloo",
"authorization": map[string]any{
"credential": "multipass",
},
}
expectedAuthJSON := `{"context":{"request":{"http":{"method":"GET","headers":{"authorization":"Bearer n3ex87bye9238ry8"},"path":"/operation","host":"my-api"}}},"request":{"host":"my-api","method":"GET","path":"/operation","url_path":"/operation","headers":{"authorization":"Bearer n3ex87bye9238ry8"}},"source":{},"destination":{},"auth":{"identity":"leeloo","authorization":{"credential":"multipass"}}}`

assert.Equal(t, expectedAuthJSON, NewAuthorizationJSON(request, authPipeline))
}
200 changes: 200 additions & 0 deletions pkg/service/well_known_attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
Copyright 2023 Red Hat, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package service

import (
"net/url"
"reflect"
"strings"

envoycore "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoyauth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
"github.com/golang/protobuf/ptypes/timestamp"
)

type WellKnownAttributes struct {
// Dynamic request metadata
Metadata *envoycore.Metadata `json:"metadata,omitempty"`
// Request attributes
Request *RequestAttributes `json:"request,omitempty"`
// Source attributes
Source *SourceAttributes `json:"source,omitempty"`
// Destination attributes
Destination *DestinationAttributes `json:"destination,omitempty"`
// Auth attributes
Auth *AuthAttributes `json:"auth,omitempty"`
}

type RequestAttributes struct {
// Request ID corresponding to x-request-id header value
Id string `json:"id,omitempty"`
// Time of the first byte received
Time *timestamp.Timestamp `json:"time,omitempty"`
// Request protocol (“HTTP/1.0”, “HTTP/1.1”, “HTTP/2”, or “HTTP/3”)
Protocol string `json:"protocol,omitempty"`
// The scheme portion of the URL e.g. “http”
Scheme string `json:"scheme,omitempty"`
// The host portion of the URL e.g. “example.com”
Host string `json:"host,omitempty"`
// Request method e.g. “GET”
Method string `json:"method,omitempty"`
// The path portion of the URL e.g. “/foo?bar=baz”
Path string `json:"path,omitempty"`
// The path portion of the URL without the query string e.g. “/foo”
URLPath string `json:"url_path,omitempty"`
// The query portion of the URL in the format of “name1=value1&name2=value2”
Query string `json:"query,omitempty"`
// All request headers indexed by the lower-cased header name e.g. “accept-encoding”: “gzip”
Headers map[string]string `json:"headers,omitempty"`
// Referer request header e.g. “https://www.kuadrant.io/”
Referer string `json:"referer,omitempty"`
// User agent request header e.g. “Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/…”
UserAgent string `json:"user_agent,omitempty"`
// The HTTP request size in bytes. If unknown, it must be -1 e.g. 1234
Size int64 `json:"size,omitempty"`
// The HTTP request body. (Disabled by default. Requires additional proxy configuration to enabled it.) e.g. “…”
Body string `json:"body,omitempty"`
// The HTTP request body in bytes. This is sometimes used instead of body depending on the proxy configuration. e.g. 1234
RawBody []byte `json:"raw_body,omitempty"`
// This is analogous to request.headers, however these contents are not sent to the upstream server. It provides an
// extension mechanism for sending additional information to the auth service without modifying the proto definition.
// It maps to the internal opaque context in the proxy filter chain. (Requires additional configuration in the proxy.)
ContextExtensions map[string]string `json:"context_extensions,omitempty"`
}

type SourceAttributes struct {
// Downstream connection remote address
Address string `json:"address,omitempty"`
// Downstream connection remote port e.g. 8080
Port int32 `json:"port,omitempty"`
// The canonical service name of the peer e.g. “foo.default.svc.cluster.local”
Service string `json:"service,omitempty"`
// The labels associated with the peer. These could be pod labels for Kubernetes or tags for VMs. The source of the
// labels could be an X.509 certificate or other configuration.
Labels map[string]string `json:"labels,omitempty"`
// The authenticated identity of this peer. If an X.509 certificate is used to assert the identity in the proxy, this
// field is sourced from "URI Subject Alternative Names", "DNS Subject Alternate Names" or "Subject" in that order.
// The format is issuer specific – e.g. SPIFFE format is spiffe://trust-domain/path, Google account format is https://accounts.google.com/{userid}.
Principal string `json:"principal,omitempty"`
// The X.509 certificate used to authenticate the identity of this peer. When present, the certificate contents are encoded in URL and PEM format.
Certificate string `json:"certificate,omitempty"`
}

type DestinationAttributes struct {
// Downstream connection local address
Address string `json:"address,omitempty"`
// Downstream connection local port e.g. 9090
Port int32 `json:"port,omitempty"`
// The canonical service name of the peer e.g. “foo.default.svc.cluster.local”
Service string `json:"service,omitempty"`
// The labels associated with the peer. These could be pod labels for Kubernetes or tags for VMs. The source of the
// labels could be an X.509 certificate or other configuration.
Labels map[string]string `json:"labels,omitempty"`
// The authenticated identity of this peer. If an X.509 certificate is used to assert the identity in the proxy, this
// field is sourced from "URI Subject Alternative Names", "DNS Subject Alternate Names" or "Subject" in that order.
// The format is issuer specific – e.g. SPIFFE format is spiffe://trust-domain/path, Google account format is https://accounts.google.com/{userid}.
Principal string `json:"principal,omitempty"`
// The X.509 certificate used to authenticate the identity of this peer. When present, the certificate contents are encoded in URL and PEM format.
Certificate string `json:"certificate,omitempty"`
}

type AuthAttributes struct {
// Single resolved identity object, post-identity verification
Identity any `json:"identity,omitempty"`
// External metadata fetched
Metadata map[string]any `json:"metadata,omitempty"`
// Authorization results resolved by each authorization rule, access granted only
Authorization map[string]any `json:"authorization,omitempty"`
// Response objects exported by the auth service post-access granted
Response map[string]any `json:"response,omitempty"`
// Response objects returned by the callback requests issued by the auth service
Callbacks map[string]any `json:"callbacks,omitempty"`
}

// NewWellKnownAttributes creates a new WellKnownAttributes object from an envoyauth.AttributeContext
func NewWellKnownAttributes(attributes *envoyauth.AttributeContext, authData map[string]any) *WellKnownAttributes {
return &WellKnownAttributes{
Metadata: attributes.MetadataContext,
Request: newRequestAttributes(attributes),
Source: newSourceAttributes(attributes),
Destination: newDestinationAttributes(attributes),
Auth: newAuthAttributes(authData),
}
}

func newRequestAttributes(attributes *envoyauth.AttributeContext) *RequestAttributes {
request := attributes.GetRequest()
httpRequest := request.GetHttp()
urlParsed, _ := url.Parse(httpRequest.Path)
headers := httpRequest.GetHeaders()
return &RequestAttributes{
Id: httpRequest.Id,
Time: request.Time,
Protocol: httpRequest.Protocol,
Scheme: httpRequest.GetScheme(),
Host: httpRequest.GetHost(),
Method: httpRequest.GetMethod(),
Path: httpRequest.GetPath(),
URLPath: urlParsed.Path,
Query: urlParsed.RawQuery,
Headers: headers,
Referer: headers["referer"],
UserAgent: headers["user-agent"],
Size: httpRequest.GetSize(),
Body: httpRequest.GetBody(),
RawBody: httpRequest.GetRawBody(),
ContextExtensions: attributes.GetContextExtensions(),
}
}

func newSourceAttributes(attributes *envoyauth.AttributeContext) *SourceAttributes {
source := attributes.Source
socketAddress := source.GetAddress().GetSocketAddress()
return &SourceAttributes{
Address: socketAddress.GetAddress(),
Port: int32(socketAddress.GetPortValue()),
Service: source.GetService(),
Labels: source.GetLabels(),
Principal: source.GetPrincipal(),
}
}

func newDestinationAttributes(attributes *envoyauth.AttributeContext) *DestinationAttributes {
destination := attributes.Destination
socketAddress := destination.GetAddress().GetSocketAddress()
return &DestinationAttributes{
Address: socketAddress.GetAddress(),
Port: int32(socketAddress.GetPortValue()),
Service: destination.GetService(),
Labels: destination.GetLabels(),
Principal: destination.GetPrincipal(),
}
}

func newAuthAttributes(authData map[string]interface{}) *AuthAttributes {
authAttributes := &AuthAttributes{}
authAttributesValue := reflect.ValueOf(authAttributes).Elem()
for key, value := range authData {
fieldValue := authAttributesValue.FieldByName(strings.ToUpper(key[:1]) + key[1:])
if fieldValue.IsValid() && fieldValue.CanSet() {
if value != nil {
fieldValue.Set(reflect.ValueOf(value))
}
}
}
return authAttributes
}
54 changes: 54 additions & 0 deletions pkg/service/well_known_attributes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package service

import (
"testing"

envoycore "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoyauth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
"github.com/golang/protobuf/ptypes/timestamp"
"github.com/stretchr/testify/assert"
)

func TestNewWellKnownAttributes(t *testing.T) {
envoyAttrs := &envoyauth.AttributeContext{
MetadataContext: &envoycore.Metadata{},
Request: &envoyauth.AttributeContext_Request{
Http: &envoyauth.AttributeContext_HttpRequest{
Headers: map[string]string{
"referer": "www.kuadrant.io",
"user-agent": "best browser ever",
},
Path: "/force",
Protocol: "HTTP/2.1",
Method: "GET",
},
Time: &timestamp.Timestamp{},
},
Source: &envoyauth.AttributeContext_Peer{
Service: "svc.rebels.local",
},
Destination: &envoyauth.AttributeContext_Peer{
Service: "svc.rogue-1.local",
Labels: map[string]string{"squad": "rogue"},
},
}
authData := map[string]interface{}{
"identity": map[string]any{"user": "luke", "group": "rebels"},
"metadata": map[string]any{"squad": "rogue"},
"authorization": map[string]any{"group": "rebels"},
"response": map[string]any{"status": 200},
}

wellKnownAttributes := NewWellKnownAttributes(envoyAttrs, authData)

assert.Equal(t, "/force", wellKnownAttributes.Request.Path)
assert.Equal(t, "www.kuadrant.io", wellKnownAttributes.Request.Referer)
assert.Equal(t, "best browser ever", wellKnownAttributes.Request.UserAgent)
assert.Equal(t, "svc.rebels.local", wellKnownAttributes.Source.Service)
assert.Equal(t, map[string]string{"squad": "rogue"}, wellKnownAttributes.Destination.Labels)
assert.Equal(t, map[string]any{"user": "luke", "group": "rebels"}, wellKnownAttributes.Auth.Identity)
assert.Equal(t, map[string]any{"squad": "rogue"}, wellKnownAttributes.Auth.Metadata)
assert.Equal(t, map[string]any{"group": "rebels"}, wellKnownAttributes.Auth.Authorization)
assert.Equal(t, map[string]any{"status": 200}, wellKnownAttributes.Auth.Response)
assert.Nil(t, wellKnownAttributes.Auth.Callbacks)
}
Loading