Skip to content

Commit

Permalink
Audit create event (#424)
Browse files Browse the repository at this point in the history
  • Loading branch information
dorsha authored Apr 11, 2024
1 parent ddee365 commit 2381056
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 3 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1201,7 +1201,7 @@ if err != nil {
token, err := descopeClient.Management.User().GenerateEmbeddedLink(context.Background(), "[email protected]", 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.

Expand All @@ -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: <user-id>,
Action: <action>,
Type: <action>,
ActorID: <actor-id>,
Data: <data>,
TenantID: <tenant-id>,
})
```

### Manage ReBAC Authz

Descope supports full relation based access control (ReBAC) using a zanzibar like schema and operations.
Expand Down
6 changes: 6 additions & 0 deletions descope/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -369,6 +370,7 @@ type mgmtEndpoints struct {
projectValidateSnapshot string

auditSearch string
auditCreate string

authzSchemaSave string
authzSchemaDelete string
Expand Down Expand Up @@ -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)
}
Expand Down
28 changes: 28 additions & 0 deletions descope/internal/mgmt/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,34 @@ func (a *audit) Search(ctx context.Context, options *descope.AuditSearchOptions)
return unmarshalAuditRecords(res)
}

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,
"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"`
Expand Down
142 changes: 142 additions & 0 deletions descope/internal/mgmt/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
)

func TestAuditSearch(t *testing.T) {
called := false
response := &apiSearchAuditResponse{Audits: []*apiAuditRecord{
{
ProjectID: "p1",
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -88,4 +90,144 @@ 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",
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.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)
}
3 changes: 2 additions & 1 deletion descope/sdk/mgmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions descope/tests/mocks/mgmt/managementmock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions descope/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down

0 comments on commit 2381056

Please sign in to comment.