diff --git a/pkg/auditlogs/auditlogs.go b/pkg/auditlogs/auditlogs.go index d6b07ef2..8cf18a08 100644 --- a/pkg/auditlogs/auditlogs.go +++ b/pkg/auditlogs/auditlogs.go @@ -64,3 +64,8 @@ func CreateExport(ctx context.Context, e CreateExportOpts) (AuditLogExport, erro func GetExport(ctx context.Context, e GetExportOpts) (AuditLogExport, error) { return DefaultClient.GetExport(ctx, e) } + +// ListActions list all the audit log actions. +func ListActions(ctx context.Context, opts ListActionsOpts) (ListActionsResponse, error) { + return DefaultClient.ListActions(ctx, opts) +} diff --git a/pkg/auditlogs/auditlogs_test.go b/pkg/auditlogs/auditlogs_test.go index ea627ee4..0b4fd1bf 100644 --- a/pkg/auditlogs/auditlogs_test.go +++ b/pkg/auditlogs/auditlogs_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/workos/workos-go/v4/pkg/common" ) func TestAuditLogsCreateEvent(t *testing.T) { @@ -34,7 +35,6 @@ func TestAuditLogsCreateExport(t *testing.T) { body := AuditLogExport{} payload, _ := json.Marshal(body) w.Write(payload) - w.WriteHeader(http.StatusOK) } server := httptest.NewServer(http.HandlerFunc(handlerFunc)) @@ -56,7 +56,6 @@ func TestAuditLogsGetExport(t *testing.T) { body := AuditLogExport{} payload, _ := json.Marshal(body) w.Write(payload) - w.WriteHeader(http.StatusOK) } server := httptest.NewServer(http.HandlerFunc(handlerFunc)) @@ -72,3 +71,52 @@ func TestAuditLogsGetExport(t *testing.T) { _, err := GetExport(context.TODO(), GetExportOpts{}) require.NoError(t, err) } + +func TestAuditLogsListActions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(listActionsTestHandler)) + defer server.Close() + + DefaultClient = &Client{ + HTTPClient: server.Client(), + Endpoint: server.URL, + } + SetAPIKey("test") + + expectedResponse := ListActionsResponse{ + Data: []AuditLogAction{ + { + Name: "document.updated", + Schema: AuditLogActionSchema{ + Version: 1, + Actor: AuditLogActionSchemaActor{ + ID: "user_1", + Name: "Test User", + Type: "User", + }, + Targets: []AuditLogActionSchemaTarget{ + { + ID: "document_39127", + Name: "Test Document", + Type: "document", + }, + }, + Context: Context{ + Location: "192.0.0.8", + UserAgent: "Firefox", + }, + }, + CreatedAt: "2024-01-01T00:00:00Z", + UpdatedAt: "2024-01-01T00:00:00Z", + }, + }, + ListMetadata: common.ListMetadata{ + Before: "", + After: "", + }, + } + + actionsResponse, err := ListActions(context.Background(), ListActionsOpts{}) + + require.NoError(t, err) + require.Equal(t, expectedResponse, actionsResponse) +} diff --git a/pkg/auditlogs/client.go b/pkg/auditlogs/client.go index 48aecd13..aca19047 100644 --- a/pkg/auditlogs/client.go +++ b/pkg/auditlogs/client.go @@ -4,10 +4,13 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net/http" "sync" "time" + "github.com/google/go-querystring/query" + "github.com/workos/workos-go/v4/pkg/common" "github.com/workos/workos-go/v4/pkg/workos_errors" "github.com/workos/workos-go/v4/internal/workos" @@ -35,6 +38,9 @@ type Client struct { // to http.Client. HTTPClient *http.Client + // The WorkOS API URL. Defaults to https://api.workos.com. + Endpoint string + // The endpoint used to request WorkOS AuditLog events creation endpoint. // Defaults to https://api.workos.com/audit_logs/events. EventsEndpoint string @@ -186,6 +192,10 @@ func (c *Client) init() { c.HTTPClient = &http.Client{Timeout: 10 * time.Second} } + if c.Endpoint == "" { + c.Endpoint = "https://api.workos.com" + } + if c.EventsEndpoint == "" { c.EventsEndpoint = "https://api.workos.com/audit_logs/events" } @@ -199,6 +209,72 @@ func (c *Client) init() { } } +type AuditLogActionSchemaMetadataProperty struct { + Type string `json:"type"` + Nullable *bool `json:"nullable,omitempty"` +} + +type AuditLogActionSchemaMetadata struct { + Type string `json:"type"` + Properties map[string]AuditLogActionSchemaMetadataProperty `json:"properties,omitempty"` +} + +type AuditLogActionSchemaActor struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Metadata AuditLogActionSchemaMetadata `json:"metadata,omitempty"` +} + +type AuditLogActionSchemaTarget struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Metadata AuditLogActionSchemaMetadata `json:"metadata,omitempty"` +} + +type AuditLogActionSchema struct { + Version int `json:"version"` + Actor AuditLogActionSchemaActor `json:"actor"` + Targets []AuditLogActionSchemaTarget `json:"targets"` + Context Context `json:"context,omitempty"` + Metadata AuditLogActionSchemaMetadata `json:"metadata,omitempty"` +} + +type AuditLogAction struct { + Name string `json:"name"` + Schema AuditLogActionSchema `json:"schema"` + // The timestamp of when the Audit Log Action was created. + CreatedAt string `json:"created_at"` + // The timestamp of when the Audit Log Action was updated. + UpdatedAt string `json:"updated_at"` +} + +// ListActionsOpts contains the options to request Audit Log Actions. +type ListActionsOpts struct { + // Maximum number of records to return. + Limit int `url:"limit,omitempty"` + + // The order in which to paginate records. + Order Order `url:"order,omitempty"` + + // Pagination cursor to receive records before a provided Organization ID. + Before string `url:"before,omitempty"` + + // Pagination cursor to receive records after a provided Organization ID. + After string `url:"after,omitempty"` +} + +// ListActionsResponse describes the response structure when requesting +// Audit Log Actions. +type ListActionsResponse struct { + // List of Audit Log Actions. + Data []AuditLogAction `json:"data"` + + // Cursor pagination options. + ListMetadata common.ListMetadata `json:"list_metadata"` +} + // CreateEvent creates an Audit Log event. func (c *Client) CreateEvent(ctx context.Context, e CreateEventOpts) error { c.once.Do(c.init) @@ -295,6 +371,59 @@ func (c *Client) GetExport(ctx context.Context, e GetExportOpts) (AuditLogExport return body, err } +// ListActions gets a list of Audit Log Actions. +func (c *Client) ListActions( + ctx context.Context, + opts ListActionsOpts, +) (ListActionsResponse, error) { + c.once.Do(c.init) + + endpoint := fmt.Sprintf("%s/audit_logs/actions", c.Endpoint) + req, err := http.NewRequest( + http.MethodGet, + endpoint, + nil, + ) + if err != nil { + return ListActionsResponse{}, err + } + + req = req.WithContext(ctx) + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + + if opts.Limit == 0 { + opts.Limit = ResponseLimit + } + + if opts.Order == "" { + opts.Order = Desc + } + + q, err := query.Values(opts) + if err != nil { + return ListActionsResponse{}, err + } + + req.URL.RawQuery = q.Encode() + + res, err := c.HTTPClient.Do(req) + if err != nil { + return ListActionsResponse{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return ListActionsResponse{}, err + } + + var body ListActionsResponse + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + return body, err +} + func defaultTime(t time.Time) time.Time { if t == (time.Time{}) { t = time.Now().UTC() diff --git a/pkg/auditlogs/client_test.go b/pkg/auditlogs/client_test.go index 4a66f221..c3266b24 100644 --- a/pkg/auditlogs/client_test.go +++ b/pkg/auditlogs/client_test.go @@ -3,42 +3,17 @@ package auditlogs import ( "context" "encoding/json" - "github.com/workos/workos-go/v4/pkg/workos_errors" "net/http" "net/http/httptest" + "strings" "testing" - "time" + + "github.com/workos/workos-go/v4/pkg/common" + "github.com/workos/workos-go/v4/pkg/workos_errors" "github.com/stretchr/testify/require" ) -var event = CreateEventOpts{ - OrganizationID: "org_123456", - Event: Event{ - Action: "document.updated", - OccurredAt: time.Now(), - Actor: Actor{ - ID: "user_1", - Name: "Jon Smith", - Type: "User", - }, - Targets: []Target{ - { - ID: "document_39127", - Type: "document", - }, - }, - Context: Context{ - Location: "192.0.0.8", - UserAgent: "Firefox", - }, - Metadata: map[string]interface{}{ - "successful": true, - }, - }, - IdempotencyKey: "key", -} - func TestCreateEvent(t *testing.T) { t.Run("Idempotency Key is sent in the header", func(t *testing.T) { handler := defaultTestHandler{} @@ -129,7 +104,7 @@ func TestCreateEvent(t *testing.T) { errorResponse := Error{ Message: "Audit Log could not be processed due to missing or incorrect data.", Code: "invalid_audit_log", - Errors: []workos_errors.FieldError{workos_errors.FieldError{Field: "name", Code: "required_field"}}, + Errors: []workos_errors.FieldError{{Field: "name", Code: "required_field"}}, } body, _ := json.Marshal(errorResponse) w.Header().Set("Content-Type", "application/json") @@ -151,7 +126,7 @@ func TestCreateEvent(t *testing.T) { httpError := err.(workos_errors.HTTPError) require.Equal(t, httpError.Message, "Audit Log could not be processed due to missing or incorrect data.") - require.Equal(t, httpError.FieldErrors, []workos_errors.FieldError{workos_errors.FieldError{Field: "name", Code: "required_field"}}) + require.Equal(t, httpError.FieldErrors, []workos_errors.FieldError{{Field: "name", Code: "required_field"}}) require.Equal(t, httpError.ErrorCode, "invalid_audit_log") }) } @@ -280,6 +255,137 @@ func TestGetExports(t *testing.T) { }) } +func TestListActions(t *testing.T) { + tests := []struct { + scenario string + client *Client + options ListActionsOpts + expected ListActionsResponse + err bool + }{ + { + scenario: "Request without API Key returns an error", + client: &Client{}, + err: true, + }, + { + scenario: "Request returns Audit Log Actions", + client: &Client{ + APIKey: "test", + }, + options: ListActionsOpts{}, + expected: ListActionsResponse{ + Data: []AuditLogAction{ + { + Name: "document.updated", + Schema: AuditLogActionSchema{ + Version: 1, + Actor: AuditLogActionSchemaActor{ + ID: "user_1", + Name: "Test User", + Type: "User", + }, + Targets: []AuditLogActionSchemaTarget{ + { + ID: "document_39127", + Name: "Test Document", + Type: "document", + }, + }, + Context: Context{ + Location: "192.0.0.8", + UserAgent: "Firefox", + }, + }, + CreatedAt: "2024-01-01T00:00:00Z", + UpdatedAt: "2024-01-01T00:00:00Z", + }, + }, + ListMetadata: common.ListMetadata{ + Before: "", + After: "", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(listActionsTestHandler)) + defer server.Close() + + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() + + actions, err := client.ListActions(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, test.expected, actions) + }) + } +} + +func listActionsTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + if userAgent := r.Header.Get("User-Agent"); !strings.Contains(userAgent, "workos-go/") { + w.WriteHeader(http.StatusBadRequest) + return + } + + body, err := json.Marshal(struct { + ListActionsResponse + }{ + ListActionsResponse: ListActionsResponse{ + Data: []AuditLogAction{ + { + Name: "document.updated", + Schema: AuditLogActionSchema{ + Version: 1, + Actor: AuditLogActionSchemaActor{ + ID: "user_1", + Name: "Test User", + Type: "User", + }, + Targets: []AuditLogActionSchemaTarget{ + { + ID: "document_39127", + Name: "Test Document", + Type: "document", + }, + }, + Context: Context{ + Location: "192.0.0.8", + UserAgent: "Firefox", + }, + }, + CreatedAt: "2024-01-01T00:00:00Z", + UpdatedAt: "2024-01-01T00:00:00Z", + }, + }, + ListMetadata: common.ListMetadata{ + Before: "", + After: "", + }, + }, + }) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(body) +} + type defaultTestHandler struct { header *http.Header } diff --git a/pkg/widgets/client_test.go b/pkg/widgets/client_test.go index afdc6bd0..60b90d0f 100644 --- a/pkg/widgets/client_test.go +++ b/pkg/widgets/client_test.go @@ -39,7 +39,7 @@ func TestGetToken(t *testing.T) { for _, test := range tests { t.Run(test.scenario, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(generateLinkTestHandler)) + server := httptest.NewServer(http.HandlerFunc(getTokenTestHandler)) defer server.Close() client := test.client @@ -57,7 +57,7 @@ func TestGetToken(t *testing.T) { } } -func generateLinkTestHandler(w http.ResponseWriter, r *http.Request) { +func getTokenTestHandler(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if auth != "Bearer test" { http.Error(w, "bad auth", http.StatusUnauthorized) diff --git a/pkg/widgets/widgets_test.go b/pkg/widgets/widgets_test.go index 15a00ffa..b2a09ea4 100644 --- a/pkg/widgets/widgets_test.go +++ b/pkg/widgets/widgets_test.go @@ -10,7 +10,7 @@ import ( ) func TestWidgetsGetToken(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(generateLinkTestHandler)) + server := httptest.NewServer(http.HandlerFunc(getTokenTestHandler)) defer server.Close() DefaultClient = &Client{