From ea23bde59b8e59ad7a436c6ed5e1956424568784 Mon Sep 17 00:00:00 2001 From: Mary Zhong Date: Sun, 5 Nov 2023 19:33:17 -0500 Subject: [PATCH] feat: Add organization roles endpoints --- clerk/clerk.go | 92 ++++++------ clerk/organization_roles.go | 123 ++++++++++++++++ clerk/organization_roles_test.go | 236 +++++++++++++++++++++++++++++++ 3 files changed, 409 insertions(+), 42 deletions(-) create mode 100644 clerk/organization_roles.go create mode 100644 clerk/organization_roles_test.go diff --git a/clerk/clerk.go b/clerk/clerk.go index b8646579..7fea3af0 100644 --- a/clerk/clerk.go +++ b/clerk/clerk.go @@ -19,27 +19,28 @@ const version = "1.48.4" const ( ProdUrl = "https://api.clerk.dev/v1/" - ActorTokensUrl = "actor_tokens" - AllowlistsUrl = "allowlist_identifiers" - BlocklistsUrl = "blocklist_identifiers" - ClientsUrl = "clients" - ClientsVerifyUrl = ClientsUrl + "/verify" - DomainsURL = "domains" - EmailAddressesURL = "email_addresses" - EmailsUrl = "emails" - InvitationsURL = "invitations" - OrganizationsUrl = "organizations" - PhoneNumbersURL = "phone_numbers" - ProxyChecksURL = "proxy_checks" - RedirectURLsUrl = "redirect_urls" - SAMLConnectionsUrl = "saml_connections" - SessionsUrl = "sessions" - SMSUrl = "sms_messages" - TemplatesUrl = "templates" - UsersUrl = "users" - UsersCountUrl = UsersUrl + "/count" - WebhooksUrl = "webhooks" - JWTTemplatesUrl = "jwt_templates" + ActorTokensUrl = "actor_tokens" + AllowlistsUrl = "allowlist_identifiers" + BlocklistsUrl = "blocklist_identifiers" + ClientsUrl = "clients" + ClientsVerifyUrl = ClientsUrl + "/verify" + DomainsURL = "domains" + EmailAddressesURL = "email_addresses" + EmailsUrl = "emails" + InvitationsURL = "invitations" + OrganizationsUrl = "organizations" + OrganizationRolesUrl = "organization_roles" + PhoneNumbersURL = "phone_numbers" + ProxyChecksURL = "proxy_checks" + RedirectURLsUrl = "redirect_urls" + SAMLConnectionsUrl = "saml_connections" + SessionsUrl = "sessions" + SMSUrl = "sms_messages" + TemplatesUrl = "templates" + UsersUrl = "users" + UsersCountUrl = UsersUrl + "/count" + WebhooksUrl = "webhooks" + JWTTemplatesUrl = "jwt_templates" ) var defaultHTTPClient = &http.Client{Timeout: time.Second * 5} @@ -62,6 +63,7 @@ type Client interface { JWKS() *JWKSService JWTTemplates() *JWTTemplatesService Organizations() *OrganizationsService + OrganizationRoles() *OrganizationRolesService PhoneNumbers() *PhoneNumbersService ProxyChecks() *ProxyChecksService RedirectURLs() *RedirectURLsService @@ -87,27 +89,28 @@ type client struct { jwksCache *jwksCache token string - allowlists *AllowlistsService - blocklists *BlocklistsService - clients *ClientsService - domains *DomainsService - emailAddresses *EmailAddressesService - emails *EmailService - actorTokens *ActorTokenService - instances *InstanceService - jwks *JWKSService - jwtTemplates *JWTTemplatesService - organizations *OrganizationsService - phoneNumbers *PhoneNumbersService - proxyChecks *ProxyChecksService - redirectURLs *RedirectURLsService - samlConnections *SAMLConnectionsService - sessions *SessionsService - sms *SMSService - templates *TemplatesService - users *UsersService - webhooks *WebhooksService - verification *VerificationService + allowlists *AllowlistsService + blocklists *BlocklistsService + clients *ClientsService + domains *DomainsService + emailAddresses *EmailAddressesService + emails *EmailService + actorTokens *ActorTokenService + instances *InstanceService + jwks *JWKSService + jwtTemplates *JWTTemplatesService + organizations *OrganizationsService + organizationRoles *OrganizationRolesService + phoneNumbers *PhoneNumbersService + proxyChecks *ProxyChecksService + redirectURLs *RedirectURLsService + samlConnections *SAMLConnectionsService + sessions *SessionsService + sms *SMSService + templates *TemplatesService + users *UsersService + webhooks *WebhooksService + verification *VerificationService } // NewClient creates a new Clerk client. @@ -147,6 +150,7 @@ func NewClient(token string, options ...ClerkOption) (Client, error) { client.jwks = (*JWKSService)(commonService) client.jwtTemplates = (*JWTTemplatesService)(commonService) client.organizations = (*OrganizationsService)(commonService) + client.organizationRoles = (*OrganizationRolesService)(commonService) client.phoneNumbers = (*PhoneNumbersService)(commonService) client.proxyChecks = (*ProxyChecksService)(commonService) client.redirectURLs = (*RedirectURLsService)(commonService) @@ -309,6 +313,10 @@ func (c *client) Organizations() *OrganizationsService { return c.organizations } +func (c *client) OrganizationRoles() *OrganizationRolesService { + return c.organizationRoles +} + func (c *client) PhoneNumbers() *PhoneNumbersService { return c.phoneNumbers } diff --git a/clerk/organization_roles.go b/clerk/organization_roles.go new file mode 100644 index 00000000..32b844d8 --- /dev/null +++ b/clerk/organization_roles.go @@ -0,0 +1,123 @@ +package clerk + +import ( + "fmt" + "net/http" + "strconv" +) + +type OrganizationRolesService service + +type OrganizationRole struct { + Object string `json:"object"` + ID string `json:"id"` + Name string `json:"name"` + Key string `json:"key"` + Description string `json:"description"` + Permissions []Permission `json:"permissions"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type OrganizationRolesResponse struct { + Data []OrganizationRole `json:"data"` + TotalCount int64 `json:"total_count"` +} + +// TODO: move this to a separate file once custom permissions endpoints are done +type Permission struct { + Object string `json:"object"` + ID string `json:"id"` + Name string `json:"name"` + Key string `json:"key"` + Description string `json:"description"` + Type string `json:"type"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type CreateOrganizationRoleParams struct { + Name string `json:"name"` + Key string `json:"key"` + Description string `json:"description"` + Permissions []string `json:"permissions,omitempty"` +} + +func (s *OrganizationRolesService) Create(params CreateOrganizationRoleParams) (*OrganizationRole, error) { + req, _ := s.client.NewRequest(http.MethodPost, OrganizationRolesUrl, ¶ms) + + var orgRole OrganizationRole + _, err := s.client.Do(req, &orgRole) + if err != nil { + return nil, err + } + return &orgRole, nil +} + +type ListOrganizationRoleParams struct { + Limit *int `json:"limit,omitempty"` + Offset *int `json:"offset,omitempty"` +} + +func (s *OrganizationRolesService) List(params ListOrganizationRoleParams) (*OrganizationRolesResponse, error) { + req, _ := s.client.NewRequest(http.MethodGet, OrganizationRolesUrl) + + query := req.URL.Query() + if params.Limit != nil { + query.Set("limit", strconv.Itoa(*params.Limit)) + } + if params.Offset != nil { + query.Set("offset", strconv.Itoa(*params.Offset)) + } + req.URL.RawQuery = query.Encode() + + var orgRolesResponse *OrganizationRolesResponse + _, err := s.client.Do(req, &orgRolesResponse) + if err != nil { + return nil, err + } + return orgRolesResponse, nil +} + +func (s *OrganizationRolesService) Read(orgRoleID string) (*OrganizationRole, error) { + req, err := s.client.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", OrganizationRolesUrl, orgRoleID)) + if err != nil { + return nil, err + } + + var orgRole OrganizationRole + _, err = s.client.Do(req, &orgRole) + if err != nil { + return nil, err + } + return &orgRole, nil +} + +type UpdateOrganizationRoleParams struct { + Name *string `json:"name,omitempty"` + Key *string `json:"key,omitempty"` + Description *string `json:"description,omitempty"` + Permissions *[]string `json:"permissions,omitempty"` +} + +func (s *OrganizationRolesService) Update(orgRoleID string, params UpdateOrganizationRoleParams) (*OrganizationRole, error) { + req, _ := s.client.NewRequest(http.MethodPatch, fmt.Sprintf("%s/%s", OrganizationRolesUrl, orgRoleID), ¶ms) + + var orgRole OrganizationRole + _, err := s.client.Do(req, &orgRole) + if err != nil { + return nil, err + } + return &orgRole, nil +} + +func (s *OrganizationRolesService) Delete(orgRoleID string) (*DeleteResponse, error) { + req, _ := s.client.NewRequest(http.MethodDelete, fmt.Sprintf("%s/%s", OrganizationRolesUrl, orgRoleID)) + + var deleteResponse DeleteResponse + _, err := s.client.Do(req, &deleteResponse) + if err != nil { + return nil, err + } + return &deleteResponse, nil +} diff --git a/clerk/organization_roles_test.go b/clerk/organization_roles_test.go new file mode 100644 index 00000000..de588a70 --- /dev/null +++ b/clerk/organization_roles_test.go @@ -0,0 +1,236 @@ +package clerk + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOrganizationRolesService_Create(t *testing.T) { + expectedResponse := dummyOrgRoleJson + + client, mux, _, teardown := setup("token") + defer teardown() + + mux.HandleFunc("/organization_roles", func(w http.ResponseWriter, req *http.Request) { + testHttpMethod(t, req, http.MethodPost) + testHeader(t, req, "Authorization", "Bearer token") + _, _ = fmt.Fprint(w, expectedResponse) + }) + + createParams := CreateOrganizationRoleParams{ + Name: "custom role", + Key: "org:custom_role", + Description: "my org custom role", + Permissions: []string{}, + } + + got, err := client.OrganizationRoles().Create(createParams) + assert.NoError(t, err) + + var want OrganizationRole + err = json.Unmarshal([]byte(expectedResponse), &want) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(got, &want) { + t.Errorf("Response = %v, want %v", got, &want) + } +} + +func TestOrganizationRolesService_Read(t *testing.T) { + client, mux, _, teardown := setup("token") + defer teardown() + + expectedResponse := dummyOrgRoleJson + + mux.HandleFunc(fmt.Sprintf("/organization_roles/%s", dummyOrgRoleID), func(w http.ResponseWriter, req *http.Request) { + testHttpMethod(t, req, "GET") + testHeader(t, req, "Authorization", "Bearer token") + fmt.Fprint(w, expectedResponse) + }) + + got, err := client.OrganizationRoles().Read(dummyOrgRoleID) + if err != nil { + t.Fatal(err) + } + + var want OrganizationRole + err = json.Unmarshal([]byte(expectedResponse), &want) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(got, &want) { + t.Errorf("Response = %v, want %v", got, &want) + } +} + +func TestOrganizationRolesService_Update(t *testing.T) { + client, mux, _, teardown := setup("token") + defer teardown() + var payload UpdateOrganizationRoleParams + _ = json.Unmarshal([]byte(dummyUpdateOrgRoleJson), &payload) + + expectedResponse := dummyOrgRoleJson + mux.HandleFunc(fmt.Sprintf("/organization_roles/%s", dummyOrgRoleID), func(w http.ResponseWriter, req *http.Request) { + testHttpMethod(t, req, "PATCH") + testHeader(t, req, "Authorization", "Bearer token") + fmt.Fprint(w, expectedResponse) + }) + + got, err := client.OrganizationRoles().Update(dummyOrgRoleID, payload) + if err != nil { + t.Fatal(err) + } + + var want OrganizationRole + err = json.Unmarshal([]byte(expectedResponse), &want) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(got, &want) { + t.Errorf("Response = %v, want %v", got, &want) + } +} + +func TestOrganizationRolesService_Update_invalidServer(t *testing.T) { + client, _ := NewClient("token") + var payload UpdateOrganizationRoleParams + _ = json.Unmarshal([]byte(dummyUpdateOrgRoleJson), &payload) + + _, err := client.OrganizationRoles().Update("someOrgRoleId", payload) + if err == nil { + t.Errorf("Expected error to be returned") + } +} + +func TestOrganizationsService_List_happyPath(t *testing.T) { + client, mux, _, teardown := setup("token") + defer teardown() + + expectedResponse := fmt.Sprintf(`{ + "data": [%s], + "total_count": 1 + }`, dummyOrgRoleJson) + + mux.HandleFunc("/organization_roles", func(w http.ResponseWriter, req *http.Request) { + testHttpMethod(t, req, "GET") + testHeader(t, req, "Authorization", "Bearer token") + fmt.Fprint(w, expectedResponse) + }) + + var want *OrganizationRolesResponse + _ = json.Unmarshal([]byte(expectedResponse), &want) + + got, _ := client.OrganizationRoles().List(ListOrganizationRoleParams{}) + if len(got.Data) != len(want.Data) { + t.Errorf("Was expecting %d organization roles to be returned, instead got %d", len(want.Data), len(got.Data)) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("Response = %v, want %v", got, want) + } +} + +func TestOrganizationsService_List_happyPathWithParameters(t *testing.T) { + client, mux, _, teardown := setup("token") + defer teardown() + + expectedResponse := fmt.Sprintf(`{ + "data": [%s], + "total_count": 1 + }`, dummyOrgRoleJson) + + mux.HandleFunc("/organization_roles", func(w http.ResponseWriter, req *http.Request) { + testHttpMethod(t, req, "GET") + testHeader(t, req, "Authorization", "Bearer token") + + actualQuery := req.URL.Query() + expectedQuery := url.Values(map[string][]string{ + "limit": {"5"}, + "offset": {"6"}, + }) + assert.Equal(t, expectedQuery, actualQuery) + fmt.Fprint(w, expectedResponse) + }) + + var want *OrganizationRolesResponse + _ = json.Unmarshal([]byte(expectedResponse), &want) + + limit := 5 + offset := 6 + got, _ := client.OrganizationRoles().List(ListOrganizationRoleParams{ + Limit: &limit, + Offset: &offset, + }) + if len(got.Data) != len(want.Data) { + t.Errorf("Was expecting %d organization roles to be returned, instead got %d", len(want.Data), len(got.Data)) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("Response = %v, want %v", got, want) + } +} + +func TestOrganizationsService_List_invalidServer(t *testing.T) { + client, _ := NewClient("token") + + orgRoles, err := client.OrganizationRoles().List(ListOrganizationRoleParams{}) + if err == nil { + t.Errorf("Expected error to be returned") + } + if orgRoles != nil { + t.Errorf("Was not expecting any organization roles to be returned, instead got %v", orgRoles) + } +} + +func TestOrganizationRolesService_Delete(t *testing.T) { + client, mux, _, teardown := setup("token") + defer teardown() + + mux.HandleFunc( + fmt.Sprintf("/organization_roles/%s", dummyOrgRoleID), + func(w http.ResponseWriter, req *http.Request) { + testHttpMethod(t, req, http.MethodDelete) + testHeader(t, req, "Authorization", "Bearer token") + fmt.Fprint(w, fmt.Sprintf(`{"id":"%s"}`, dummyOrgRoleID)) + }, + ) + + _, err := client.OrganizationRoles().Delete(dummyOrgRoleID) + if err != nil { + t.Fatal(err) + } +} + +const dummyOrgRoleID = "role_1mebQggrD3xO5JfuHk7clQ94ysA" + +const dummyOrgRoleJson = `{ + "object": "organization_role", + "id": "role_1mebQggrD3xO5JfuHk7clQ94ysA", + "name": "custom role", + "key": "org:custom_role", + "description": "my org custom role", + "permissions": [], + "created_at": 1610783813, + "updated_at": 1610783813 +}` + +const dummyUpdateOrgRoleJson = `{ + "object": "organization_role", + "id": "role_1mebQggrD3xO5JfuHk7clQ94ysA", + "name": "custom org 2", + "key": "org:custom_role_2", + "description": "my org custom role", + "permissions": [], + "created_at": 1610783813, + "updated_at": 1610783813 +}`