From 768793b05cabdb372ad72b486ae3a4de26a23f8d Mon Sep 17 00:00:00 2001 From: dorsha Date: Thu, 11 Apr 2024 21:12:28 +0300 Subject: [PATCH 1/2] Audit create event --- README.md | 17 +++++++++++++-- descope/api/client.go | 6 ++++++ descope/internal/mgmt/audit.go | 16 +++++++++++++++ descope/internal/mgmt/audit_test.go | 24 ++++++++++++++++++++++ descope/sdk/mgmt.go | 3 ++- descope/tests/mocks/mgmt/managementmock.go | 10 +++++++++ descope/types.go | 9 ++++++++ 7 files changed, 82 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9074aa3b..2e72bbdf 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ These sections show how to use the SDK to perform API management functions. Befo 8. [Manage Flows](#manage-flows) 9. [Manage JWTs](#manage-jwts) 10. [Impersonate](#impersonate) -11. [Search Audit](#search-audit) +11. [Audit](#audit) 12. [Embedded Links](#embedded-links) 13. [Manage ReBAC Authz](#manage-rebac-authz) 14. [Manage Project](#manage-project) @@ -1201,7 +1201,7 @@ if err != nil { token, err := descopeClient.Management.User().GenerateEmbeddedLink(context.Background(), "desmond@descope.com", map[string]any{"key1":"value1"}) ``` -### Search Audit +### Audit You can perform an audit search for either specific values or full-text across the fields. Audit search is limited to the last 30 days. @@ -1219,6 +1219,19 @@ if err == nil { } ``` +You can also create audit event with data + +```go +err := descopeClient.Management.Audit().CreateEvent(context.Background(), &descope.AuditCreateOptions{ + UserID: , + Action: , + Type: , + ActorID: , + Data: , + TenantID: , +}) +``` + ### Manage ReBAC Authz Descope supports full relation based access control (ReBAC) using a zanzibar like schema and operations. diff --git a/descope/api/client.go b/descope/api/client.go index b3dc7038..e77b6886 100644 --- a/descope/api/client.go +++ b/descope/api/client.go @@ -172,6 +172,7 @@ var ( projectImportSnapshot: "mgmt/project/snapshot/import", projectValidateSnapshot: "mgmt/project/snapshot/validate", auditSearch: "mgmt/audit/search", + auditCreate: "mgmt/audit/event", authzSchemaSave: "mgmt/authz/schema/save", authzSchemaDelete: "mgmt/authz/schema/delete", authzSchemaLoad: "mgmt/authz/schema/load", @@ -369,6 +370,7 @@ type mgmtEndpoints struct { projectValidateSnapshot string auditSearch string + auditCreate string authzSchemaSave string authzSchemaDelete string @@ -959,6 +961,10 @@ func (e *endpoints) ManagementAuditSearch() string { return path.Join(e.version, e.mgmt.auditSearch) } +func (e *endpoints) ManagementAuditCreate() string { + return path.Join(e.version, e.mgmt.auditCreate) +} + func (e *endpoints) ManagementAuthzSchemaSave() string { return path.Join(e.version, e.mgmt.authzSchemaSave) } diff --git a/descope/internal/mgmt/audit.go b/descope/internal/mgmt/audit.go index edbdfeeb..935055d3 100644 --- a/descope/internal/mgmt/audit.go +++ b/descope/internal/mgmt/audit.go @@ -37,6 +37,22 @@ func (a *audit) Search(ctx context.Context, options *descope.AuditSearchOptions) return unmarshalAuditRecords(res) } +func (a *audit) CreateEvent(ctx context.Context, options *descope.AuditCreateOptions) error { + body := map[string]any{ + "userId": options.UserID, + "action": options.Action, + "type": options.Type, + "actorId": options.ActorID, + "data": options.Data, + "tenantId": options.TenantID, + } + _, err := a.client.DoPostRequest(ctx, api.Routes.ManagementAuditCreate(), body, nil, a.conf.ManagementKey) + if err != nil { + return err + } + return nil +} + type apiAuditRecord struct { ProjectID string `json:"projectId,omitempty"` UserID string `json:"userId,omitempty"` diff --git a/descope/internal/mgmt/audit_test.go b/descope/internal/mgmt/audit_test.go index c9727f73..6387c7ff 100644 --- a/descope/internal/mgmt/audit_test.go +++ b/descope/internal/mgmt/audit_test.go @@ -89,3 +89,27 @@ func TestAuditSearch(t *testing.T) { assert.EqualValues(t, response.Audits[0].Tenants, res[0].Tenants) assert.EqualValues(t, response.Audits[0].Data["x"], res[0].Data["x"]) } + +func TestAuditCreate(t *testing.T) { + auditCreateOptions := &descope.AuditCreateOptions{ + UserID: "userId", + Action: "action", + Type: "type", + ActorID: "actorId", + Data: map[string]interface{}{"aaa": "bbb"}, + TenantID: "tenantId", + } + mgmt := newTestMgmt(nil, helpers.DoOkWithBody(func(r *http.Request) { + require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key") + req := map[string]any{} + require.NoError(t, helpers.ReadBody(r, &req)) + assert.EqualValues(t, auditCreateOptions.UserID, req["userId"]) + assert.EqualValues(t, auditCreateOptions.Action, req["action"]) + assert.EqualValues(t, auditCreateOptions.Type, req["type"]) + assert.EqualValues(t, auditCreateOptions.ActorID, req["actorId"]) + assert.EqualValues(t, auditCreateOptions.Data, req["data"]) + assert.EqualValues(t, auditCreateOptions.TenantID, req["tenantId"]) + }, nil)) + err := mgmt.Audit().CreateEvent(context.Background(), auditCreateOptions) + require.NoError(t, err) +} diff --git a/descope/sdk/mgmt.go b/descope/sdk/mgmt.go index 5c320701..9939d786 100644 --- a/descope/sdk/mgmt.go +++ b/descope/sdk/mgmt.go @@ -699,6 +699,7 @@ type Project interface { // Provides search project audit trail type Audit interface { Search(ctx context.Context, options *descope.AuditSearchOptions) ([]*descope.AuditRecord, error) + CreateEvent(ctx context.Context, options *descope.AuditCreateOptions) error } // Provides authorization ReBAC capabilities @@ -802,7 +803,7 @@ type Management interface { // Provide functions for managing flows and theme in a project Flow() Flow - // Provides search project audit trail + // Provides functions for managing audit Audit() Audit // Provide functions for managing projects diff --git a/descope/tests/mocks/mgmt/managementmock.go b/descope/tests/mocks/mgmt/managementmock.go index 0b852bd8..259d11c5 100644 --- a/descope/tests/mocks/mgmt/managementmock.go +++ b/descope/tests/mocks/mgmt/managementmock.go @@ -1221,6 +1221,9 @@ type MockAudit struct { SearchAssert func(*descope.AuditSearchOptions) SearchResponse []*descope.AuditRecord SearchError error + + CreateEventAssert func(*descope.AuditCreateOptions) + CreateEventError error } func (m *MockAudit) Search(_ context.Context, options *descope.AuditSearchOptions) ([]*descope.AuditRecord, error) { @@ -1230,6 +1233,13 @@ func (m *MockAudit) Search(_ context.Context, options *descope.AuditSearchOption return m.SearchResponse, m.SearchError } +func (m *MockAudit) CreateEvent(_ context.Context, options *descope.AuditCreateOptions) error { + if m.CreateEventAssert != nil { + m.CreateEventAssert(options) + } + return m.CreateEventError +} + type MockAuthz struct { SaveSchemaAssert func(schema *descope.AuthzSchema, upgrade bool) SaveSchemaError error diff --git a/descope/types.go b/descope/types.go index eaf8a7d9..11dc943e 100644 --- a/descope/types.go +++ b/descope/types.go @@ -806,6 +806,15 @@ type AuditSearchOptions struct { Text string `json:"text"` // Free text search across all fields } +type AuditCreateOptions struct { + UserID string `json:"userId,omitempty"` + Action string `json:"action,omitempty"` + Type string `json:"type,omitempty"` + ActorID string `json:"actorId,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` + TenantID string `json:"tenantId,omitempty"` +} + type ExportSnapshotResponse struct { // All project settings and configurations represented as JSON files Files map[string]any `json:"files"` From a13c3397833c3462db7594ce91d4ec604eea50af Mon Sep 17 00:00:00 2001 From: dorsha Date: Thu, 11 Apr 2024 21:36:54 +0300 Subject: [PATCH 2/2] Validate attributes --- descope/internal/mgmt/audit.go | 12 +++ descope/internal/mgmt/audit_test.go | 118 ++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/descope/internal/mgmt/audit.go b/descope/internal/mgmt/audit.go index 935055d3..0b5af8dc 100644 --- a/descope/internal/mgmt/audit.go +++ b/descope/internal/mgmt/audit.go @@ -38,6 +38,18 @@ func (a *audit) Search(ctx context.Context, options *descope.AuditSearchOptions) } func (a *audit) CreateEvent(ctx context.Context, options *descope.AuditCreateOptions) error { + if options.Action == "" { + return utils.NewInvalidArgumentError("Action") + } + if options.TenantID == "" { + return utils.NewInvalidArgumentError("TenantID") + } + if options.Type == "" { + return utils.NewInvalidArgumentError("Type") + } + if options.ActorID == "" { + return utils.NewInvalidArgumentError("ActorID") + } body := map[string]any{ "userId": options.UserID, "action": options.Action, diff --git a/descope/internal/mgmt/audit_test.go b/descope/internal/mgmt/audit_test.go index 6387c7ff..a453f894 100644 --- a/descope/internal/mgmt/audit_test.go +++ b/descope/internal/mgmt/audit_test.go @@ -14,6 +14,7 @@ import ( ) func TestAuditSearch(t *testing.T) { + called := false response := &apiSearchAuditResponse{Audits: []*apiAuditRecord{ { ProjectID: "p1", @@ -57,6 +58,7 @@ func TestAuditSearch(t *testing.T) { Text: "kuku", } mgmt := newTestMgmt(nil, helpers.DoOkWithBody(func(r *http.Request) { + called = true require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key") req := map[string]any{} require.NoError(t, helpers.ReadBody(r, &req)) @@ -88,9 +90,11 @@ func TestAuditSearch(t *testing.T) { assert.EqualValues(t, response.Audits[0].ExternalIDs, res[0].LoginIDs) assert.EqualValues(t, response.Audits[0].Tenants, res[0].Tenants) assert.EqualValues(t, response.Audits[0].Data["x"], res[0].Data["x"]) + assert.True(t, called) } func TestAuditCreate(t *testing.T) { + called := false auditCreateOptions := &descope.AuditCreateOptions{ UserID: "userId", Action: "action", @@ -100,6 +104,7 @@ func TestAuditCreate(t *testing.T) { TenantID: "tenantId", } mgmt := newTestMgmt(nil, helpers.DoOkWithBody(func(r *http.Request) { + called = true require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key") req := map[string]any{} require.NoError(t, helpers.ReadBody(r, &req)) @@ -112,4 +117,117 @@ func TestAuditCreate(t *testing.T) { }, nil)) err := mgmt.Audit().CreateEvent(context.Background(), auditCreateOptions) require.NoError(t, err) + assert.True(t, called) +} + +func TestAuditCreateMissingArgumentAction(t *testing.T) { + called := false + auditCreateOptions := &descope.AuditCreateOptions{ + UserID: "userId", + Action: "", + Type: "type", + ActorID: "actorId", + Data: map[string]interface{}{"aaa": "bbb"}, + TenantID: "tenantId", + } + mgmt := newTestMgmt(nil, helpers.DoOkWithBody(func(r *http.Request) { + called = true + require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key") + req := map[string]any{} + require.NoError(t, helpers.ReadBody(r, &req)) + assert.EqualValues(t, auditCreateOptions.UserID, req["userId"]) + assert.EqualValues(t, auditCreateOptions.Action, req["action"]) + assert.EqualValues(t, auditCreateOptions.Type, req["type"]) + assert.EqualValues(t, auditCreateOptions.ActorID, req["actorId"]) + assert.EqualValues(t, auditCreateOptions.Data, req["data"]) + assert.EqualValues(t, auditCreateOptions.TenantID, req["tenantId"]) + }, nil)) + err := mgmt.Audit().CreateEvent(context.Background(), auditCreateOptions) + require.ErrorIs(t, err, descope.ErrInvalidArguments) + require.Contains(t, err.Error(), "Action") + assert.False(t, called) +} + +func TestAuditCreateMissingArgumentType(t *testing.T) { + called := false + auditCreateOptions := &descope.AuditCreateOptions{ + UserID: "userId", + Action: "action", + Type: "", + ActorID: "actorId", + Data: map[string]interface{}{"aaa": "bbb"}, + TenantID: "tenantId", + } + mgmt := newTestMgmt(nil, helpers.DoOkWithBody(func(r *http.Request) { + called = true + require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key") + req := map[string]any{} + require.NoError(t, helpers.ReadBody(r, &req)) + assert.EqualValues(t, auditCreateOptions.UserID, req["userId"]) + assert.EqualValues(t, auditCreateOptions.Action, req["action"]) + assert.EqualValues(t, auditCreateOptions.Type, req["type"]) + assert.EqualValues(t, auditCreateOptions.ActorID, req["actorId"]) + assert.EqualValues(t, auditCreateOptions.Data, req["data"]) + assert.EqualValues(t, auditCreateOptions.TenantID, req["tenantId"]) + }, nil)) + err := mgmt.Audit().CreateEvent(context.Background(), auditCreateOptions) + require.ErrorIs(t, err, descope.ErrInvalidArguments) + require.Contains(t, err.Error(), "Type") + assert.False(t, called) +} + +func TestAuditCreateMissingArgumentActorID(t *testing.T) { + called := false + auditCreateOptions := &descope.AuditCreateOptions{ + UserID: "userId", + Action: "action", + Type: "type", + ActorID: "", + Data: map[string]interface{}{"aaa": "bbb"}, + TenantID: "tenantId", + } + mgmt := newTestMgmt(nil, helpers.DoOkWithBody(func(r *http.Request) { + called = true + require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key") + req := map[string]any{} + require.NoError(t, helpers.ReadBody(r, &req)) + assert.EqualValues(t, auditCreateOptions.UserID, req["userId"]) + assert.EqualValues(t, auditCreateOptions.Action, req["action"]) + assert.EqualValues(t, auditCreateOptions.Type, req["type"]) + assert.EqualValues(t, auditCreateOptions.ActorID, req["actorId"]) + assert.EqualValues(t, auditCreateOptions.Data, req["data"]) + assert.EqualValues(t, auditCreateOptions.TenantID, req["tenantId"]) + }, nil)) + err := mgmt.Audit().CreateEvent(context.Background(), auditCreateOptions) + require.ErrorIs(t, err, descope.ErrInvalidArguments) + require.Contains(t, err.Error(), "ActorID") + assert.False(t, called) +} + +func TestAuditCreateMissingArgumentTenantID(t *testing.T) { + called := false + auditCreateOptions := &descope.AuditCreateOptions{ + UserID: "userId", + Action: "action", + Type: "type", + ActorID: "actor", + Data: map[string]interface{}{"aaa": "bbb"}, + TenantID: "", + } + mgmt := newTestMgmt(nil, helpers.DoOkWithBody(func(r *http.Request) { + called = true + require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key") + req := map[string]any{} + require.NoError(t, helpers.ReadBody(r, &req)) + assert.EqualValues(t, auditCreateOptions.UserID, req["userId"]) + assert.EqualValues(t, auditCreateOptions.Action, req["action"]) + assert.EqualValues(t, auditCreateOptions.Type, req["type"]) + assert.EqualValues(t, auditCreateOptions.ActorID, req["actorId"]) + assert.EqualValues(t, auditCreateOptions.Data, req["data"]) + assert.EqualValues(t, auditCreateOptions.TenantID, req["tenantId"]) + }, nil)) + err := mgmt.Audit().CreateEvent(context.Background(), auditCreateOptions) + require.ErrorIs(t, err, descope.ErrInvalidArguments) + require.Contains(t, err.Error(), "TenantID") + assert.False(t, called) }