Skip to content

Commit 15dd344

Browse files
Merge pull request #428 from Kuadrant/well_known_attrs
Well known attributes
2 parents ecf0b52 + 18c9da2 commit 15dd344

File tree

4 files changed

+285
-10
lines changed

4 files changed

+285
-10
lines changed

pkg/service/auth_pipeline.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -534,8 +534,9 @@ func (pipeline *AuthPipeline) GetResolvedIdentity() (interface{}, interface{}) {
534534
}
535535

536536
type authorizationJSON struct {
537-
Context *envoy_auth.AttributeContext `json:"context"`
538-
AuthData map[string]interface{} `json:"auth"`
537+
// Deprecated: Use WellKnownAttributes instead.
538+
Context *envoy_auth.AttributeContext `json:"context"`
539+
*WellKnownAttributes `json:""`
539540
}
540541

541542
func (pipeline *AuthPipeline) GetAuthorizationJSON() string {
@@ -574,12 +575,7 @@ func (pipeline *AuthPipeline) GetAuthorizationJSON() string {
574575
authData["callbacks"] = callbacks
575576
}
576577

577-
authJSON, _ := gojson.Marshal(&authorizationJSON{
578-
Context: pipeline.GetRequest().Attributes,
579-
AuthData: authData,
580-
})
581-
582-
return string(authJSON)
578+
return NewAuthorizationJSON(pipeline.GetRequest(), authData)
583579
}
584580

585581
func (pipeline *AuthPipeline) customizeDenyWith(authResult auth.AuthResult, denyWith *evaluators.DenyWithValues) auth.AuthResult {
@@ -610,3 +606,11 @@ func (pipeline *AuthPipeline) customizeDenyWith(authResult auth.AuthResult, deny
610606

611607
return authResult
612608
}
609+
610+
func NewAuthorizationJSON(request *envoy_auth.CheckRequest, authPipeline map[string]any) string {
611+
authJSON, _ := gojson.Marshal(&authorizationJSON{
612+
Context: request.Attributes,
613+
WellKnownAttributes: NewWellKnownAttributes(request.Attributes, authPipeline),
614+
})
615+
return string(authJSON)
616+
}

pkg/service/auth_pipeline_test.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,10 @@ func TestAuthPipelineGetAuthorizationJSON(t *testing.T) {
317317
}, &requestMock)
318318

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

