Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Audit failed authz attempts #3155

Merged
merged 12 commits into from
Jun 9, 2024
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ devenv.local.nix
.pre-commit-config.yaml

build/mage_output_file.go
*.rego
8 changes: 8 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ func (e ErrUnauthenticated) Error() string {
return string(e)
}

type ErrUnauthorized string

var ErrUnauthorizedf = NewErrorf[ErrUnauthorized]

func (e ErrUnauthorized) Error() string {
return string(e)
}

// StringError is any error that also happens to have an underlying type of string.
type StringError interface {
error
Expand Down
90 changes: 45 additions & 45 deletions internal/cmd/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ func NewGRPCServer(
skipAuthnIfExcluded(evalsrv, cfg.Authentication.Exclude.Evaluation)
skipAuthnIfExcluded(evaldatasrv, cfg.Authentication.Exclude.Evaluation)

var checker *audit.Checker
var checker audit.EventPairChecker = &audit.NoOpChecker{}

// We have to check if audit logging is enabled here for informing the authentication service that
// the user would like to receive token:deleted events.
Expand Down Expand Up @@ -345,50 +345,6 @@ func NewGRPCServer(
)...,
)

// cache must come after auth interceptors
if cfg.Cache.Enabled && cacher != nil {
interceptors = append(interceptors, middlewaregrpc.CacheUnaryInterceptor(cacher, logger))
}

if cfg.Authorization.Required {
authzOpts := []containers.Option[authzmiddlewaregrpc.InterceptorOptions]{
authzmiddlewaregrpc.WithServerSkipsAuthorization(healthsrv),
}

engineOpts := []containers.Option[authz.Engine]{
authz.WithPollDuration(cfg.Authorization.Policy.PollInterval),
}

if cfg.Authorization.Data != nil {
switch cfg.Authorization.Data.Backend {
case config.AuthorizationBackendLocal:
engineOpts = append(engineOpts, authz.WithDataSource(
filesystem.DataSourceFromPath(cfg.Authorization.Data.Local.Path),
cfg.Authorization.Data.PollInterval,
))
default:
return nil, fmt.Errorf("unexpected authz data backend type: %q", cfg.Authorization.Data.Backend)
}
}

var source authz.PolicySource
switch cfg.Authorization.Policy.Backend {
case config.AuthorizationBackendLocal:
source = filesystem.PolicySourceFromPath(cfg.Authorization.Policy.Local.Path)
default:
return nil, fmt.Errorf("unexpected authz policy backend type: %q", cfg.Authorization.Policy.Backend)
}

policyEngine, err := authz.NewEngine(ctx, logger, source, engineOpts...)
if err != nil {
return nil, fmt.Errorf("creating authorization policy engine: %w", err)
}

interceptors = append(interceptors, authzmiddlewaregrpc.AuthorizationRequiredInterceptor(logger, policyEngine, authzOpts...))

logger.Info("authorization middleware enabled")
}

// audit sinks configuration
sinks := make([]audit.Sink, 0)

Expand Down Expand Up @@ -488,6 +444,50 @@ func NewGRPCServer(
}
otel.SetTextMapPropagator(textMapPropagator)

if cfg.Authorization.Required {
authzOpts := []containers.Option[authzmiddlewaregrpc.InterceptorOptions]{
authzmiddlewaregrpc.WithServerSkipsAuthorization(healthsrv),
}

engineOpts := []containers.Option[authz.Engine]{
authz.WithPollDuration(cfg.Authorization.Policy.PollInterval),
}

if cfg.Authorization.Data != nil {
switch cfg.Authorization.Data.Backend {
case config.AuthorizationBackendLocal:
engineOpts = append(engineOpts, authz.WithDataSource(
filesystem.DataSourceFromPath(cfg.Authorization.Data.Local.Path),
cfg.Authorization.Data.PollInterval,
))
default:
return nil, fmt.Errorf("unexpected authz data backend type: %q", cfg.Authorization.Data.Backend)
}
}

var source authz.PolicySource
switch cfg.Authorization.Policy.Backend {
case config.AuthorizationBackendLocal:
source = filesystem.PolicySourceFromPath(cfg.Authorization.Policy.Local.Path)
default:
return nil, fmt.Errorf("unexpected authz policy backend type: %q", cfg.Authorization.Policy.Backend)
}

policyEngine, err := authz.NewEngine(ctx, logger, source, engineOpts...)
if err != nil {
return nil, fmt.Errorf("creating authorization policy engine: %w", err)
}

interceptors = append(interceptors, authzmiddlewaregrpc.AuthorizationRequiredInterceptor(logger, policyEngine, authzOpts...))

logger.Info("authorization middleware enabled")
}

// cache must come after authn and authz interceptors
if cfg.Cache.Enabled && cacher != nil {
interceptors = append(interceptors, middlewaregrpc.CacheUnaryInterceptor(cacher, logger))
}

grpcOpts := []grpc.ServerOption{
grpc.ChainUnaryInterceptor(interceptors...),
grpc.KeepaliveParams(keepalive.ServerParameters{
Expand Down
4 changes: 3 additions & 1 deletion internal/server/analytics/sink.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ func NewAnalyticsSinkSpanExporter(logger *zap.Logger, analyticsStoreMutator Anal
}
}

const evaluationResponseKey = "flipt.evaluation.response"

// transformSpanEventToEvaluationResponses is a convenience function to transform a span event into an []*EvaluationResponse.
func transformSpanEventToEvaluationResponses(event sdktrace.Event) ([]*EvaluationResponse, error) {
for _, attr := range event.Attributes {
if string(attr.Key) == "flipt.evaluation.response" {
if string(attr.Key) == evaluationResponseKey {
evaluationResponseBytes := []byte(attr.Value.AsString())
var evaluationResponse []*EvaluationResponse

Expand Down
16 changes: 16 additions & 0 deletions internal/server/audit/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import (
"strings"
)

// EventPairChecker is the contract for checking if an event pair exists and if it should be emitted to configured sinks.
type EventPairChecker interface {
Check(eventPair string) bool
Events() []string
}

// Checker holds a map that maps event pairs to a dummy struct. It is basically
// used as a set to check for existence.
type Checker struct {
Expand Down Expand Up @@ -93,3 +99,13 @@ func (c *Checker) Events() []string {

return events
}

type NoOpChecker struct{}

func (n *NoOpChecker) Check(eventPair string) bool {
return false
}

func (n *NoOpChecker) Events() []string {
return []string{}
}
20 changes: 17 additions & 3 deletions internal/server/audit/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ import (
)

const (
eventVersion = "0.1"
eventVersion = "0.2"
eventVersionKey = "flipt.event.version"
eventActionKey = "flipt.event.action"
eventTypeKey = "flipt.event.type"
eventMetadataActorKey = "flipt.event.metadata.actor"
eventPayloadKey = "flipt.event.payload"
eventTimestampKey = "flipt.event.timestamp"
eventStatusKey = "flipt.event.status"
)

// Event holds information that represents an action that has occurred in the system.
// Event holds information that represents an action that was attempted in the system.
type Event struct {
Version string `json:"version"`

Expand All @@ -35,6 +36,8 @@ type Event struct {
Payload interface{} `json:"payload"`

Timestamp string `json:"timestamp"`

Status string `json:"status"`
}

// NewEvent is the constructor for an event.
Expand All @@ -61,6 +64,7 @@ func NewEvent(r flipt.Request, actor *Actor, payload interface{}) *Event {
Version: eventVersion,
Action: action,
Type: typ,
Status: string(r.Status),
Metadata: Metadata{
Actor: actor,
},
Expand Down Expand Up @@ -102,6 +106,13 @@ func (e Event) DecodeToAttributes() []attribute.KeyValue {
})
}

if e.Status != "" {
akv = append(akv, attribute.KeyValue{
Key: eventStatusKey,
Value: attribute.StringValue(e.Status),
})
}

b, err := json.Marshal(e.Metadata.Actor)
if err == nil {
akv = append(akv, attribute.KeyValue{
Expand Down Expand Up @@ -129,13 +140,14 @@ func (e *Event) AddToSpan(ctx context.Context) {
}

func (e *Event) Valid() bool {
return e.Version != "" && e.Action != "" && e.Type != "" && e.Timestamp != "" && e.Payload != nil
return e.Version != "" && e.Action != "" && e.Type != "" && e.Timestamp != ""
}

func (e Event) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("version", e.Version)
enc.AddString("type", e.Type)
enc.AddString("action", e.Action)
enc.AddString("status", e.Status)
if err := enc.AddReflected("metadata", e.Metadata); err != nil {
return err
}
Expand All @@ -162,6 +174,8 @@ func decodeToEvent(kvs []attribute.KeyValue) (*Event, error) {
e.Type = kv.Value.AsString()
case eventTimestampKey:
e.Timestamp = kv.Value.AsString()
case eventStatusKey:
e.Status = kv.Value.AsString()
case eventMetadataActorKey:
var actor Actor
if err := json.Unmarshal([]byte(kv.Value.AsString()), &actor); err != nil {
Expand Down
27 changes: 16 additions & 11 deletions internal/server/audit/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,28 @@ import (
)

func TestMarshalLogObject(t *testing.T) {
actor := Actor{Authentication: "github"}
e := Event{
Version: "0.1",
Type: "sometype",
Action: "modified",
Metadata: Metadata{Actor: &actor},
Payload: "custom payload",
}
enc := zapcore.NewMapObjectEncoder()
err := e.MarshalLogObject(enc)
var (
actor = Actor{Authentication: "github"}
e = Event{
Version: "0.2",
Type: "sometype",
Action: "modified",
Metadata: Metadata{Actor: &actor},
Payload: "custom payload",
Status: "success",
}
enc = zapcore.NewMapObjectEncoder()
err = e.MarshalLogObject(enc)
)

require.NoError(t, err)
assert.Equal(t, map[string]any{
"version": "0.1",
"version": "0.2",
"action": "modified",
"type": "sometype",
"metadata": Metadata{Actor: &actor},
"payload": "custom payload",
"timestamp": "",
"status": "success",
}, enc.Fields)
}
16 changes: 10 additions & 6 deletions internal/server/audit/log/log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@ func TestSink(t *testing.T) {

err = s.SendAudits(context.TODO(), []audit.Event{
{
Version: "0.1",
Version: "0.2",
Type: string(flipt.SubjectFlag),
Action: string(flipt.ActionCreate),
Status: string(flipt.StatusSuccess),
},
{
Version: "0.1",
Version: "0.2",
Type: string(flipt.SubjectConstraint),
Action: string(flipt.ActionUpdate),
Status: string(flipt.StatusSuccess),
},
})

Expand All @@ -58,7 +60,7 @@ func TestSink(t *testing.T) {
assert.NotEmpty(t, lines)
assert.NotEmpty(t, lines[0])

assert.JSONEq(t, `{"version": "0.1", "type": "flag", "action": "create", "metadata": {}, "payload": null, "timestamp": ""}`, lines[0])
assert.JSONEq(t, `{"version": "0.2", "type": "flag", "action": "create", "metadata": {}, "payload": null, "timestamp": "", "status": "success"}`, lines[0])
}

func TestSink_DirNotExists(t *testing.T) {
Expand Down Expand Up @@ -87,14 +89,16 @@ func TestSink_DirNotExists(t *testing.T) {

err = s.SendAudits(context.TODO(), []audit.Event{
{
Version: "0.1",
Version: "0.2",
Type: string(flipt.SubjectFlag),
Action: string(flipt.ActionCreate),
Status: string(flipt.StatusSuccess),
},
{
Version: "0.1",
Version: "0.2",
Type: string(flipt.SubjectConstraint),
Action: string(flipt.ActionUpdate),
Status: string(flipt.StatusSuccess),
},
})

Expand All @@ -113,7 +117,7 @@ func TestSink_DirNotExists(t *testing.T) {
assert.NotEmpty(t, lines)
assert.NotEmpty(t, lines[0])

assert.JSONEq(t, `{"version": "0.1", "type": "flag", "action": "create", "metadata": {}, "payload": null, "timestamp": ""}`, lines[0])
assert.JSONEq(t, `{"version": "0.2", "type": "flag", "action": "create", "metadata": {}, "payload": null, "timestamp": "", "status": "success"}`, lines[0])
})
}
}
Loading
Loading