diff --git a/internal/server/audit/cloud/cloud_test.go b/internal/server/audit/cloud/cloud_test.go index f7ca8903b6..72b99d042d 100644 --- a/internal/server/audit/cloud/cloud_test.go +++ b/internal/server/audit/cloud/cloud_test.go @@ -27,13 +27,13 @@ func TestSink(t *testing.T) { err := s.SendAudits(context.TODO(), []audit.Event{ { Version: "0.1", - Type: flipt.SubjectFlag, - Action: flipt.ActionCreate, + Type: string(flipt.SubjectFlag), + Action: string(flipt.ActionCreate), }, { Version: "0.1", - Type: flipt.SubjectConstraint, - Action: flipt.ActionUpdate, + Type: string(flipt.SubjectConstraint), + Action: string(flipt.ActionUpdate), }, }) diff --git a/internal/server/audit/events.go b/internal/server/audit/events.go index c4876fe65f..2564aaa755 100644 --- a/internal/server/audit/events.go +++ b/internal/server/audit/events.go @@ -25,9 +25,9 @@ const ( type Event struct { Version string `json:"version"` - Type flipt.Subject `json:"type"` + Type string `json:"type"` - Action flipt.Action `json:"-"` + Action string `json:"action"` Metadata Metadata `json:"metadata"` @@ -36,26 +36,25 @@ type Event struct { Timestamp string `json:"timestamp"` } -func (e Event) MarshalJSON() ([]byte, error) { - - type EventAlias Event - - return json.Marshal(&struct { - *EventAlias - Action string `json:"action"` - }{ - EventAlias: (*EventAlias)(&e), - Action: e.Action.Past(), - }) - -} - // NewEvent is the constructor for an event. func NewEvent(r flipt.Request, actor *Actor, payload interface{}) *Event { + var action string + + switch r.Action { + case flipt.ActionCreate: + action = "created" + case flipt.ActionUpdate: + action = "updated" + case flipt.ActionDelete: + action = "deleted" + default: + action = "read" + } + return &Event{ Version: eventVersion, - Action: r.Action, - Type: r.Subject, + Action: action, + Type: string(r.Subject), Metadata: Metadata{ Actor: actor, }, @@ -79,14 +78,14 @@ func (e Event) DecodeToAttributes() []attribute.KeyValue { if e.Action != "" { akv = append(akv, attribute.KeyValue{ Key: eventActionKey, - Value: attribute.StringValue(string(e.Action)), + Value: attribute.StringValue(e.Action), }) } if e.Type != "" { akv = append(akv, attribute.KeyValue{ Key: eventTypeKey, - Value: attribute.StringValue(string(e.Type)), + Value: attribute.StringValue(e.Type), }) } @@ -138,9 +137,9 @@ func decodeToEvent(kvs []attribute.KeyValue) (*Event, error) { case eventVersionKey: e.Version = kv.Value.AsString() case eventActionKey: - e.Action = flipt.Action(kv.Value.AsString()) + e.Action = kv.Value.AsString() case eventTypeKey: - e.Type = flipt.Subject(kv.Value.AsString()) + e.Type = kv.Value.AsString() case eventTimestampKey: e.Timestamp = kv.Value.AsString() case eventMetadataActorKey: diff --git a/internal/server/audit/logfile/logfile_test.go b/internal/server/audit/logfile/logfile_test.go index 44a7fea522..1f277d1c0a 100644 --- a/internal/server/audit/logfile/logfile_test.go +++ b/internal/server/audit/logfile/logfile_test.go @@ -3,6 +3,7 @@ package logfile import ( "bytes" "context" + "encoding/json" "errors" "os" "path/filepath" @@ -207,15 +208,19 @@ func TestSink_SendAudits(t *testing.T) { require.NoError(t, err) require.NotNil(t, sink) - assert.NoError(t, sink.SendAudits(context.Background(), []audit.Event{ - { - Version: "1", - Type: flipt.SubjectFlag, - Action: flipt.ActionCreate, - }, - })) + e := audit.NewEvent(flipt.NewRequest(flipt.SubjectFlag, flipt.ActionCreate), nil, nil) + assert.NoError(t, sink.SendAudits(context.Background(), []audit.Event{*e})) + + assert.NotNil(t, f.Buffer) + + ee := &audit.Event{} + err = json.Unmarshal(f.Bytes(), ee) + require.NoError(t, err) + + assert.NotEmpty(t, ee.Version) + assert.Equal(t, e.Type, ee.Type) + assert.Equal(t, e.Action, ee.Action) + assert.NotEmpty(t, ee.Timestamp) - assert.JSONEq(t, `{"version":"1","type":"flag","action":"created","metadata":{},"payload":null,"timestamp":""} -`, f.Buffer.String()) assert.NoError(t, sink.Close()) } diff --git a/internal/server/audit/template/executer_test.go b/internal/server/audit/template/executer_test.go index 2cbcf1e451..2e8b7f25ae 100644 --- a/internal/server/audit/template/executer_test.go +++ b/internal/server/audit/template/executer_test.go @@ -44,8 +44,8 @@ func TestExecuter_JSON_Failure(t *testing.T) { } err = whTemplate.Execute(context.TODO(), audit.Event{ - Type: flipt.SubjectFlag, - Action: flipt.ActionCreate, + Type: string(flipt.SubjectFlag), + Action: string(flipt.ActionCreate), }) assert.EqualError(t, err, "invalid JSON: this is invalid JSON flag, create") @@ -67,8 +67,8 @@ func TestExecuter_Execute(t *testing.T) { } err = whTemplate.Execute(context.TODO(), audit.Event{ - Type: flipt.SubjectFlag, - Action: flipt.ActionCreate, + Type: string(flipt.SubjectFlag), + Action: string(flipt.ActionCreate), }) require.NoError(t, err) @@ -91,8 +91,8 @@ func TestExecuter_Execute_toJson_valid_Json(t *testing.T) { } err = whTemplate.Execute(context.TODO(), audit.Event{ - Type: flipt.SubjectFlag, - Action: flipt.ActionCreate, + Type: string(flipt.SubjectFlag), + Action: string(flipt.ActionCreate), Payload: &flipt.CreateFlagRequest{ Key: "foo", Name: "foo", diff --git a/internal/server/audit/template/template_test.go b/internal/server/audit/template/template_test.go index 3997496eb0..79b3437722 100644 --- a/internal/server/audit/template/template_test.go +++ b/internal/server/audit/template/template_test.go @@ -27,13 +27,13 @@ func TestSink(t *testing.T) { err := s.SendAudits(context.TODO(), []audit.Event{ { Version: "0.1", - Type: flipt.SubjectFlag, - Action: flipt.ActionCreate, + Type: string(flipt.SubjectFlag), + Action: string(flipt.ActionCreate), }, { Version: "0.1", - Type: flipt.SubjectConstraint, - Action: flipt.ActionUpdate, + Type: string(flipt.SubjectConstraint), + Action: string(flipt.ActionUpdate), }, }) diff --git a/internal/server/audit/webhook/client.go b/internal/server/audit/webhook/client.go index 916d3feb0e..c7339feb89 100644 --- a/internal/server/audit/webhook/client.go +++ b/internal/server/audit/webhook/client.go @@ -14,9 +14,7 @@ import ( "go.uber.org/zap" ) -const ( - fliptSignatureHeader = "x-flipt-webhook-signature" -) +const fliptSignatureHeader = "x-flipt-webhook-signature" var _ Client = (*webhookClient)(nil) diff --git a/internal/server/audit/webhook/client_test.go b/internal/server/audit/webhook/client_test.go index 4ec0461833..2d0353a216 100644 --- a/internal/server/audit/webhook/client_test.go +++ b/internal/server/audit/webhook/client_test.go @@ -40,8 +40,8 @@ func TestWebhookClient(t *testing.T) { } err := whclient.SendAudit(context.TODO(), audit.Event{ - Type: flipt.SubjectFlag, - Action: flipt.ActionCreate, + Type: string(flipt.SubjectFlag), + Action: string(flipt.ActionCreate), }) require.NoError(t, err) diff --git a/internal/server/audit/webhook/webhook_test.go b/internal/server/audit/webhook/webhook_test.go index 36f896930d..c40527a17a 100644 --- a/internal/server/audit/webhook/webhook_test.go +++ b/internal/server/audit/webhook/webhook_test.go @@ -25,13 +25,13 @@ func TestSink(t *testing.T) { err := s.SendAudits(context.TODO(), []audit.Event{ { Version: "0.1", - Type: flipt.SubjectFlag, - Action: flipt.ActionCreate, + Type: string(flipt.SubjectFlag), + Action: string(flipt.ActionCreate), }, { Version: "0.1", - Type: flipt.SubjectConstraint, - Action: flipt.ActionUpdate, + Type: string(flipt.SubjectConstraint), + Action: string(flipt.ActionUpdate), }, }) diff --git a/internal/server/authz/engine.go b/internal/server/authz/engine.go index a2c4a9f10f..2dc4ebcdad 100644 --- a/internal/server/authz/engine.go +++ b/internal/server/authz/engine.go @@ -27,10 +27,10 @@ default allow = false allow { input.role != "" input.action != "" - input.subject != "" + input.scope != "" permissions := get_permissions(input.role) - allowed(permissions, input.action, input.subject) + allowed(permissions, input.action, input.scope) } get_permissions(role) = result { @@ -39,28 +39,28 @@ get_permissions(role) = result { result = data.roles[idx].rules } -allowed(permissions, action, subject) { +allowed(permissions, action, scope) { permissions[action] # First, ensure the action key exists - subject_in_list(permissions[action], subject) # Check if the subject is in the list + scope_in_list(permissions[action], scope) # Check if the scope is in the list } -allowed(permissions, action, subject) { - permissions[action]["*"] # Checks if all subjects are allowed for the action +allowed(permissions, action, scope) { + permissions[action]["*"] # Checks if all scopes are allowed for the action } # Handles cases where "*" is provided for all actions -allowed(permissions, action, subject) { +allowed(permissions, action, scope) { permissions["*"] != null # Check if wildcard for all actions exists - subject_in_list(permissions["*"], subject) # Check if subject is universally allowed or specific to an action + scope_in_list(permissions["*"], scope) # Check if scope is universally allowed or specific to an action } # Helper to handle array membership or wildcard -subject_in_list(list, subject) { - list[_] = subject # Subject is explicitly listed in the permissions +scope_in_list(list, scope) { + list[_] = scope # Scope is explicitly listed in the permissions } -subject_in_list(list, subject) { - list[_] = "*" # Wildcard entry that permits all subjects +scope_in_list(list, scope) { + list[_] = "*" # Wildcard entry that permits all scopes } ` diff --git a/internal/server/authz/engine_test.go b/internal/server/authz/engine_test.go index 739ce26ee3..c5d8f4a7f0 100644 --- a/internal/server/authz/engine_test.go +++ b/internal/server/authz/engine_test.go @@ -26,7 +26,7 @@ func TestEngine_IsAllowed(t *testing.T) { input: `{ "role": "admin", "action": "create", - "subject": "flag" + "scope": "flag" }`, expected: true, }, @@ -35,7 +35,7 @@ func TestEngine_IsAllowed(t *testing.T) { input: `{ "role": "admin", "action": "read", - "subject": "flag" + "scope": "flag" }`, expected: true, }, @@ -44,7 +44,7 @@ func TestEngine_IsAllowed(t *testing.T) { input: `{ "role": "editor", "action": "create", - "subject": "flag" + "scope": "flag" }`, expected: true, }, @@ -53,7 +53,7 @@ func TestEngine_IsAllowed(t *testing.T) { input: `{ "role": "editor", "action": "read", - "subject": "flag" + "scope": "flag" }`, expected: true, }, @@ -62,7 +62,7 @@ func TestEngine_IsAllowed(t *testing.T) { input: `{ "role": "editor", "action": "create", - "subject": "namespace" + "scope": "namespace" }`, expected: false, }, @@ -71,7 +71,7 @@ func TestEngine_IsAllowed(t *testing.T) { input: `{ "role": "viewer", "action": "read", - "subject": "flag" + "scope": "flag" }`, expected: true, }, @@ -80,7 +80,7 @@ func TestEngine_IsAllowed(t *testing.T) { input: `{ "role": "viewer", "action": "create", - "subject": "flag" + "scope": "flag" }`, expected: false, }, diff --git a/internal/server/authz/middleware/grpc/middleware.go b/internal/server/authz/middleware/grpc/middleware.go index add831a38b..e03d9bda00 100644 --- a/internal/server/authz/middleware/grpc/middleware.go +++ b/internal/server/authz/middleware/grpc/middleware.go @@ -80,22 +80,27 @@ func AuthorizationRequiredInterceptor(logger *zap.Logger, policyVerifier authz.V } request := requester.Request() - action := request.Action + action := string(request.Action) if action == "" { logger.Error("unauthorized", zap.String("reason", "request action required")) return ctx, errUnauthorized } - sub := request.Subject - if sub == "" { - logger.Error("unauthorized", zap.String("reason", "request subject required")) + scope := string(request.Scope) + if scope == "" { + // fallback to subject if scope is not provided + scope = string(request.Subject) + } + + if scope == "" { + logger.Error("unauthorized", zap.String("reason", "request scope or subject required")) return ctx, errUnauthorized } allowed, err := policyVerifier.IsAllowed(ctx, map[string]interface{}{ - "role": role, - "action": action, - "subject": sub, + "role": role, + "action": action, + "scope": scope, }) if err != nil { diff --git a/internal/server/authz/middleware/grpc/middleware_test.go b/internal/server/authz/middleware/grpc/middleware_test.go index afd5b1ce65..76bc6d12f1 100644 --- a/internal/server/authz/middleware/grpc/middleware_test.go +++ b/internal/server/authz/middleware/grpc/middleware_test.go @@ -32,14 +32,14 @@ func (s *mockServer) SkipsAuthorization(ctx context.Context) bool { } type mockRequester struct { - action flipt.Action - subject flipt.Subject + action flipt.Action + scope flipt.Scope } func (r *mockRequester) Request() flipt.Request { return flipt.Request{ - Action: r.action, - Subject: r.subject, + Action: r.action, + Scope: r.scope, } } @@ -114,12 +114,12 @@ func TestAuthorizationRequiredInterceptor(t *testing.T) { }, }, req: &mockRequester{ - subject: "subject", + scope: "foo", }, wantAllowed: false, }, { - name: "missing subject", + name: "missing scope", authn: &authrpc.Authentication{ Metadata: map[string]string{ "io.flipt.auth.role": "admin", diff --git a/internal/server/authz/policies/default.json b/internal/server/authz/policies/default.json index 15c3ed6424..630aefd8e7 100644 --- a/internal/server/authz/policies/default.json +++ b/internal/server/authz/policies/default.json @@ -10,31 +10,10 @@ { "name": "editor", "rules": { - "create": [ - "flag", - "variant", - "rollout", - "rule", - "segment", - "distribution" - ], + "create": ["flag", "segment"], "read": ["*"], - "update": [ - "flag", - "variant", - "rollout", - "rule", - "segment", - "distribution" - ], - "delete": [ - "flag", - "variant", - "rollout", - "rule", - "segment", - "distribution" - ] + "update": ["flag", "segment"], + "delete": ["flag", "segment"] } }, { diff --git a/internal/server/middleware/grpc/middleware.go b/internal/server/middleware/grpc/middleware.go index 750a7ace30..8e56d19090 100644 --- a/internal/server/middleware/grpc/middleware.go +++ b/internal/server/middleware/grpc/middleware.go @@ -451,9 +451,7 @@ func AuditEventUnaryInterceptor(logger *zap.Logger, eventPairChecker EventPairCh defer func() { if event != nil { - ts := string(event.Type) - as := event.Action.Past() - eventPair := fmt.Sprintf("%s:%s", ts, as) + eventPair := fmt.Sprintf("%s:%s", event.Type, event.Action) exists := eventPairChecker.Check(eventPair) if exists { diff --git a/rpc/flipt/request.go b/rpc/flipt/request.go index 32e80fb00b..9e7cd72ee2 100644 --- a/rpc/flipt/request.go +++ b/rpc/flipt/request.go @@ -4,27 +4,20 @@ type Requester interface { Request() Request } -// Subject represents what resource is being acted on. +// Scope represents what resource or parent resource is being acted on. +type Scope string + +// Subject returns the subject of the request. type Subject string // Action represents the action being taken on the resource. type Action string -// Past returns the past tense of the action. Required for backwards compatibility with the audit log. -func (a Action) Past() string { - switch a { - case ActionCreate: - return "created" - case ActionUpdate: - return "updated" - case ActionDelete: - return "deleted" - default: - return "read" - } -} - const ( + ScopeNamespace Scope = "namespace" + ScopeFlag Scope = "flag" + ScopeSegment Scope = "segment" + SubjectConstraint Subject = "constraint" SubjectDistribution Subject = "distribution" SubjectFlag Subject = "flag" @@ -34,27 +27,43 @@ const ( SubjectSegment Subject = "segment" SubjectToken Subject = "token" SubjectVariant Subject = "variant" - SubjectAll Subject = "*" ActionCreate Action = "create" ActionDelete Action = "delete" ActionUpdate Action = "update" ActionRead Action = "read" - ActionAll Action = "*" ) type Request struct { + Namespaced + Scope Scope `json:"scope"` Subject Subject `json:"subject"` Action Action `json:"action"` } -func NewRequest(t Subject, a Action) Request { +func NewScopedRequest(scope Scope, s Subject, a Action) Request { + return Request{ + Scope: scope, + Subject: s, + Action: a, + } +} + +func NewRequest(s Subject, a Action) Request { return Request{ - Subject: t, + Subject: s, Action: a, } } +func newFlagScopedRequest(s Subject, a Action) Request { + return NewScopedRequest(ScopeFlag, s, a) +} + +func newSegmentScopedRequest(s Subject, a Action) Request { + return NewScopedRequest(ScopeSegment, s, a) +} + // Namespaces func (req *GetNamespaceRequest) Request() Request { return NewRequest(SubjectNamespace, ActionRead) @@ -99,65 +108,65 @@ func (req *DeleteFlagRequest) Request() Request { // Variants func (req *CreateVariantRequest) Request() Request { - return NewRequest(SubjectVariant, ActionCreate) + return newFlagScopedRequest(SubjectVariant, ActionCreate) } func (req *UpdateVariantRequest) Request() Request { - return NewRequest(SubjectVariant, ActionUpdate) + return newFlagScopedRequest(SubjectVariant, ActionUpdate) } func (req *DeleteVariantRequest) Request() Request { - return NewRequest(SubjectVariant, ActionDelete) + return newFlagScopedRequest(SubjectVariant, ActionDelete) } // Rules func (req *ListRuleRequest) Request() Request { - return NewRequest(SubjectRule, ActionRead) + return newFlagScopedRequest(SubjectRule, ActionRead) } func (req *GetRuleRequest) Request() Request { - return NewRequest(SubjectRule, ActionRead) + return newFlagScopedRequest(SubjectRule, ActionRead) } func (req *CreateRuleRequest) Request() Request { - return NewRequest(SubjectRule, ActionCreate) + return newFlagScopedRequest(SubjectRule, ActionCreate) } func (req *UpdateRuleRequest) Request() Request { - return NewRequest(SubjectRule, ActionUpdate) + return newFlagScopedRequest(SubjectRule, ActionUpdate) } func (req *OrderRulesRequest) Request() Request { - return NewRequest(SubjectRule, ActionUpdate) + return newFlagScopedRequest(SubjectRule, ActionUpdate) } func (req *DeleteRuleRequest) Request() Request { - return NewRequest(SubjectRule, ActionDelete) + return newFlagScopedRequest(SubjectRule, ActionDelete) } // Rollouts func (req *ListRolloutRequest) Request() Request { - return NewRequest(SubjectRollout, ActionRead) + return newFlagScopedRequest(SubjectRollout, ActionRead) } func (req *GetRolloutRequest) Request() Request { - return NewRequest(SubjectRollout, ActionRead) + return newFlagScopedRequest(SubjectRollout, ActionRead) } func (req *CreateRolloutRequest) Request() Request { - return NewRequest(SubjectRollout, ActionCreate) + return newFlagScopedRequest(SubjectRollout, ActionCreate) } func (req *UpdateRolloutRequest) Request() Request { - return NewRequest(SubjectRollout, ActionUpdate) + return newFlagScopedRequest(SubjectRollout, ActionUpdate) } func (req *OrderRolloutsRequest) Request() Request { - return NewRequest(SubjectRollout, ActionUpdate) + return newFlagScopedRequest(SubjectRollout, ActionUpdate) } func (req *DeleteRolloutRequest) Request() Request { - return NewRequest(SubjectRollout, ActionDelete) + return newFlagScopedRequest(SubjectRollout, ActionDelete) } // Segments @@ -183,26 +192,26 @@ func (req *DeleteSegmentRequest) Request() Request { // Constraints func (req *CreateConstraintRequest) Request() Request { - return NewRequest(SubjectConstraint, ActionCreate) + return newSegmentScopedRequest(SubjectConstraint, ActionCreate) } func (req *UpdateConstraintRequest) Request() Request { - return NewRequest(SubjectConstraint, ActionUpdate) + return newSegmentScopedRequest(SubjectConstraint, ActionUpdate) } func (req *DeleteConstraintRequest) Request() Request { - return NewRequest(SubjectConstraint, ActionDelete) + return newSegmentScopedRequest(SubjectConstraint, ActionDelete) } // Distributions func (req *CreateDistributionRequest) Request() Request { - return NewRequest(SubjectDistribution, ActionCreate) + return newSegmentScopedRequest(SubjectDistribution, ActionCreate) } func (req *UpdateDistributionRequest) Request() Request { - return NewRequest(SubjectDistribution, ActionUpdate) + return newSegmentScopedRequest(SubjectDistribution, ActionUpdate) } func (req *DeleteDistributionRequest) Request() Request { - return NewRequest(SubjectDistribution, ActionDelete) + return newSegmentScopedRequest(SubjectDistribution, ActionDelete) }