324326
func TestEvaluateWithCustomDenyOptions(t *testing.T) {
@@ -577,3 +579,18 @@ func BenchmarkAuthPipeline(b *testing.B) {
577579
assert.DeepEqual(b, r.Message, "")
578580
assert.DeepEqual(b, r.Code, rpc.OK)
579581
}
582+
583+
func TestNewAuthorizationJSON(t *testing.T) {
584+
request := &envoy_auth.CheckRequest{}
585+
_ = gojson.Unmarshal([]byte(rawRequest), &request)
586+
587+
authPipeline := map[string]any{
588+
"identity": "leeloo",
589+
"authorization": map[string]any{
590+
"credential": "multipass",
591+
},
592+
}
593+
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"}}}`
594+
595+
assert.Equal(t, expectedAuthJSON, NewAuthorizationJSON(request, authPipeline))
596+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
Copyright 2023 Red Hat, Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package service
18+
19+
import (
20+
"net/url"
21+
"reflect"
22+
"strings"
23+
24+
envoycore "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
25+
envoyauth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
26+
"github.com/golang/protobuf/ptypes/timestamp"
27+
)
28+
29+
type WellKnownAttributes struct {
30+
// Dynamic request metadata
31+
Metadata *envoycore.Metadata `json:"metadata,omitempty"`
32+
// Request attributes
33+
Request *RequestAttributes `json:"request,omitempty"`
34+
// Source attributes
35+
Source *SourceAttributes `json:"source,omitempty"`
36+
// Destination attributes
37+
Destination *DestinationAttributes `json:"destination,omitempty"`
38+
// Auth attributes
39+
Auth *AuthAttributes `json:"auth,omitempty"`
40+
}
41+
42+
type RequestAttributes struct {
43+
// Request ID corresponding to x-request-id header value
44+
Id string `json:"id,omitempty"`
45+
// Time of the first byte received
46+
Time *timestamp.Timestamp `json:"time,omitempty"`
47+
// Request protocol (“HTTP/1.0”, “HTTP/1.1”, “HTTP/2”, or “HTTP/3”)
48+
Protocol string `json:"protocol,omitempty"`
49+
// The scheme portion of the URL e.g. “http”
50+
Scheme string `json:"scheme,omitempty"`
51+
// The host portion of the URL e.g. “example.com”
52+
Host string `json:"host,omitempty"`
53+
// Request method e.g. “GET”
54+
Method string `json:"method,omitempty"`
55+
// The path portion of the URL e.g. “/foo?bar=baz”
56+
Path string `json:"path,omitempty"`
57+
// The path portion of the URL without the query string e.g. “/foo”
58+
URLPath string `json:"url_path,omitempty"`
59+
// The query portion of the URL in the format of “name1=value1&name2=value2”
60+
Query string `json:"query,omitempty"`
61+
// All request headers indexed by the lower-cased header name e.g. “accept-encoding”: “gzip”
62+
Headers map[string]string `json:"headers,omitempty"`
63+
// Referer request header e.g. “https://www.kuadrant.io/”
64+
Referer string `json:"referer,omitempty"`
65+
// User agent request header e.g. “Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/…”
66+
UserAgent string `json:"user_agent,omitempty"`
67+
// The HTTP request size in bytes. If unknown, it must be -1 e.g. 1234
68+
Size int64 `json:"size,omitempty"`
69+
// The HTTP request body. (Disabled by default. Requires additional proxy configuration to enabled it.) e.g. “…”
70+
Body string `json:"body,omitempty"`
71+
// The HTTP request body in bytes. This is sometimes used instead of body depending on the proxy configuration. e.g. 1234
72+
RawBody []byte `json:"raw_body,omitempty"`
73+
// This is analogous to request.headers, however these contents are not sent to the upstream server. It provides an
74+
// extension mechanism for sending additional information to the auth service without modifying the proto definition.
75+
// It maps to the internal opaque context in the proxy filter chain. (Requires additional configuration in the proxy.)
76+
ContextExtensions map[string]string `json:"context_extensions,omitempty"`
77+
}
78+
79+
type SourceAttributes struct {
80+
// Downstream connection remote address
81+
Address string `json:"address,omitempty"`
82+
// Downstream connection remote port e.g. 8080
83+
Port int32 `json:"port,omitempty"`
84+
// The canonical service name of the peer e.g. “foo.default.svc.cluster.local”
85+
Service string `json:"service,omitempty"`
86+
// The labels associated with the peer. These could be pod labels for Kubernetes or tags for VMs. The source of the
87+
// labels could be an X.509 certificate or other configuration.
88+
Labels map[string]string `json:"labels,omitempty"`
89+
// The authenticated identity of this peer. If an X.509 certificate is used to assert the identity in the proxy, this
90+
// field is sourced from "URI Subject Alternative Names", "DNS Subject Alternate Names" or "Subject" in that order.
91+
// The format is issuer specific – e.g. SPIFFE format is spiffe://trust-domain/path, Google account format is https://accounts.google.com/{userid}.
92+
Principal string `json:"principal,omitempty"`
93+
// The X.509 certificate used to authenticate the identity of this peer. When present, the certificate contents are encoded in URL and PEM format.
94+
Certificate string `json:"certificate,omitempty"`
95+
}
96+
97+
type DestinationAttributes struct {
98+
// Downstream connection local address
99+
Address string `json:"address,omitempty"`
100+
// Downstream connection local port e.g. 9090
101+
Port int32 `json:"port,omitempty"`
102+
// The canonical service name of the peer e.g. “foo.default.svc.cluster.local”
103+
Service string `json:"service,omitempty"`
104+
// The labels associated with the peer. These could be pod labels for Kubernetes or tags for VMs. The source of the
105+
// labels could be an X.509 certificate or other configuration.
106+
Labels map[string]string `json:"labels,omitempty"`
107+
// The authenticated identity of this peer. If an X.509 certificate is used to assert the identity in the proxy, this
108+
// field is sourced from "URI Subject Alternative Names", "DNS Subject Alternate Names" or "Subject" in that order.
109+
// The format is issuer specific – e.g. SPIFFE format is spiffe://trust-domain/path, Google account format is https://accounts.google.com/{userid}.
110+
Principal string `json:"principal,omitempty"`
111+
// The X.509 certificate used to authenticate the identity of this peer. When present, the certificate contents are encoded in URL and PEM format.
112+
Certificate string `json:"certificate,omitempty"`
113+
}
114+
115+
type AuthAttributes struct {
116+
// Single resolved identity object, post-identity verification
117+
Identity any `json:"identity,omitempty"`
118+
// External metadata fetched
119+
Metadata map[string]any `json:"metadata,omitempty"`
120+
// Authorization results resolved by each authorization rule, access granted only
121+
Authorization map[string]any `json:"authorization,omitempty"`
122+
// Response objects exported by the auth service post-access granted
123+
Response map[string]any `json:"response,omitempty"`
124+
// Response objects returned by the callback requests issued by the auth service
125+
Callbacks map[string]any `json:"callbacks,omitempty"`
126+
}
127+
128+
// NewWellKnownAttributes creates a new WellKnownAttributes object from an envoyauth.AttributeContext
129+
func NewWellKnownAttributes(attributes *envoyauth.AttributeContext, authData map[string]any) *WellKnownAttributes {
130+
return &WellKnownAttributes{
131+
Metadata: attributes.MetadataContext,
132+
Request: newRequestAttributes(attributes),
133+
Source: newSourceAttributes(attributes),
134+
Destination: newDestinationAttributes(attributes),
135+
Auth: newAuthAttributes(authData),
136+
}
137+
}
138+
139+
func newRequestAttributes(attributes *envoyauth.AttributeContext) *RequestAttributes {
140+
request := attributes.GetRequest()
141+
httpRequest := request.GetHttp()
142+
urlParsed, _ := url.Parse(httpRequest.Path)
143+
headers := httpRequest.GetHeaders()
144+
return &RequestAttributes{
145+
Id: httpRequest.Id,
146+
Time: request.Time,
147+
Protocol: httpRequest.Protocol,
148+
Scheme: httpRequest.GetScheme(),
149+
Host: httpRequest.GetHost(),
150+
Method: httpRequest.GetMethod(),
151+
Path: httpRequest.GetPath(),
152+
URLPath: urlParsed.Path,
153+
Query: urlParsed.RawQuery,
154+
Headers: headers,
155+
Referer: headers["referer"],
156+
UserAgent: headers["user-agent"],
157+
Size: httpRequest.GetSize(),
158+
Body: httpRequest.GetBody(),
159+
RawBody: httpRequest.GetRawBody(),
160+
ContextExtensions: attributes.GetContextExtensions(),
161+
}
162+
}
163+
164+
func newSourceAttributes(attributes *envoyauth.AttributeContext) *SourceAttributes {
165+
source := attributes.Source
166+
socketAddress := source.GetAddress().GetSocketAddress()
167+
return &SourceAttributes{
168+
Address: socketAddress.GetAddress(),
169+
Port: int32(socketAddress.GetPortValue()),
170+
Service: source.GetService(),
171+
Labels: source.GetLabels(),
172+
Principal: source.GetPrincipal(),
173+
}
174+
}
175+
176+
func newDestinationAttributes(attributes *envoyauth.AttributeContext) *DestinationAttributes {
177+
destination := attributes.Destination
178+
socketAddress := destination.GetAddress().GetSocketAddress()
179+
return &DestinationAttributes{
180+
Address: socketAddress.GetAddress(),
181+
Port: int32(socketAddress.GetPortValue()),
182+
Service: destination.GetService(),
183+
Labels: destination.GetLabels(),
184+
Principal: destination.GetPrincipal(),
185+
}
186+
}
187+
188+
func newAuthAttributes(authData map[string]interface{}) *AuthAttributes {
189+
authAttributes := &AuthAttributes{}
190+
authAttributesValue := reflect.ValueOf(authAttributes).Elem()
191+
for key, value := range authData {
192+
fieldValue := authAttributesValue.FieldByName(strings.ToUpper(key[:1]) + key[1:])
193+
if fieldValue.IsValid() && fieldValue.CanSet() {
194+
if value != nil {
195+
fieldValue.Set(reflect.ValueOf(value))
196+
}
197+
}
198+
}
199+
return authAttributes
200+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package service
2+
3+
import (
4+
"testing"
5+
6+
envoycore "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
7+
envoyauth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
8+
"github.com/golang/protobuf/ptypes/timestamp"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestNewWellKnownAttributes(t *testing.T) {
13+
envoyAttrs := &envoyauth.AttributeContext{
14+
MetadataContext: &envoycore.Metadata{},
15+
Request: &envoyauth.AttributeContext_Request{
16+
Http: &envoyauth.AttributeContext_HttpRequest{
17+
Headers: map[string]string{
18+
"referer": "www.kuadrant.io",
19+
"user-agent": "best browser ever",
20+
},
21+
Path: "/force",
22+
Protocol: "HTTP/2.1",
23+
Method: "GET",
24+
},
25+
Time: &timestamp.Timestamp{},
26+
},
27+
Source: &envoyauth.AttributeContext_Peer{
28+
Service: "svc.rebels.local",
29+
},
30+
Destination: &envoyauth.AttributeContext_Peer{
31+
Service: "svc.rogue-1.local",
32+
Labels: map[string]string{"squad": "rogue"},
33+
},
34+
}
35+
authData := map[string]interface{}{
36+
"identity": map[string]any{"user": "luke", "group": "rebels"},
37+
"metadata": map[string]any{"squad": "rogue"},
38+
"authorization": map[string]any{"group": "rebels"},
39+
"response": map[string]any{"status": 200},
40+
}
41+
42+
wellKnownAttributes := NewWellKnownAttributes(envoyAttrs, authData)
43+
44+
assert.Equal(t, "/force", wellKnownAttributes.Request.Path)
45+
assert.Equal(t, "www.kuadrant.io", wellKnownAttributes.Request.Referer)
46+
assert.Equal(t, "best browser ever", wellKnownAttributes.Request.UserAgent)
47+
assert.Equal(t, "svc.rebels.local", wellKnownAttributes.Source.Service)
48+
assert.Equal(t, map[string]string{"squad": "rogue"}, wellKnownAttributes.Destination.Labels)
49+
assert.Equal(t, map[string]any{"user": "luke", "group": "rebels"}, wellKnownAttributes.Auth.Identity)
50+
assert.Equal(t, map[string]any{"squad": "rogue"}, wellKnownAttributes.Auth.Metadata)
51+
assert.Equal(t, map[string]any{"group": "rebels"}, wellKnownAttributes.Auth.Authorization)
52+
assert.Equal(t, map[string]any{"status": 200}, wellKnownAttributes.Auth.Response)
53+
assert.Nil(t, wellKnownAttributes.Auth.Callbacks)
54+
}

0 commit comments

Comments
 (0)