mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-05 12:23:03 +02:00
authorize: refactor logAuthorizeCheck() (#5576)
Currently, policy evaluation and authorize logging are coupled to the Envoy CheckRequest proto message (part of the ext_authz API). In the context of ssh proxy authentication, we won't have a CheckRequest. Instead, let's make the existing evaluator.Request type the source of truth for the authorize log fields. This way, whether we populate the evaluator.Request struct from an ext_authz request or from an ssh proxy request, we can use the same logAuthorizeCheck() method for logging. Add some additional fields to evaluator.RequestHTTP for the authorize log fields that are not currently represented in this struct.
This commit is contained in:
parent
8738066ce4
commit
2e7d1c7f12
10 changed files with 326 additions and 258 deletions
|
@ -4,16 +4,21 @@ package evaluator
|
|||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/hashicorp/go-set/v3"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"github.com/pomerium/pomerium/authorize/checkrequest"
|
||||
"github.com/pomerium/pomerium/authorize/internal/store"
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/errgrouputil"
|
||||
|
@ -36,30 +41,37 @@ type Request struct {
|
|||
// RequestHTTP is the HTTP field in the request.
|
||||
type RequestHTTP struct {
|
||||
Method string `json:"method"`
|
||||
Host string `json:"host"`
|
||||
Hostname string `json:"hostname"`
|
||||
Path string `json:"path"`
|
||||
RawPath string `json:"raw_path"`
|
||||
RawQuery string `json:"raw_query"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
ClientCertificate ClientCertificateInfo `json:"client_certificate"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
// NewRequestHTTP creates a new RequestHTTP.
|
||||
func NewRequestHTTP(
|
||||
method string,
|
||||
requestURL url.URL,
|
||||
headers map[string]string,
|
||||
clientCertificate ClientCertificateInfo,
|
||||
ip string,
|
||||
// RequestHTTPFromCheckRequest populates a RequestHTTP from an Envoy CheckRequest proto.
|
||||
func RequestHTTPFromCheckRequest(
|
||||
ctx context.Context,
|
||||
in *envoy_service_auth_v3.CheckRequest,
|
||||
) RequestHTTP {
|
||||
requestURL := checkrequest.GetURL(in)
|
||||
rawPath, rawQuery, _ := strings.Cut(in.GetAttributes().GetRequest().GetHttp().GetPath(), "?")
|
||||
attrs := in.GetAttributes()
|
||||
clientCertMetadata := attrs.GetMetadataContext().GetFilterMetadata()["com.pomerium.client-certificate-info"]
|
||||
return RequestHTTP{
|
||||
Method: method,
|
||||
Method: attrs.GetRequest().GetHttp().GetMethod(),
|
||||
Host: attrs.GetRequest().GetHttp().GetHost(),
|
||||
Hostname: requestURL.Hostname(),
|
||||
Path: requestURL.Path,
|
||||
RawPath: rawPath,
|
||||
RawQuery: rawQuery,
|
||||
URL: requestURL.String(),
|
||||
Headers: headers,
|
||||
ClientCertificate: clientCertificate,
|
||||
IP: ip,
|
||||
Headers: checkrequest.GetHeaders(in),
|
||||
ClientCertificate: getClientCertificateInfo(ctx, clientCertMetadata),
|
||||
IP: attrs.GetSource().GetAddress().GetSocketAddress().GetAddress(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,6 +89,41 @@ type ClientCertificateInfo struct {
|
|||
Intermediates string `json:"intermediates,omitempty"`
|
||||
}
|
||||
|
||||
// getClientCertificateInfo translates from the client certificate Envoy
|
||||
// metadata to the ClientCertificateInfo type.
|
||||
func getClientCertificateInfo(
|
||||
ctx context.Context, metadata *structpb.Struct,
|
||||
) ClientCertificateInfo {
|
||||
var c ClientCertificateInfo
|
||||
if metadata == nil {
|
||||
return c
|
||||
}
|
||||
c.Presented = metadata.Fields["presented"].GetBoolValue()
|
||||
escapedChain := metadata.Fields["chain"].GetStringValue()
|
||||
if escapedChain == "" {
|
||||
// No validated client certificate.
|
||||
return c
|
||||
}
|
||||
|
||||
chain, err := url.QueryUnescape(escapedChain)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Error().Str("chain", escapedChain).Err(err).
|
||||
Msg(`received unexpected client certificate "chain" value`)
|
||||
return c
|
||||
}
|
||||
|
||||
// Split the chain into the leaf and any intermediate certificates.
|
||||
p, rest := pem.Decode([]byte(chain))
|
||||
if p == nil {
|
||||
log.Ctx(ctx).Error().Str("chain", escapedChain).
|
||||
Msg(`received unexpected client certificate "chain" value (no PEM block found)`)
|
||||
return c
|
||||
}
|
||||
c.Leaf = string(pem.EncodeToMemory(p))
|
||||
c.Intermediates = string(rest)
|
||||
return c
|
||||
}
|
||||
|
||||
// RequestSession is the session field in the request.
|
||||
type RequestSession struct {
|
||||
ID string `json:"id"`
|
||||
|
|
|
@ -10,10 +10,12 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"github.com/pomerium/pomerium/authorize/internal/store"
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/testutil"
|
||||
"github.com/pomerium/pomerium/pkg/cryptutil"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||
|
@ -22,6 +24,113 @@ import (
|
|||
"github.com/pomerium/pomerium/pkg/storage"
|
||||
)
|
||||
|
||||
func Test_getClientCertificateInfo(t *testing.T) {
|
||||
const leafPEM = `-----BEGIN CERTIFICATE-----
|
||||
MIIBZTCCAQugAwIBAgICEAEwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPSW50ZXJt
|
||||
ZWRpYXRlIENBMCIYDzAwMDEwMTAxMDAwMDAwWhgPMDAwMTAxMDEwMDAwMDBaMB8x
|
||||
HTAbBgNVBAMTFENsaWVudCBjZXJ0aWZpY2F0ZSAxMFkwEwYHKoZIzj0CAQYIKoZI
|
||||
zj0DAQcDQgAESly1cwEbcxaJBl6qAhrX1k7vejTFNE2dEbrTMpUYMl86GEWdsDYN
|
||||
KSa/1wZCowPy82gPGjfAU90odkqJOusCQqM4MDYwEwYDVR0lBAwwCgYIKwYBBQUH
|
||||
AwIwHwYDVR0jBBgwFoAU6Qb7nEl2XHKpf/QLL6PENsHFqbowCgYIKoZIzj0EAwID
|
||||
SAAwRQIgXREMUz81pYwJCMLGcV0ApaXIUap1V5n1N4VhyAGxGLYCIQC8p/LwoSgu
|
||||
71H3/nCi5MxsECsvVtsmHIfwXt0wulQ1TA==
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
const intermediatePEM = `-----BEGIN CERTIFICATE-----
|
||||
MIIBYzCCAQigAwIBAgICEAEwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMHUm9vdCBD
|
||||
QTAiGA8wMDAxMDEwMTAwMDAwMFoYDzAwMDEwMTAxMDAwMDAwWjAaMRgwFgYDVQQD
|
||||
Ew9JbnRlcm1lZGlhdGUgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATYaTr9
|
||||
uH4LpEp541/2SlKrdQZwNns+NHY/ftm++NhMDUn+izzNbPZ5aPT6VBs4Q6vbgfkK
|
||||
kDaBpaKzb+uOT+o1o0IwQDAdBgNVHQ4EFgQU6Qb7nEl2XHKpf/QLL6PENsHFqbow
|
||||
HwYDVR0jBBgwFoAUiQ3r61y+vxDn6PMWZrpISr67HiQwCgYIKoZIzj0EAwIDSQAw
|
||||
RgIhAMvdURs28uib2QwSMnqJjKasMb30yrSJvTiSU+lcg97/AiEA+6GpioM0c221
|
||||
n/XNKVYEkPmeXHRoz9ZuVDnSfXKJoHE=
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
const rootPEM = `-----BEGIN CERTIFICATE-----
|
||||
MIIBNzCB36ADAgECAgIQADAKBggqhkjOPQQDAjASMRAwDgYDVQQDEwdSb290IENB
|
||||
MCIYDzAwMDEwMTAxMDAwMDAwWhgPMDAwMTAxMDEwMDAwMDBaMBIxEDAOBgNVBAMT
|
||||
B1Jvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS6q0mTvm29xasq7Lwk
|
||||
aRGb2S/LkQFsAwaCXohSNvonCQHRMCRvA1IrQGk/oyBS5qrDoD9/7xkcVYHuTv5D
|
||||
CbtuoyEwHzAdBgNVHQ4EFgQUiQ3r61y+vxDn6PMWZrpISr67HiQwCgYIKoZIzj0E
|
||||
AwIDRwAwRAIgF1ux0ridbN+bo0E3TTcNY8Xfva7yquYRMmEkfbGvSb0CIDqK80B+
|
||||
fYCZHo3CID0gRSemaQ/jYMgyeBFrHIr6icZh
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
cases := []struct {
|
||||
label string
|
||||
presented bool
|
||||
chain string
|
||||
expected ClientCertificateInfo
|
||||
expectedLog string
|
||||
}{
|
||||
{
|
||||
"not presented",
|
||||
false,
|
||||
"",
|
||||
ClientCertificateInfo{},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"presented",
|
||||
true,
|
||||
url.QueryEscape(leafPEM),
|
||||
ClientCertificateInfo{
|
||||
Presented: true,
|
||||
Leaf: leafPEM,
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"presented with intermediates",
|
||||
true,
|
||||
url.QueryEscape(leafPEM + intermediatePEM + rootPEM),
|
||||
ClientCertificateInfo{
|
||||
Presented: true,
|
||||
Leaf: leafPEM,
|
||||
Intermediates: intermediatePEM + rootPEM,
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"invalid chain URL encoding",
|
||||
false,
|
||||
"invalid%URL%encoding",
|
||||
ClientCertificateInfo{},
|
||||
`{"chain":"invalid%URL%encoding","error":"invalid URL escape \"%UR\"","level":"error","message":"received unexpected client certificate \"chain\" value"}`,
|
||||
},
|
||||
{
|
||||
"invalid chain PEM encoding",
|
||||
true,
|
||||
"not valid PEM data",
|
||||
ClientCertificateInfo{
|
||||
Presented: true,
|
||||
},
|
||||
`{"chain":"not valid PEM data","level":"error","message":"received unexpected client certificate \"chain\" value (no PEM block found)"}`,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for i := range cases {
|
||||
c := &cases[i]
|
||||
t.Run(c.label, func(t *testing.T) {
|
||||
metadata := &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"presented": structpb.NewBoolValue(c.presented),
|
||||
"chain": structpb.NewStringValue(c.chain),
|
||||
},
|
||||
}
|
||||
var info ClientCertificateInfo
|
||||
logOutput := testutil.CaptureLogs(t, func() {
|
||||
info = getClientCertificateInfo(ctx, metadata)
|
||||
})
|
||||
assert.Equal(t, c.expected, info)
|
||||
assert.Contains(t, logOutput, c.expectedLog)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluator(t *testing.T) {
|
||||
signingKey, err := cryptutil.NewSigningKey()
|
||||
require.NoError(t, err)
|
||||
|
@ -527,13 +636,9 @@ func TestEvaluator(t *testing.T) {
|
|||
t.Run("http method", func(t *testing.T) {
|
||||
res, err := eval(t, options, []proto.Message{}, &Request{
|
||||
Policy: policies[8],
|
||||
HTTP: NewRequestHTTP(
|
||||
http.MethodGet,
|
||||
*mustParseURL("https://from.example.com/"),
|
||||
nil,
|
||||
ClientCertificateInfo{},
|
||||
"",
|
||||
),
|
||||
HTTP: RequestHTTP{
|
||||
Method: http.MethodGet,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.Allow.Value)
|
||||
|
@ -541,13 +646,10 @@ func TestEvaluator(t *testing.T) {
|
|||
t.Run("http path", func(t *testing.T) {
|
||||
res, err := eval(t, options, []proto.Message{}, &Request{
|
||||
Policy: policies[9],
|
||||
HTTP: NewRequestHTTP(
|
||||
"POST",
|
||||
*mustParseURL("https://from.example.com/test"),
|
||||
nil,
|
||||
ClientCertificateInfo{},
|
||||
"",
|
||||
),
|
||||
HTTP: RequestHTTP{
|
||||
Method: "POST",
|
||||
Path: "/test",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.Allow.Value)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue