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

@ -84,10 +84,11 @@ type RequestSession struct {
// Result is the result of evaluation. // Result is the result of evaluation.
type Result struct { type Result struct {
Allow RuleResult Allow RuleResult
Deny RuleResult Deny RuleResult
Headers http.Header Headers http.Header
Traces []contextutil.PolicyEvaluationTrace Traces []contextutil.PolicyEvaluationTrace
AdditionalLogFields map[log.AuthorizeLogField]any
} }
// An Evaluator evaluates policies. // An Evaluator evaluates policies.
@ -228,10 +229,11 @@ func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error)
} }
res := &Result{ res := &Result{
Allow: policyOutput.Allow, Allow: policyOutput.Allow,
Deny: policyOutput.Deny, Deny: policyOutput.Deny,
Headers: headersOutput.Headers, Headers: headersOutput.Headers,
Traces: policyOutput.Traces, Traces: policyOutput.Traces,
AdditionalLogFields: headersOutput.AdditionalLogFields,
} }
return res, nil return res, nil
} }

View file

@ -8,12 +8,14 @@ import (
"github.com/open-policy-agent/opa/rego" "github.com/open-policy-agent/opa/rego"
"github.com/pomerium/pomerium/authorize/internal/store" "github.com/pomerium/pomerium/authorize/internal/store"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/telemetry/trace" "github.com/pomerium/pomerium/internal/telemetry/trace"
) )
// HeadersResponse is the output from the headers.rego script. // HeadersResponse is the output from the headers.rego script.
type HeadersResponse struct { type HeadersResponse struct {
Headers http.Header Headers http.Header
AdditionalLogFields map[log.AuthorizeLogField]any
} }
// A HeadersEvaluator evaluates the headers.rego script. // 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" envoy_config_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
"github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v3"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/rs/zerolog"
"google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/structpb"
"github.com/pomerium/datasource/pkg/directory" "github.com/pomerium/datasource/pkg/directory"
@ -23,7 +24,7 @@ import (
"github.com/pomerium/pomerium/pkg/cryptutil" "github.com/pomerium/pomerium/pkg/cryptutil"
"github.com/pomerium/pomerium/pkg/grpc/session" "github.com/pomerium/pomerium/pkg/grpc/session"
"github.com/pomerium/pomerium/pkg/grpc/user" "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. // A headersEvaluatorEvaluation is a single evaluation of the headers evaluator.
@ -57,8 +58,11 @@ func newHeadersEvaluatorEvaluation(evaluator *HeadersEvaluator, request *Request
return &headersEvaluatorEvaluation{ return &headersEvaluatorEvaluation{
evaluator: evaluator, evaluator: evaluator,
request: request, request: request,
response: &HeadersResponse{Headers: make(http.Header)}, response: &HeadersResponse{
now: now, 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 { if len(filters) == 0 {
return groups 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. // A group should be included if it appears in either the global or the route-level filter list.
for _, f := range filters { for _, f := range filters {
if f.IsAllowed(g) { if f.IsAllowed(g) {
@ -345,7 +350,30 @@ func (e *headersEvaluatorEvaluation) getFilteredGroups(ctx context.Context) []st
} }
} }
return false 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). // 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") evt := log.Ctx(ctx).Info().Str("service", "authorize")
fields := a.currentOptions.Load().GetAuthorizeLogFields() fields := a.currentOptions.Load().GetAuthorizeLogFields()
for _, field := range fields { 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) evt = log.HTTPHeaders(evt, fields, hdrs)
@ -132,6 +132,7 @@ func populateLogEvent(
u *user.User, u *user.User,
hdrs map[string]string, hdrs map[string]string,
impersonateDetails *impersonateDetails, impersonateDetails *impersonateDetails,
res *evaluator.Result,
) *zerolog.Event { ) *zerolog.Event {
path, query, _ := strings.Cut(in.GetAttributes().GetRequest().GetHttp().GetPath(), "?") path, query, _ := strings.Cut(in.GetAttributes().GetRequest().GetHttp().GetPath(), "?")
@ -198,6 +199,11 @@ func populateLogEvent(
} }
return evt.Str(string(field), userID) return evt.Str(string(field), userID)
default: default:
if res != nil {
if v, ok := res.AdditionalLogFields[field]; ok {
evt = evt.Interface(string(field), v)
}
}
return evt return evt
} }
} }

View file

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

View file

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