From edf05d122c81220043a58c02f8f588cf62715403 Mon Sep 17 00:00:00 2001 From: mattgd Date: Wed, 18 Dec 2024 17:33:39 -0500 Subject: [PATCH 1/6] Add list audit log actions API endpoint support. --- pkg/auditlogs/auditlogs.go | 5 + pkg/auditlogs/auditlogs_test.go | 52 +++++++++- pkg/auditlogs/client.go | 105 ++++++++++++++++++++ pkg/auditlogs/client_test.go | 168 ++++++++++++++++++++++++++------ 4 files changed, 297 insertions(+), 33 deletions(-) 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..d690d3fc 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: Actor{ + ID: "user_1", + Name: "Test User", + Type: "User", + }, + Targets: []Target{ + { + 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..02a8f7db 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,48 @@ func (c *Client) init() { } } +type AuditLogActionSchema struct { + Version int `json:"version"` + Actor Actor `json:"actor"` + Targets []Target `json:"targets"` + Context Context `json:"context"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type AuditLogAction struct { + Name string `json:"name"` + Schema AuditLogActionSchema `json:"schema"` + // The timestamp of when the Organization was created. + CreatedAt string `json:"created_at"` + // The timestamp of when the Organization 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 +347,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..e92a5989 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: Actor{ + ID: "user_1", + Name: "Test User", + Type: "User", + }, + Targets: []Target{ + { + 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: Actor{ + ID: "user_1", + Name: "Test User", + Type: "User", + }, + Targets: []Target{ + { + 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 } From 7fba0cdf89ccdb0b7a3cc6f211f0b79a815ffbc9 Mon Sep 17 00:00:00 2001 From: mattgd Date: Thu, 19 Dec 2024 10:25:34 -0500 Subject: [PATCH 2/6] Add API methods required for type generation. --- pkg/auditlogs/auditlogs.go | 5 + pkg/auditlogs/auditlogs_test.go | 52 ++++++++- pkg/auditlogs/client.go | 105 +++++++++++++++++ pkg/auditlogs/client_test.go | 168 +++++++++++++++++++++++----- pkg/permissions/README.md | 15 +++ pkg/permissions/client.go | 145 ++++++++++++++++++++++++ pkg/permissions/client_test.go | 107 ++++++++++++++++++ pkg/permissions/permissions.go | 26 +++++ pkg/permissions/permissions_test.go | 45 ++++++++ pkg/roles/README.md | 15 +++ pkg/roles/client.go | 129 +++++++++++++++++++++ pkg/roles/client_test.go | 98 ++++++++++++++++ pkg/roles/roles.go | 26 +++++ pkg/roles/roles_test.go | 40 +++++++ pkg/widgets/client_test.go | 4 +- pkg/widgets/widgets_test.go | 2 +- 16 files changed, 946 insertions(+), 36 deletions(-) create mode 100644 pkg/permissions/README.md create mode 100644 pkg/permissions/client.go create mode 100644 pkg/permissions/client_test.go create mode 100644 pkg/permissions/permissions.go create mode 100644 pkg/permissions/permissions_test.go create mode 100644 pkg/roles/README.md create mode 100644 pkg/roles/client.go create mode 100644 pkg/roles/client_test.go create mode 100644 pkg/roles/roles.go create mode 100644 pkg/roles/roles_test.go 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..d690d3fc 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: Actor{ + ID: "user_1", + Name: "Test User", + Type: "User", + }, + Targets: []Target{ + { + 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..02a8f7db 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,48 @@ func (c *Client) init() { } } +type AuditLogActionSchema struct { + Version int `json:"version"` + Actor Actor `json:"actor"` + Targets []Target `json:"targets"` + Context Context `json:"context"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type AuditLogAction struct { + Name string `json:"name"` + Schema AuditLogActionSchema `json:"schema"` + // The timestamp of when the Organization was created. + CreatedAt string `json:"created_at"` + // The timestamp of when the Organization 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 +347,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..e92a5989 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: Actor{ + ID: "user_1", + Name: "Test User", + Type: "User", + }, + Targets: []Target{ + { + 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: Actor{ + ID: "user_1", + Name: "Test User", + Type: "User", + }, + Targets: []Target{ + { + 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/permissions/README.md b/pkg/permissions/README.md new file mode 100644 index 00000000..2a5a2d7f --- /dev/null +++ b/pkg/permissions/README.md @@ -0,0 +1,15 @@ +# permissions + +[![Go Report Card](https://img.shields.io/badge/dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/workos/workos-go/v4/pkg/permissions) + +A Go package to make requests to the WorkOS Permissions API. + +## Install + +```sh +go get -u github.com/workos/workos-go/v4/pkg/permissions +``` + +## How it works + +See the [Roles and Permissions documentation](https://workos.com/docs/user-management/roles-and-permissions). diff --git a/pkg/permissions/client.go b/pkg/permissions/client.go new file mode 100644 index 00000000..3193bdf2 --- /dev/null +++ b/pkg/permissions/client.go @@ -0,0 +1,145 @@ +package permissions + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "sync" + "time" + + "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" +) + +// ResponseLimit is the default number of records to limit a response to. +const ResponseLimit = 10 + +// Order represents the order of records. +type Order string + +// Constants that enumerate the available orders. +const ( + Asc Order = "asc" + Desc Order = "desc" +) + +// Client represents a client that performs Permissions requests to the WorkOS API. +type Client struct { + // The WorkOS API Key. It can be found in https://dashboard.workos.com/api-keys. + APIKey string + + // The http.Client that is used to manage Permissions API calls to WorkOS. + // Defaults to http.Client. + HTTPClient *http.Client + + // The endpoint to WorkOS API. Defaults to https://api.workos.com. + Endpoint string + + // The function used to encode in JSON. Defaults to json.Marshal. + JSONEncode func(v interface{}) ([]byte, error) + + once sync.Once +} + +func (c *Client) init() { + if c.HTTPClient == nil { + c.HTTPClient = &http.Client{Timeout: 10 * time.Second} + } + + if c.Endpoint == "" { + c.Endpoint = "https://api.workos.com" + } + + if c.JSONEncode == nil { + c.JSONEncode = json.Marshal + } +} + +// Permission contains data about a WorkOS Permission. +type Permission struct { + // The Permission's unique identifier. + ID string `json:"id"` + + Name string `json:"name"` + + // The Permission's slug key for referencing it in code. + Slug string `json:"slug"` + + Description string `json:"description"` + + // Whether this Permission is a system permission. + System bool `json:"system"` + + // The timestamp of when the Permission was created. + CreatedAt string `json:"created_at"` + + // The timestamp of when the Permission was updated. + UpdatedAt string `json:"updated_at"` +} + +// ListPermissionsOpts contains the options to request Permissions. +type ListPermissionsOpts 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"` +} + +// ListPermissionsResponse describes the response structure when requesting Permissions +type ListPermissionsResponse struct { + // List of provisioned Permissions. + Data []Permission `json:"data"` + + // Cursor pagination options. + ListMetadata common.ListMetadata `json:"listMetadata"` +} + +// ListPermissions lists all permissions in a WorkOS environment. +func (c *Client) ListPermissions( + ctx context.Context, + opts ListPermissionsOpts, +) (ListPermissionsResponse, error) { + c.once.Do(c.init) + + data, err := c.JSONEncode(opts) + if err != nil { + return ListPermissionsResponse{}, err + } + + endpoint := fmt.Sprintf("%s/permissions", c.Endpoint) + req, err := http.NewRequest(http.MethodGet, endpoint, bytes.NewBuffer(data)) + if err != nil { + return ListPermissionsResponse{}, err + } + req = req.WithContext(ctx) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + + res, err := c.HTTPClient.Do(req) + if err != nil { + return ListPermissionsResponse{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return ListPermissionsResponse{}, err + } + + var body ListPermissionsResponse + + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + return body, err +} diff --git a/pkg/permissions/client_test.go b/pkg/permissions/client_test.go new file mode 100644 index 00000000..45364712 --- /dev/null +++ b/pkg/permissions/client_test.go @@ -0,0 +1,107 @@ +package permissions + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + "github.com/workos/workos-go/v4/pkg/common" +) + +func TestListPermissions(t *testing.T) { + tests := []struct { + scenario string + client *Client + options ListPermissionsOpts + expected ListPermissionsResponse + err bool + }{ + { + scenario: "Request without API Key returns an error", + client: &Client{}, + err: true, + }, + { + scenario: "Request returns list of permissions", + client: &Client{ + APIKey: "test", + }, + options: ListPermissionsOpts{}, + expected: ListPermissionsResponse{ + Data: []Permission{ + { + ID: "permission_01EHWNCE74X7JSDV0X3SZ3KJNY", + Name: "Manage users", + Slug: "users:manage", + Description: "Manage users in the application.", + System: false, + CreatedAt: "2024-12-01T00:00:00.000Z", + UpdatedAt: "2024-12-01T00:00:00.000Z", + }, + }, + ListMetadata: common.ListMetadata{ + Before: "", + After: "", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(listPermissionsTestHandler)) + defer server.Close() + + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() + + response, err := client.ListPermissions(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, test.expected, response) + }) + } +} + +func listPermissionsTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + body, err := json.Marshal(struct { + ListPermissionsResponse + }{ListPermissionsResponse{ + Data: []Permission{ + { + ID: "permission_01EHWNCE74X7JSDV0X3SZ3KJNY", + Name: "Manage users", + Slug: "users:manage", + Description: "Manage users in the application.", + System: false, + CreatedAt: "2024-12-01T00:00:00.000Z", + UpdatedAt: "2024-12-01T00:00:00.000Z", + }, + }, + ListMetadata: common.ListMetadata{ + Before: "", + After: "", + }, + }}) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(body) +} diff --git a/pkg/permissions/permissions.go b/pkg/permissions/permissions.go new file mode 100644 index 00000000..f705f17d --- /dev/null +++ b/pkg/permissions/permissions.go @@ -0,0 +1,26 @@ +// Package `permissions` provides a client wrapping the WorkOS Permissions API. +package permissions + +import ( + "context" +) + +// DefaultClient is the client used by SetAPIKey and Permissions functions. +var ( + DefaultClient = &Client{ + Endpoint: "https://api.workos.com", + } +) + +// SetAPIKey sets the WorkOS API key for Permissions API requests. +func SetAPIKey(apiKey string) { + DefaultClient.APIKey = apiKey +} + +// ListPermissions lists all Permissions in an environment. +func ListPermissions( + ctx context.Context, + opts ListPermissionsOpts, +) (ListPermissionsResponse, error) { + return DefaultClient.ListPermissions(ctx, opts) +} diff --git a/pkg/permissions/permissions_test.go b/pkg/permissions/permissions_test.go new file mode 100644 index 00000000..886cb63b --- /dev/null +++ b/pkg/permissions/permissions_test.go @@ -0,0 +1,45 @@ +package permissions + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + "github.com/workos/workos-go/v4/pkg/common" +) + +func TestPermissionsListPermissions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(listPermissionsTestHandler)) + defer server.Close() + + DefaultClient = &Client{ + HTTPClient: server.Client(), + Endpoint: server.URL, + } + SetAPIKey("test") + + expectedResponse := ListPermissionsResponse{ + Data: []Permission{ + { + ID: "permission_01EHWNCE74X7JSDV0X3SZ3KJNY", + Name: "Manage users", + Slug: "users:manage", + Description: "Manage users in the application.", + System: false, + CreatedAt: "2024-12-01T00:00:00.000Z", + UpdatedAt: "2024-12-01T00:00:00.000Z", + }, + }, + ListMetadata: common.ListMetadata{ + Before: "", + After: "", + }, + } + + response, err := ListPermissions(context.Background(), ListPermissionsOpts{}) + + require.NoError(t, err) + require.Equal(t, expectedResponse, response) +} diff --git a/pkg/roles/README.md b/pkg/roles/README.md new file mode 100644 index 00000000..7ad52072 --- /dev/null +++ b/pkg/roles/README.md @@ -0,0 +1,15 @@ +# roles + +[![Go Report Card](https://img.shields.io/badge/dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/workos/workos-go/v4/pkg/roles) + +A Go package to make requests to the WorkOS Roles API. + +## Install + +```sh +go get -u github.com/workos/workos-go/v4/pkg/roles +``` + +## How it works + +See the [Roles and Permissions documentation](https://workos.com/docs/user-management/roles-and-permissions). diff --git a/pkg/roles/client.go b/pkg/roles/client.go new file mode 100644 index 00000000..8e3152a2 --- /dev/null +++ b/pkg/roles/client.go @@ -0,0 +1,129 @@ +package roles + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "sync" + "time" + + "github.com/workos/workos-go/v4/pkg/workos_errors" + + "github.com/workos/workos-go/v4/internal/workos" +) + +// ResponseLimit is the default number of records to limit a response to. +const ResponseLimit = 10 + +// Client represents a client that performs Roles requests to the WorkOS API. +type Client struct { + // The WorkOS API Key. It can be found in https://dashboard.workos.com/api-keys. + APIKey string + + // The http.Client that is used to manage Roles API calls to WorkOS. + // Defaults to http.Client. + HTTPClient *http.Client + + // The endpoint to WorkOS API. Defaults to https://api.workos.com. + Endpoint string + + // The function used to encode in JSON. Defaults to json.Marshal. + JSONEncode func(v interface{}) ([]byte, error) + + once sync.Once +} + +func (c *Client) init() { + if c.HTTPClient == nil { + c.HTTPClient = &http.Client{Timeout: 10 * time.Second} + } + + if c.Endpoint == "" { + c.Endpoint = "https://api.workos.com" + } + + if c.JSONEncode == nil { + c.JSONEncode = json.Marshal + } +} + +// RoleType represents the type of a Role. +type RoleType string + +// Constants that enumerate the type of a Role. +const ( + Environment RoleType = "EnvironmentRole" + Organization RoleType = "OrganizationRole" +) + +// Role contains data about a WorkOS Role. +type Role struct { + // The Role's unique identifier. + ID string `json:"id"` + + Name string `json:"name"` + + // The Role's slug key for referencing it in code. + Slug string `json:"slug"` + + Description string `json:"description"` + + // The type of role + Type RoleType `json:"type"` + + // The timestamp of when the Role was created. + CreatedAt string `json:"created_at"` + + // The timestamp of when the Role was updated. + UpdatedAt string `json:"updated_at"` +} + +// ListRolesOpts contains the options to request Roles. +type ListRolesOpts struct{} + +// ListRolesResponse describes the response structure when requesting Roles. +type ListRolesResponse struct { + // List of provisioned Roles. + Data []Role `json:"data"` +} + +// ListRoles lists all roles in a WorkOS environment. +func (c *Client) ListRoles( + ctx context.Context, + opts ListRolesOpts, +) (ListRolesResponse, error) { + c.once.Do(c.init) + + data, err := c.JSONEncode(opts) + if err != nil { + return ListRolesResponse{}, err + } + + endpoint := fmt.Sprintf("%s/roles", c.Endpoint) + req, err := http.NewRequest(http.MethodGet, endpoint, bytes.NewBuffer(data)) + if err != nil { + return ListRolesResponse{}, err + } + req = req.WithContext(ctx) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + + res, err := c.HTTPClient.Do(req) + if err != nil { + return ListRolesResponse{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return ListRolesResponse{}, err + } + + var body ListRolesResponse + + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + return body, err +} diff --git a/pkg/roles/client_test.go b/pkg/roles/client_test.go new file mode 100644 index 00000000..de010072 --- /dev/null +++ b/pkg/roles/client_test.go @@ -0,0 +1,98 @@ +package roles + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestListRoles(t *testing.T) { + tests := []struct { + scenario string + client *Client + options ListRolesOpts + expected ListRolesResponse + err bool + }{ + { + scenario: "Request without API Key returns an error", + client: &Client{}, + err: true, + }, + { + scenario: "Request returns list of roles", + client: &Client{ + APIKey: "test", + }, + options: ListRolesOpts{}, + expected: ListRolesResponse{ + Data: []Role{ + { + ID: "role_01EHWNCE74X7JSDV0X3SZ3KJNY", + Name: "Member", + Slug: "member", + Description: "The default role for all users.", + Type: Environment, + CreatedAt: "2024-12-01T00:00:00.000Z", + UpdatedAt: "2024-12-01T00:00:00.000Z", + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(listRolesTestHandler)) + defer server.Close() + + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() + + response, err := client.ListRoles(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, test.expected, response) + }) + } +} + +func listRolesTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + body, err := json.Marshal(struct { + ListRolesResponse + }{ListRolesResponse{ + Data: []Role{ + { + ID: "role_01EHWNCE74X7JSDV0X3SZ3KJNY", + Name: "Member", + Slug: "member", + Description: "The default role for all users.", + Type: Environment, + CreatedAt: "2024-12-01T00:00:00.000Z", + UpdatedAt: "2024-12-01T00:00:00.000Z", + }, + }, + }}) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(body) +} diff --git a/pkg/roles/roles.go b/pkg/roles/roles.go new file mode 100644 index 00000000..0312bfd9 --- /dev/null +++ b/pkg/roles/roles.go @@ -0,0 +1,26 @@ +// Package `roles` provides a client wrapping the WorkOS Roles API. +package roles + +import ( + "context" +) + +// DefaultClient is the client used by SetAPIKey and Roles functions. +var ( + DefaultClient = &Client{ + Endpoint: "https://api.workos.com", + } +) + +// SetAPIKey sets the WorkOS API key for Roles API requests. +func SetAPIKey(apiKey string) { + DefaultClient.APIKey = apiKey +} + +// ListRoles lists all Roles in an environment. +func ListRoles( + ctx context.Context, + opts ListRolesOpts, +) (ListRolesResponse, error) { + return DefaultClient.ListRoles(ctx, opts) +} diff --git a/pkg/roles/roles_test.go b/pkg/roles/roles_test.go new file mode 100644 index 00000000..603ada87 --- /dev/null +++ b/pkg/roles/roles_test.go @@ -0,0 +1,40 @@ +package roles + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRolesListRoles(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(listRolesTestHandler)) + defer server.Close() + + DefaultClient = &Client{ + HTTPClient: server.Client(), + Endpoint: server.URL, + } + SetAPIKey("test") + + expectedResponse := ListRolesResponse{ + Data: []Role{ + { + ID: "role_01EHWNCE74X7JSDV0X3SZ3KJNY", + Name: "Member", + Slug: "member", + Description: "The default role for all users.", + Type: Environment, + CreatedAt: "2024-12-01T00:00:00.000Z", + UpdatedAt: "2024-12-01T00:00:00.000Z", + }, + }, + } + + response, err := ListRoles(context.Background(), ListRolesOpts{}) + + require.NoError(t, err) + require.Equal(t, expectedResponse, response) +} 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{ From c298a262009bf7b7d8e30963acd75bca5bcaaa28 Mon Sep 17 00:00:00 2001 From: mattgd Date: Thu, 19 Dec 2024 12:04:48 -0500 Subject: [PATCH 3/6] Fix types. --- pkg/auditlogs/auditlogs_test.go | 4 ++-- pkg/auditlogs/client.go | 34 ++++++++++++++++++++++++++++----- pkg/auditlogs/client_test.go | 8 ++++---- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/pkg/auditlogs/auditlogs_test.go b/pkg/auditlogs/auditlogs_test.go index d690d3fc..0b4fd1bf 100644 --- a/pkg/auditlogs/auditlogs_test.go +++ b/pkg/auditlogs/auditlogs_test.go @@ -88,12 +88,12 @@ func TestAuditLogsListActions(t *testing.T) { Name: "document.updated", Schema: AuditLogActionSchema{ Version: 1, - Actor: Actor{ + Actor: AuditLogActionSchemaActor{ ID: "user_1", Name: "Test User", Type: "User", }, - Targets: []Target{ + Targets: []AuditLogActionSchemaTarget{ { ID: "document_39127", Name: "Test Document", diff --git a/pkg/auditlogs/client.go b/pkg/auditlogs/client.go index 02a8f7db..ea9dd934 100644 --- a/pkg/auditlogs/client.go +++ b/pkg/auditlogs/client.go @@ -209,12 +209,36 @@ 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:"metadata,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 Actor `json:"actor"` - Targets []Target `json:"targets"` - Context Context `json:"context"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + Version int `json:"version"` + Actor AuditLogActionSchemaActor `json:"actor"` + Targets []AuditLogActionSchemaTarget `json:"targets"` + Context Context `json:"context"` + Metadata AuditLogActionSchemaMetadata `json:"metadata,omitempty"` } type AuditLogAction struct { diff --git a/pkg/auditlogs/client_test.go b/pkg/auditlogs/client_test.go index e92a5989..c3266b24 100644 --- a/pkg/auditlogs/client_test.go +++ b/pkg/auditlogs/client_test.go @@ -280,12 +280,12 @@ func TestListActions(t *testing.T) { Name: "document.updated", Schema: AuditLogActionSchema{ Version: 1, - Actor: Actor{ + Actor: AuditLogActionSchemaActor{ ID: "user_1", Name: "Test User", Type: "User", }, - Targets: []Target{ + Targets: []AuditLogActionSchemaTarget{ { ID: "document_39127", Name: "Test Document", @@ -350,12 +350,12 @@ func listActionsTestHandler(w http.ResponseWriter, r *http.Request) { Name: "document.updated", Schema: AuditLogActionSchema{ Version: 1, - Actor: Actor{ + Actor: AuditLogActionSchemaActor{ ID: "user_1", Name: "Test User", Type: "User", }, - Targets: []Target{ + Targets: []AuditLogActionSchemaTarget{ { ID: "document_39127", Name: "Test Document", From 3ecd219ec436e8bbcb0c12e7a1c98c8cc9b440a9 Mon Sep 17 00:00:00 2001 From: mattgd Date: Thu, 19 Dec 2024 12:05:24 -0500 Subject: [PATCH 4/6] Update audit logs types. --- pkg/auditlogs/auditlogs_test.go | 4 ++-- pkg/auditlogs/client.go | 34 ++++++++++++++++++++++++++++----- pkg/auditlogs/client_test.go | 8 ++++---- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/pkg/auditlogs/auditlogs_test.go b/pkg/auditlogs/auditlogs_test.go index d690d3fc..0b4fd1bf 100644 --- a/pkg/auditlogs/auditlogs_test.go +++ b/pkg/auditlogs/auditlogs_test.go @@ -88,12 +88,12 @@ func TestAuditLogsListActions(t *testing.T) { Name: "document.updated", Schema: AuditLogActionSchema{ Version: 1, - Actor: Actor{ + Actor: AuditLogActionSchemaActor{ ID: "user_1", Name: "Test User", Type: "User", }, - Targets: []Target{ + Targets: []AuditLogActionSchemaTarget{ { ID: "document_39127", Name: "Test Document", diff --git a/pkg/auditlogs/client.go b/pkg/auditlogs/client.go index 02a8f7db..ea9dd934 100644 --- a/pkg/auditlogs/client.go +++ b/pkg/auditlogs/client.go @@ -209,12 +209,36 @@ 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:"metadata,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 Actor `json:"actor"` - Targets []Target `json:"targets"` - Context Context `json:"context"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + Version int `json:"version"` + Actor AuditLogActionSchemaActor `json:"actor"` + Targets []AuditLogActionSchemaTarget `json:"targets"` + Context Context `json:"context"` + Metadata AuditLogActionSchemaMetadata `json:"metadata,omitempty"` } type AuditLogAction struct { diff --git a/pkg/auditlogs/client_test.go b/pkg/auditlogs/client_test.go index e92a5989..c3266b24 100644 --- a/pkg/auditlogs/client_test.go +++ b/pkg/auditlogs/client_test.go @@ -280,12 +280,12 @@ func TestListActions(t *testing.T) { Name: "document.updated", Schema: AuditLogActionSchema{ Version: 1, - Actor: Actor{ + Actor: AuditLogActionSchemaActor{ ID: "user_1", Name: "Test User", Type: "User", }, - Targets: []Target{ + Targets: []AuditLogActionSchemaTarget{ { ID: "document_39127", Name: "Test Document", @@ -350,12 +350,12 @@ func listActionsTestHandler(w http.ResponseWriter, r *http.Request) { Name: "document.updated", Schema: AuditLogActionSchema{ Version: 1, - Actor: Actor{ + Actor: AuditLogActionSchemaActor{ ID: "user_1", Name: "Test User", Type: "User", }, - Targets: []Target{ + Targets: []AuditLogActionSchemaTarget{ { ID: "document_39127", Name: "Test Document", From f955b0c971ae7dbf2add772f4a0743379bdd31fd Mon Sep 17 00:00:00 2001 From: mattgd Date: Thu, 19 Dec 2024 16:19:02 -0500 Subject: [PATCH 5/6] Fix schema properties. --- pkg/auditlogs/client.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/auditlogs/client.go b/pkg/auditlogs/client.go index ea9dd934..aca19047 100644 --- a/pkg/auditlogs/client.go +++ b/pkg/auditlogs/client.go @@ -216,7 +216,7 @@ type AuditLogActionSchemaMetadataProperty struct { type AuditLogActionSchemaMetadata struct { Type string `json:"type"` - Properties map[string]AuditLogActionSchemaMetadataProperty `json:"metadata,omitempty"` + Properties map[string]AuditLogActionSchemaMetadataProperty `json:"properties,omitempty"` } type AuditLogActionSchemaActor struct { @@ -237,16 +237,16 @@ type AuditLogActionSchema struct { Version int `json:"version"` Actor AuditLogActionSchemaActor `json:"actor"` Targets []AuditLogActionSchemaTarget `json:"targets"` - Context Context `json:"context"` + 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 Organization was created. + // The timestamp of when the Audit Log Action was created. CreatedAt string `json:"created_at"` - // The timestamp of when the Organization was updated. + // The timestamp of when the Audit Log Action was updated. UpdatedAt string `json:"updated_at"` } From 76c0e7b2434892da617111d86c123a88aef04c2b Mon Sep 17 00:00:00 2001 From: mattgd Date: Tue, 14 Jan 2025 08:50:20 -0500 Subject: [PATCH 6/6] Update changes. --- pkg/permissions/README.md | 15 --- pkg/permissions/client.go | 145 ---------------------------- pkg/permissions/client_test.go | 107 -------------------- pkg/permissions/permissions.go | 26 ----- pkg/permissions/permissions_test.go | 45 --------- pkg/roles/client_test.go | 98 ------------------- pkg/roles/roles.go | 26 ----- pkg/roles/roles_test.go | 40 -------- 8 files changed, 502 deletions(-) delete mode 100644 pkg/permissions/README.md delete mode 100644 pkg/permissions/client.go delete mode 100644 pkg/permissions/client_test.go delete mode 100644 pkg/permissions/permissions.go delete mode 100644 pkg/permissions/permissions_test.go delete mode 100644 pkg/roles/client_test.go delete mode 100644 pkg/roles/roles.go delete mode 100644 pkg/roles/roles_test.go diff --git a/pkg/permissions/README.md b/pkg/permissions/README.md deleted file mode 100644 index 2a5a2d7f..00000000 --- a/pkg/permissions/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# permissions - -[![Go Report Card](https://img.shields.io/badge/dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/workos/workos-go/v4/pkg/permissions) - -A Go package to make requests to the WorkOS Permissions API. - -## Install - -```sh -go get -u github.com/workos/workos-go/v4/pkg/permissions -``` - -## How it works - -See the [Roles and Permissions documentation](https://workos.com/docs/user-management/roles-and-permissions). diff --git a/pkg/permissions/client.go b/pkg/permissions/client.go deleted file mode 100644 index 3193bdf2..00000000 --- a/pkg/permissions/client.go +++ /dev/null @@ -1,145 +0,0 @@ -package permissions - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "sync" - "time" - - "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" -) - -// ResponseLimit is the default number of records to limit a response to. -const ResponseLimit = 10 - -// Order represents the order of records. -type Order string - -// Constants that enumerate the available orders. -const ( - Asc Order = "asc" - Desc Order = "desc" -) - -// Client represents a client that performs Permissions requests to the WorkOS API. -type Client struct { - // The WorkOS API Key. It can be found in https://dashboard.workos.com/api-keys. - APIKey string - - // The http.Client that is used to manage Permissions API calls to WorkOS. - // Defaults to http.Client. - HTTPClient *http.Client - - // The endpoint to WorkOS API. Defaults to https://api.workos.com. - Endpoint string - - // The function used to encode in JSON. Defaults to json.Marshal. - JSONEncode func(v interface{}) ([]byte, error) - - once sync.Once -} - -func (c *Client) init() { - if c.HTTPClient == nil { - c.HTTPClient = &http.Client{Timeout: 10 * time.Second} - } - - if c.Endpoint == "" { - c.Endpoint = "https://api.workos.com" - } - - if c.JSONEncode == nil { - c.JSONEncode = json.Marshal - } -} - -// Permission contains data about a WorkOS Permission. -type Permission struct { - // The Permission's unique identifier. - ID string `json:"id"` - - Name string `json:"name"` - - // The Permission's slug key for referencing it in code. - Slug string `json:"slug"` - - Description string `json:"description"` - - // Whether this Permission is a system permission. - System bool `json:"system"` - - // The timestamp of when the Permission was created. - CreatedAt string `json:"created_at"` - - // The timestamp of when the Permission was updated. - UpdatedAt string `json:"updated_at"` -} - -// ListPermissionsOpts contains the options to request Permissions. -type ListPermissionsOpts 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"` -} - -// ListPermissionsResponse describes the response structure when requesting Permissions -type ListPermissionsResponse struct { - // List of provisioned Permissions. - Data []Permission `json:"data"` - - // Cursor pagination options. - ListMetadata common.ListMetadata `json:"listMetadata"` -} - -// ListPermissions lists all permissions in a WorkOS environment. -func (c *Client) ListPermissions( - ctx context.Context, - opts ListPermissionsOpts, -) (ListPermissionsResponse, error) { - c.once.Do(c.init) - - data, err := c.JSONEncode(opts) - if err != nil { - return ListPermissionsResponse{}, err - } - - endpoint := fmt.Sprintf("%s/permissions", c.Endpoint) - req, err := http.NewRequest(http.MethodGet, endpoint, bytes.NewBuffer(data)) - if err != nil { - return ListPermissionsResponse{}, err - } - req = req.WithContext(ctx) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+c.APIKey) - req.Header.Set("User-Agent", "workos-go/"+workos.Version) - - res, err := c.HTTPClient.Do(req) - if err != nil { - return ListPermissionsResponse{}, err - } - defer res.Body.Close() - - if err = workos_errors.TryGetHTTPError(res); err != nil { - return ListPermissionsResponse{}, err - } - - var body ListPermissionsResponse - - dec := json.NewDecoder(res.Body) - err = dec.Decode(&body) - return body, err -} diff --git a/pkg/permissions/client_test.go b/pkg/permissions/client_test.go deleted file mode 100644 index 45364712..00000000 --- a/pkg/permissions/client_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package permissions - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/require" - "github.com/workos/workos-go/v4/pkg/common" -) - -func TestListPermissions(t *testing.T) { - tests := []struct { - scenario string - client *Client - options ListPermissionsOpts - expected ListPermissionsResponse - err bool - }{ - { - scenario: "Request without API Key returns an error", - client: &Client{}, - err: true, - }, - { - scenario: "Request returns list of permissions", - client: &Client{ - APIKey: "test", - }, - options: ListPermissionsOpts{}, - expected: ListPermissionsResponse{ - Data: []Permission{ - { - ID: "permission_01EHWNCE74X7JSDV0X3SZ3KJNY", - Name: "Manage users", - Slug: "users:manage", - Description: "Manage users in the application.", - System: false, - CreatedAt: "2024-12-01T00:00:00.000Z", - UpdatedAt: "2024-12-01T00:00:00.000Z", - }, - }, - ListMetadata: common.ListMetadata{ - Before: "", - After: "", - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.scenario, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(listPermissionsTestHandler)) - defer server.Close() - - client := test.client - client.Endpoint = server.URL - client.HTTPClient = server.Client() - - response, err := client.ListPermissions(context.Background(), test.options) - if test.err { - require.Error(t, err) - return - } - require.NoError(t, err) - require.Equal(t, test.expected, response) - }) - } -} - -func listPermissionsTestHandler(w http.ResponseWriter, r *http.Request) { - auth := r.Header.Get("Authorization") - if auth != "Bearer test" { - http.Error(w, "bad auth", http.StatusUnauthorized) - return - } - - body, err := json.Marshal(struct { - ListPermissionsResponse - }{ListPermissionsResponse{ - Data: []Permission{ - { - ID: "permission_01EHWNCE74X7JSDV0X3SZ3KJNY", - Name: "Manage users", - Slug: "users:manage", - Description: "Manage users in the application.", - System: false, - CreatedAt: "2024-12-01T00:00:00.000Z", - UpdatedAt: "2024-12-01T00:00:00.000Z", - }, - }, - ListMetadata: common.ListMetadata{ - Before: "", - After: "", - }, - }}) - - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - w.Write(body) -} diff --git a/pkg/permissions/permissions.go b/pkg/permissions/permissions.go deleted file mode 100644 index f705f17d..00000000 --- a/pkg/permissions/permissions.go +++ /dev/null @@ -1,26 +0,0 @@ -// Package `permissions` provides a client wrapping the WorkOS Permissions API. -package permissions - -import ( - "context" -) - -// DefaultClient is the client used by SetAPIKey and Permissions functions. -var ( - DefaultClient = &Client{ - Endpoint: "https://api.workos.com", - } -) - -// SetAPIKey sets the WorkOS API key for Permissions API requests. -func SetAPIKey(apiKey string) { - DefaultClient.APIKey = apiKey -} - -// ListPermissions lists all Permissions in an environment. -func ListPermissions( - ctx context.Context, - opts ListPermissionsOpts, -) (ListPermissionsResponse, error) { - return DefaultClient.ListPermissions(ctx, opts) -} diff --git a/pkg/permissions/permissions_test.go b/pkg/permissions/permissions_test.go deleted file mode 100644 index 886cb63b..00000000 --- a/pkg/permissions/permissions_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package permissions - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/require" - "github.com/workos/workos-go/v4/pkg/common" -) - -func TestPermissionsListPermissions(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(listPermissionsTestHandler)) - defer server.Close() - - DefaultClient = &Client{ - HTTPClient: server.Client(), - Endpoint: server.URL, - } - SetAPIKey("test") - - expectedResponse := ListPermissionsResponse{ - Data: []Permission{ - { - ID: "permission_01EHWNCE74X7JSDV0X3SZ3KJNY", - Name: "Manage users", - Slug: "users:manage", - Description: "Manage users in the application.", - System: false, - CreatedAt: "2024-12-01T00:00:00.000Z", - UpdatedAt: "2024-12-01T00:00:00.000Z", - }, - }, - ListMetadata: common.ListMetadata{ - Before: "", - After: "", - }, - } - - response, err := ListPermissions(context.Background(), ListPermissionsOpts{}) - - require.NoError(t, err) - require.Equal(t, expectedResponse, response) -} diff --git a/pkg/roles/client_test.go b/pkg/roles/client_test.go deleted file mode 100644 index de010072..00000000 --- a/pkg/roles/client_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package roles - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestListRoles(t *testing.T) { - tests := []struct { - scenario string - client *Client - options ListRolesOpts - expected ListRolesResponse - err bool - }{ - { - scenario: "Request without API Key returns an error", - client: &Client{}, - err: true, - }, - { - scenario: "Request returns list of roles", - client: &Client{ - APIKey: "test", - }, - options: ListRolesOpts{}, - expected: ListRolesResponse{ - Data: []Role{ - { - ID: "role_01EHWNCE74X7JSDV0X3SZ3KJNY", - Name: "Member", - Slug: "member", - Description: "The default role for all users.", - Type: Environment, - CreatedAt: "2024-12-01T00:00:00.000Z", - UpdatedAt: "2024-12-01T00:00:00.000Z", - }, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.scenario, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(listRolesTestHandler)) - defer server.Close() - - client := test.client - client.Endpoint = server.URL - client.HTTPClient = server.Client() - - response, err := client.ListRoles(context.Background(), test.options) - if test.err { - require.Error(t, err) - return - } - require.NoError(t, err) - require.Equal(t, test.expected, response) - }) - } -} - -func listRolesTestHandler(w http.ResponseWriter, r *http.Request) { - auth := r.Header.Get("Authorization") - if auth != "Bearer test" { - http.Error(w, "bad auth", http.StatusUnauthorized) - return - } - - body, err := json.Marshal(struct { - ListRolesResponse - }{ListRolesResponse{ - Data: []Role{ - { - ID: "role_01EHWNCE74X7JSDV0X3SZ3KJNY", - Name: "Member", - Slug: "member", - Description: "The default role for all users.", - Type: Environment, - CreatedAt: "2024-12-01T00:00:00.000Z", - UpdatedAt: "2024-12-01T00:00:00.000Z", - }, - }, - }}) - - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - w.Write(body) -} diff --git a/pkg/roles/roles.go b/pkg/roles/roles.go deleted file mode 100644 index 0312bfd9..00000000 --- a/pkg/roles/roles.go +++ /dev/null @@ -1,26 +0,0 @@ -// Package `roles` provides a client wrapping the WorkOS Roles API. -package roles - -import ( - "context" -) - -// DefaultClient is the client used by SetAPIKey and Roles functions. -var ( - DefaultClient = &Client{ - Endpoint: "https://api.workos.com", - } -) - -// SetAPIKey sets the WorkOS API key for Roles API requests. -func SetAPIKey(apiKey string) { - DefaultClient.APIKey = apiKey -} - -// ListRoles lists all Roles in an environment. -func ListRoles( - ctx context.Context, - opts ListRolesOpts, -) (ListRolesResponse, error) { - return DefaultClient.ListRoles(ctx, opts) -} diff --git a/pkg/roles/roles_test.go b/pkg/roles/roles_test.go deleted file mode 100644 index 603ada87..00000000 --- a/pkg/roles/roles_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package roles - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestRolesListRoles(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(listRolesTestHandler)) - defer server.Close() - - DefaultClient = &Client{ - HTTPClient: server.Client(), - Endpoint: server.URL, - } - SetAPIKey("test") - - expectedResponse := ListRolesResponse{ - Data: []Role{ - { - ID: "role_01EHWNCE74X7JSDV0X3SZ3KJNY", - Name: "Member", - Slug: "member", - Description: "The default role for all users.", - Type: Environment, - CreatedAt: "2024-12-01T00:00:00.000Z", - UpdatedAt: "2024-12-01T00:00:00.000Z", - }, - }, - } - - response, err := ListRoles(context.Background(), ListRolesOpts{}) - - require.NoError(t, err) - require.Equal(t, expectedResponse, response) -}