diff --git a/pkg/service/auth_pipeline.go b/pkg/service/auth_pipeline.go index dd50ea32..7e2478ab 100644 --- a/pkg/service/auth_pipeline.go +++ b/pkg/service/auth_pipeline.go @@ -534,8 +534,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 { @@ -574,12 +575,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 { @@ -610,3 +606,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) +} diff --git a/pkg/service/auth_pipeline_test.go b/pkg/service/auth_pipeline_test.go index 767fc701..446ddad7 100644 --- a/pkg/service/auth_pipeline_test.go +++ b/pkg/service/auth_pipeline_test.go @@ -317,8 +317,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) { @@ -577,3 +579,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)) +} diff --git a/pkg/service/well_known_attributes.go b/pkg/service/well_known_attributes.go new file mode 100644 index 00000000..2c2d3fbc --- /dev/null +++ b/pkg/service/well_known_attributes.go @@ -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 +} diff --git a/pkg/service/well_known_attributes_test.go b/pkg/service/well_known_attributes_test.go new file mode 100644 index 00000000..96b67952 --- /dev/null +++ b/pkg/service/well_known_attributes_test.go @@ -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: ×tamp.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) +}