Skip to content

Commit

Permalink
feat: modify events to return more information about entity during au…
Browse files Browse the repository at this point in the history
…diting
  • Loading branch information
yquansah committed May 1, 2023
1 parent 081fe68 commit 8a1655d
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 104 deletions.
28 changes: 20 additions & 8 deletions internal/server/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"

"github.com/hashicorp/go-multierror"
Expand All @@ -31,14 +32,14 @@ type Type string
type Action string

const (
Constraint Type = "constraint"
Distribution Type = "distribution"
Flag Type = "flag"
Namespace Type = "namespace"
Rule Type = "rule"
Segment Type = "segment"
Token Type = "token"
Variant Type = "variant"
ConstraintType Type = "constraint"
DistributionType Type = "distribution"
FlagType Type = "flag"
NamespaceType Type = "namespace"
RuleType Type = "rule"
SegmentType Type = "segment"
TokenType Type = "token"
VariantType Type = "variant"

Create Action = "created"
Delete Action = "deleted"
Expand All @@ -58,6 +59,17 @@ type Event struct {
Timestamp string `json:"timestamp"`
}

// GRPCMethodToAction returns the Action from the gRPC method.
func GRPCMethodToAction(method string) Action {
if strings.Contains(method, "Create") {
return Create
} else if strings.Contains(method, "Update") {
return Update
}

return ""
}

// DecodeToAttributes provides a helper method for an Event that will return
// a value compatible to a SpanEvent.
func (e Event) DecodeToAttributes() []attribute.KeyValue {
Expand Down
13 changes: 10 additions & 3 deletions internal/server/audit/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"go.flipt.io/flipt/rpc/flipt"
"go.uber.org/zap"

sdktrace "go.opentelemetry.io/otel/sdk/trace"
Expand Down Expand Up @@ -40,12 +39,12 @@ func TestSinkSpanExporter(t *testing.T) {
_, span := tr.Start(context.Background(), "OnStart")

e := NewEvent(
Flag,
FlagType,
Create,
map[string]string{
"authentication": "token",
"ip": "127.0.0.1",
}, &flipt.CreateFlagRequest{
}, &Flag{
Key: "this-flag",
Name: "this-flag",
Description: "this description",
Expand All @@ -59,3 +58,11 @@ func TestSinkSpanExporter(t *testing.T) {
assert.Equal(t, e.Metadata, se.Metadata)
assert.Equal(t, e.Version, se.Version)
}

func TestGRPCMethodToAction(t *testing.T) {
a := GRPCMethodToAction("CreateNamespace")
assert.Equal(t, Create, a)

a = GRPCMethodToAction("UpdateSegment")
assert.Equal(t, Update, a)
}
152 changes: 152 additions & 0 deletions internal/server/audit/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package audit

import (
"go.flipt.io/flipt/rpc/flipt"
)

// All types in this file represent an audit representation of the Flipt type that we will send to
// the different sinks.

type Flag struct {
Key string `json:"key"`
Name string `json:"name"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
NamespaceKey string `json:"namespace_key"`
}

func NewFlag(f *flipt.Flag) *Flag {
return &Flag{
Key: f.Key,
Name: f.Name,
Description: f.Description,
Enabled: f.Enabled,
NamespaceKey: f.NamespaceKey,
}
}

type Variant struct {
Id string `json:"id"`
FlagKey string `json:"flag_key"`
Key string `json:"key"`
Name string `json:"name"`
Description string `json:"description"`
Attachment string `json:"attachment"`
NamespaceKey string `json:"namespace_key"`
}

func NewVariant(v *flipt.Variant) *Variant {
return &Variant{
Id: v.Id,
FlagKey: v.FlagKey,
Key: v.Key,
Name: v.Name,
Description: v.Description,
Attachment: v.Attachment,
NamespaceKey: v.NamespaceKey,
}
}

type Constraint struct {
Id string `json:"id"`
SegmentKey string `json:"segment_key"`
Type string `json:"type"`
Property string `json:"property"`
Operator string `json:"operator"`
Value string `json:"value"`
NamespaceKey string `json:"namespace_key"`
}

func NewConstraint(c *flipt.Constraint) *Constraint {
return &Constraint{
Id: c.Id,
SegmentKey: c.SegmentKey,
Type: c.Type.String(),
Property: c.Property,
Operator: c.Operator,
Value: c.Value,
NamespaceKey: c.NamespaceKey,
}
}

type Namespace struct {
Key string `json:"key"`
Name string `json:"name"`
Description string `json:"description"`
Protected bool `json:"protected"`
}

func NewNamespace(n *flipt.Namespace) *Namespace {
return &Namespace{
Key: n.Key,
Name: n.Name,
Description: n.Description,
Protected: n.Protected,
}
}

type Distribution struct {
Id string `json:"id"`
RuleId string `json:"rule_id"`
VariantId string `json:"variant_id"`
Rollout float32 `json:"rollout"`
}

func NewDistribution(d *flipt.Distribution) *Distribution {
return &Distribution{
Id: d.Id,
RuleId: d.RuleId,
VariantId: d.VariantId,
Rollout: d.Rollout,
}
}

type Segment struct {
Key string `json:"key"`
Name string `json:"name"`
Description string `json:"description"`
Constraints []*Constraint `json:"constraints"`
MatchType string `json:"match_type"`
NamespaceKey string `json:"namespace_key"`
}

func NewSegment(s *flipt.Segment) *Segment {
c := make([]*Constraint, 0, len(s.Constraints))
for _, sc := range s.Constraints {
c = append(c, NewConstraint(sc))
}

return &Segment{
Key: s.Key,
Name: s.Name,
Description: s.Description,
Constraints: c,
MatchType: s.MatchType.String(),
NamespaceKey: s.NamespaceKey,
}
}

type Rule struct {
Id string `json:"id"`
FlagKey string `json:"flag_key"`
SegmentKey string `json:"segment_key"`
Distributions []*Distribution `json:"distributions"`
Rank int32 `json:"rank"`
NamespaceKey string `json:"namespace_key"`
}

func NewRule(r *flipt.Rule) *Rule {
d := make([]*Distribution, 0, len(r.Distributions))
for _, rd := range r.Distributions {
d = append(d, NewDistribution(rd))
}

return &Rule{
Id: r.Id,
FlagKey: r.FlagKey,
SegmentKey: r.SegmentKey,
Distributions: d,
Rank: r.Rank,
NamespaceKey: r.NamespaceKey,
}
}
2 changes: 1 addition & 1 deletion internal/server/auth/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func (s *Server) DeleteAuthentication(ctx context.Context, req *auth.DeleteAuthe
return nil, err
}
if a.Method == auth.Method_METHOD_TOKEN {
event := audit.NewEvent(audit.Token, audit.Delete, actor, a.Metadata)
event := audit.NewEvent(audit.TokenType, audit.Delete, actor, a.Metadata)
event.AddToSpan(ctx)
}
}
Expand Down
107 changes: 61 additions & 46 deletions internal/server/middleware/grpc/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"go.flipt.io/flipt/internal/server/cache"
"go.flipt.io/flipt/internal/server/metrics"
flipt "go.flipt.io/flipt/rpc/flipt"
authrpc "go.flipt.io/flipt/rpc/flipt/auth"
fauth "go.flipt.io/flipt/rpc/flipt/auth"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
"google.golang.org/grpc"
Expand Down Expand Up @@ -244,66 +244,81 @@ func CacheUnaryInterceptor(cache cache.Cacher, logger *zap.Logger) grpc.UnarySer

// AuditUnaryInterceptor sends audit logs to configured sinks upon successful RPC requests for auditable events.
func AuditUnaryInterceptor(logger *zap.Logger) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
resp, err := handler(ctx, req)
if err != nil {
return resp, err
}

actor := auth.ActorFromContext(ctx)

var event *audit.Event

defer func() {
if event != nil {
span := trace.SpanFromContext(ctx)
span.AddEvent("event", trace.WithAttributes(event.DecodeToAttributes()...))
}
}()

// Delete request(s) have to be handled separately because they do not
// return the concrete type but rather an *empty.Empty response.
switch r := req.(type) {
case *flipt.CreateFlagRequest:
event = audit.NewEvent(audit.Flag, audit.Create, actor, r)
case *flipt.UpdateFlagRequest:
event = audit.NewEvent(audit.Flag, audit.Update, actor, r)
case *flipt.DeleteFlagRequest:
event = audit.NewEvent(audit.Flag, audit.Delete, actor, r)
case *flipt.CreateVariantRequest:
event = audit.NewEvent(audit.Variant, audit.Create, actor, r)
case *flipt.UpdateVariantRequest:
event = audit.NewEvent(audit.Variant, audit.Update, actor, r)
event = audit.NewEvent(audit.FlagType, audit.Delete, actor, r)
case *flipt.DeleteVariantRequest:
event = audit.NewEvent(audit.Variant, audit.Delete, actor, r)
case *flipt.CreateSegmentRequest:
event = audit.NewEvent(audit.Segment, audit.Create, actor, r)
case *flipt.UpdateSegmentRequest:
event = audit.NewEvent(audit.Segment, audit.Update, actor, r)
event = audit.NewEvent(audit.VariantType, audit.Delete, actor, r)
case *flipt.DeleteSegmentRequest:
event = audit.NewEvent(audit.Segment, audit.Delete, actor, r)
case *flipt.CreateConstraintRequest:
event = audit.NewEvent(audit.Constraint, audit.Create, actor, r)
case *flipt.UpdateConstraintRequest:
event = audit.NewEvent(audit.Constraint, audit.Update, actor, r)
case *flipt.DeleteConstraintRequest:
event = audit.NewEvent(audit.Constraint, audit.Delete, actor, r)
case *flipt.CreateDistributionRequest:
event = audit.NewEvent(audit.Distribution, audit.Create, actor, r)
case *flipt.UpdateDistributionRequest:
event = audit.NewEvent(audit.Distribution, audit.Update, actor, r)
event = audit.NewEvent(audit.SegmentType, audit.Delete, actor, r)
case *flipt.DeleteDistributionRequest:
event = audit.NewEvent(audit.Distribution, audit.Delete, actor, r)
case *flipt.CreateRuleRequest:
event = audit.NewEvent(audit.Rule, audit.Create, actor, r)
case *flipt.UpdateRuleRequest:
event = audit.NewEvent(audit.Rule, audit.Update, actor, r)
case *flipt.DeleteRuleRequest:
event = audit.NewEvent(audit.Rule, audit.Delete, actor, r)
case *flipt.CreateNamespaceRequest:
event = audit.NewEvent(audit.Namespace, audit.Create, actor, r)
case *flipt.UpdateNamespaceRequest:
event = audit.NewEvent(audit.Namespace, audit.Update, actor, r)
event = audit.NewEvent(audit.DistributionType, audit.Delete, actor, r)
case *flipt.DeleteConstraintRequest:
event = audit.NewEvent(audit.ConstraintType, audit.Delete, actor, r)
case *flipt.DeleteNamespaceRequest:
event = audit.NewEvent(audit.Namespace, audit.Delete, actor, r)
case *authrpc.CreateTokenRequest:
event = audit.NewEvent(audit.Token, audit.Create, actor, r)
event = audit.NewEvent(audit.NamespaceType, audit.Delete, actor, r)
case *flipt.DeleteRuleRequest:
event = audit.NewEvent(audit.RuleType, audit.Delete, actor, r)
}

resp, err := handler(ctx, req)
if err != nil {
// Short circuiting the middleware here since we have a non-nil event from
// detecting a delete.
if event != nil {
return resp, err
}

if event != nil {
span := trace.SpanFromContext(ctx)
span.AddEvent("event", trace.WithAttributes(event.DecodeToAttributes()...))
action := audit.GRPCMethodToAction(info.FullMethod)

switch r := resp.(type) {
case *flipt.Flag:
if action != "" {
event = audit.NewEvent(audit.FlagType, action, actor, audit.NewFlag(r))
}
case *flipt.Variant:
if action != "" {
event = audit.NewEvent(audit.VariantType, action, actor, audit.NewVariant(r))
}
case *flipt.Segment:
if action != "" {
event = audit.NewEvent(audit.SegmentType, action, actor, audit.NewSegment(r))
}
case *flipt.Distribution:
if action != "" {
event = audit.NewEvent(audit.DistributionType, action, actor, audit.NewDistribution(r))
}
case *flipt.Constraint:
if action != "" {
event = audit.NewEvent(audit.ConstraintType, action, actor, audit.NewConstraint(r))
}
case *flipt.Namespace:
if action != "" {
event = audit.NewEvent(audit.NamespaceType, action, actor, audit.NewNamespace(r))
}
case *flipt.Rule:
if action != "" {
event = audit.NewEvent(audit.RuleType, action, actor, audit.NewRule(r))
}
case *fauth.CreateTokenResponse:
event = audit.NewEvent(audit.TokenType, audit.Create, actor, r.Authentication.Metadata)
}

return resp, err
Expand Down
Loading

0 comments on commit 8a1655d

Please sign in to comment.