From 1420d0b97924c8ed2bb1150a128f3b26765ff60a Mon Sep 17 00:00:00 2001 From: mattgd Date: Tue, 17 Dec 2024 08:50:47 -0500 Subject: [PATCH] Add environment roles API support. --- 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 +- 7 files changed, 311 insertions(+), 3 deletions(-) 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/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{