authorize: log JWT groups filtering

This commit is contained in:
Kenneth Jenkins 2025-01-08 13:33:21 -08:00
parent 8bc86fe06f
commit e7831cc299
6 changed files with 64 additions and 16 deletions

View file

@ -88,6 +88,7 @@ type Result struct {
Deny RuleResult
Headers http.Header
Traces []contextutil.PolicyEvaluationTrace
AdditionalLogFields map[log.AuthorizeLogField]any
}
// An Evaluator evaluates policies.
@ -232,6 +233,7 @@ func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error)
Deny: policyOutput.Deny,
Headers: headersOutput.Headers,
Traces: policyOutput.Traces,
AdditionalLogFields: headersOutput.AdditionalLogFields,
}
return res, nil
}

View file

@ -8,12 +8,14 @@ import (
"github.com/open-policy-agent/opa/rego"
"github.com/pomerium/pomerium/authorize/internal/store"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/telemetry/trace"
)
// HeadersResponse is the output from the headers.rego script.
type HeadersResponse struct {
Headers http.Header
AdditionalLogFields map[log.AuthorizeLogField]any
}
// A HeadersEvaluator evaluates the headers.rego script.

View file

@ -15,6 +15,7 @@ import (
envoy_config_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
"github.com/go-jose/go-jose/v3"
"github.com/google/uuid"
"github.com/rs/zerolog"
"google.golang.org/protobuf/types/known/structpb"
"github.com/pomerium/datasource/pkg/directory"
@ -23,7 +24,7 @@ import (
"github.com/pomerium/pomerium/pkg/cryptutil"
"github.com/pomerium/pomerium/pkg/grpc/session"
"github.com/pomerium/pomerium/pkg/grpc/user"
"github.com/pomerium/pomerium/pkg/slices"
"github.com/pomerium/pomerium/pkg/telemetry/requestid"
)
// A headersEvaluatorEvaluation is a single evaluation of the headers evaluator.
@ -57,7 +58,10 @@ func newHeadersEvaluatorEvaluation(evaluator *HeadersEvaluator, request *Request
return &headersEvaluatorEvaluation{
evaluator: evaluator,
request: request,
response: &HeadersResponse{Headers: make(http.Header)},
response: &HeadersResponse{
Headers: make(http.Header),
AdditionalLogFields: make(map[log.AuthorizeLogField]any),
},
now: now,
}
}
@ -337,7 +341,8 @@ func (e *headersEvaluatorEvaluation) getFilteredGroups(ctx context.Context) []st
if len(filters) == 0 {
return groups
}
return slices.Filter(groups, func(g string) bool {
filterFn := func(g string) bool {
// A group should be included if it appears in either the global or the route-level filter list.
for _, f := range filters {
if f.IsAllowed(g) {
@ -345,7 +350,30 @@ func (e *headersEvaluatorEvaluation) getFilteredGroups(ctx context.Context) []st
}
}
return false
})
}
var included []string
// Log the specifics of which groups were filtered out at Debug level.
var excluded *zerolog.Array
if log.Ctx(ctx).GetLevel() <= zerolog.DebugLevel {
excluded = zerolog.Arr()
}
for _, g := range groups {
if filterFn(g) {
included = append(included, g)
} else if excluded != nil {
excluded.Str(g)
}
}
if removedCount := len(groups) - len(included); removedCount > 0 {
log.Ctx(ctx).Debug().
Str("request-id", requestid.FromContext(ctx)).
Array("removed-groups", excluded).
Msg("group filtering removed groups")
e.response.AdditionalLogFields[log.AuthorizeLogFieldRemovedGroupsCount] = removedCount
}
return included
}
// getAllGroups returns the full group names/IDs list (without any filtering).

View file

@ -33,7 +33,7 @@ func (a *Authorize) logAuthorizeCheck(
evt := log.Ctx(ctx).Info().Str("service", "authorize")
fields := a.currentOptions.Load().GetAuthorizeLogFields()
for _, field := range fields {
evt = populateLogEvent(ctx, field, evt, in, s, u, hdrs, impersonateDetails)
evt = populateLogEvent(ctx, field, evt, in, s, u, hdrs, impersonateDetails, res)
}
evt = log.HTTPHeaders(evt, fields, hdrs)
@ -132,6 +132,7 @@ func populateLogEvent(
u *user.User,
hdrs map[string]string,
impersonateDetails *impersonateDetails,
res *evaluator.Result,
) *zerolog.Event {
path, query, _ := strings.Cut(in.GetAttributes().GetRequest().GetHttp().GetPath(), "?")
@ -198,6 +199,11 @@ func populateLogEvent(
}
return evt.Str(string(field), userID)
default:
if res != nil {
if v, ok := res.AdditionalLogFields[field]; ok {
evt = evt.Interface(string(field), v)
}
}
return evt
}
}

View file

@ -11,6 +11,7 @@ import (
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/pomerium/pomerium/authorize/evaluator"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/pkg/grpc/session"
"github.com/pomerium/pomerium/pkg/grpc/user"
@ -64,6 +65,11 @@ func Test_populateLogEvent(t *testing.T) {
sessionID: "IMPERSONATE-SESSION-ID",
userID: "IMPERSONATE-USER-ID",
}
res := &evaluator.Result{
AdditionalLogFields: map[log.AuthorizeLogField]any{
log.AuthorizeLogFieldRemovedGroupsCount: 42,
},
}
for _, tc := range []struct {
field log.AuthorizeLogField
@ -82,6 +88,7 @@ func Test_populateLogEvent(t *testing.T) {
{log.AuthorizeLogFieldMethod, s, `{"method":"GET"}`},
{log.AuthorizeLogFieldPath, s, `{"path":"https://www.example.com/some/path"}`},
{log.AuthorizeLogFieldQuery, s, `{"query":"a=b"}`},
{log.AuthorizeLogFieldRemovedGroupsCount, s, `{"removed-groups-count":42}`},
{log.AuthorizeLogFieldRequestID, s, `{"request-id":"REQUEST-ID"}`},
{log.AuthorizeLogFieldServiceAccountID, sa, `{"service-account-id":"SERVICE-ACCOUNT-ID"}`},
{log.AuthorizeLogFieldSessionID, s, `{"session-id":"SESSION-ID"}`},
@ -97,7 +104,7 @@ func Test_populateLogEvent(t *testing.T) {
var buf bytes.Buffer
log := zerolog.New(&buf)
evt := log.Log()
evt = populateLogEvent(ctx, tc.field, evt, checkRequest, tc.s, u, headers, impersonateDetails)
evt = populateLogEvent(ctx, tc.field, evt, checkRequest, tc.s, u, headers, impersonateDetails, res)
evt.Send()
assert.Equal(t, tc.expect, strings.TrimSpace(buf.String()))

View file

@ -23,6 +23,7 @@ const (
AuthorizeLogFieldMethod AuthorizeLogField = "method"
AuthorizeLogFieldPath AuthorizeLogField = "path"
AuthorizeLogFieldQuery AuthorizeLogField = "query"
AuthorizeLogFieldRemovedGroupsCount AuthorizeLogField = "removed-groups-count"
AuthorizeLogFieldRequestID AuthorizeLogField = "request-id"
AuthorizeLogFieldServiceAccountID AuthorizeLogField = "service-account-id"
AuthorizeLogFieldSessionID AuthorizeLogField = "session-id"
@ -41,6 +42,7 @@ var DefaultAuthorizeLogFields = []AuthorizeLogField{
AuthorizeLogFieldImpersonateSessionID,
AuthorizeLogFieldImpersonateUserID,
AuthorizeLogFieldImpersonateEmail,
AuthorizeLogFieldRemovedGroupsCount,
AuthorizeLogFieldServiceAccountID,
AuthorizeLogFieldUser,
AuthorizeLogFieldEmail,
@ -63,6 +65,7 @@ var authorizeLogFieldLookup = map[AuthorizeLogField]struct{}{
AuthorizeLogFieldMethod: {},
AuthorizeLogFieldPath: {},
AuthorizeLogFieldQuery: {},
AuthorizeLogFieldRemovedGroupsCount: {},
AuthorizeLogFieldRequestID: {},
AuthorizeLogFieldServiceAccountID: {},
AuthorizeLogFieldSessionID: {},