From f26e846a1b81bdba9e08bbf0a43127d173726514 Mon Sep 17 00:00:00 2001 From: Brad Sickles Date: Mon, 21 Oct 2024 16:59:26 -0400 Subject: [PATCH 1/7] initial setup --- knock/client.go | 2 + knock/providers.go | 106 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 knock/providers.go diff --git a/knock/client.go b/knock/client.go index 9b875fd..491163f 100644 --- a/knock/client.go +++ b/knock/client.go @@ -44,6 +44,7 @@ type Client struct { Tenants TenantsService Users UsersService Workflows WorkflowsService + Providers ProvidersService } // ClientOption provides a variadic option for configuring the client @@ -117,6 +118,7 @@ func NewClient(opts ...ClientOption) (*Client, error) { c.Tenants = &tenantsService{client: c} c.Users = &usersService{client: c} c.Workflows = &workflowsService{client: c} + c.Providers = &providersService{client: c} return c, nil } diff --git a/knock/providers.go b/knock/providers.go new file mode 100644 index 0000000..f3ce7e8 --- /dev/null +++ b/knock/providers.go @@ -0,0 +1,106 @@ +package knock + +import ( + "context" + "fmt" + "github.com/pkg/errors" + "net/http" +) + +// ProvidersService is an interface for communicating with the Knock +// Providers API endpoints. +type ProvidersService interface { + AuthCheck(context.Context, *ProviderAuthCheckRequest) (*ProviderAuthCheckResponse, error) + ListChannels(context.Context, *ProviderListChannelsRequest) (*ProviderListChannelsResponse, error) + RevokeAccess(context.Context, *ProviderRevokeAccessRequest) (bool, error) +} + +type providersService struct { + client *Client +} + +var _ ProvidersService = &providersService{} + +func NewProvidersService(client *Client) *providersService { + return &providersService{ + client: client, + } +} + +// Client structs +type ProviderContext struct { + // ProviderName is included as a path parameter + ProviderName string `json:"-"` + // ChannelId is included as a path parameter + ChannelId string `json:"-"` +} + +type ProviderAccessTokenObject struct { + ObjectId string `json:"object_id"` + Collection string `json:"collection"` +} + +type ProviderAuthCheckRequest struct { + ProviderContext `json:"-"` + AccessTokenObject ProviderAccessTokenObject `json:"access_token_object"` +} + +type ProviderAuthCheckResponse struct { + Ok bool `json:"ok"` + Url string `json:"url"` + Team string `json:"team"` + User string `json:"user"` + TeamId string `json:"team_id"` + UserId string `json:"user_id"` + Error string `json:"error,omitempty"` +} + +type ProviderListChannelsRequest struct { + ProviderContext `json:"-"` + AccessTokenObject ProviderAccessTokenObject `json:"access_token_object"` + // TODO: query_options +} + +type ProviderListChannelsResponse struct { + // TODO: +} + +type ProviderRevokeAccessRequest struct { + ProviderContext `json:"-"` + AccessTokenObject ProviderAccessTokenObject `json:"access_token_object"` +} + +func providersAPIPath(providerName, channelId string) string { + return fmt.Sprintf("v1/providers/%s/%s", providerName, channelId) +} + +func (ps providersService) AuthCheck(ctx context.Context, request *ProviderAuthCheckRequest) (*ProviderAuthCheckResponse, error) { + path := providersAPIPath(request.ProviderName, request.ChannelId) + path = fmt.Sprintf("%s/auth_check", path) + + req, err := ps.client.newRequest(http.MethodGet, path, nil, nil) + if err != nil { + return nil, errors.Wrap(err, "error creating request for provider auth check") + } + + authCheckResponse := &ProviderAuthCheckResponse{} + _, err = ps.client.do(ctx, req, authCheckResponse) + if err != nil { + return nil, errors.Wrap(err, "error making request for provider auth check") + } + + return authCheckResponse, nil +} + +func (ps providersService) ListChannels(ctx context.Context, request *ProviderListChannelsRequest) (*ProviderListChannelsResponse, error) { + path := providersAPIPath(request.ProviderName, request.ChannelId) + path = fmt.Sprintf("%s/channels", path) + + //TODO implement me + panic("implement me") +} + +func (ps providersService) RevokeAccess(ctx context.Context, request *ProviderRevokeAccessRequest) (bool, error) { + //TODO implement me + panic("implement me") +} From eca21f745e46375deac7a4407f27c581d016d47e Mon Sep 17 00:00:00 2001 From: Brad Sickles Date: Tue, 22 Oct 2024 08:20:27 -0400 Subject: [PATCH 2/7] Initial draft --- knock/providers.go | 73 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/knock/providers.go b/knock/providers.go index f3ce7e8..aa5153d 100644 --- a/knock/providers.go +++ b/knock/providers.go @@ -1,7 +1,9 @@ package knock import ( + "bytes" "context" + "encoding/json" "fmt" "github.com/pkg/errors" "net/http" @@ -58,11 +60,28 @@ type ProviderAuthCheckResponse struct { type ProviderListChannelsRequest struct { ProviderContext `json:"-"` AccessTokenObject ProviderAccessTokenObject `json:"access_token_object"` - // TODO: query_options + SlackQueryOptions *SlackQueryOptions `json:"query_options,omitempty"` } type ProviderListChannelsResponse struct { - // TODO: + SlackChannels []SlackChannel `json:"slack_channels"` + NextCursor string `json:"next_cursor"` +} + +type SlackQueryOptions struct { + Cursor string `json:"cursor,omitempty"` + ExcludeArchived bool `json:"exclude_archived,omitempty"` + Limit int `json:"limit,omitempty"` + TeamId string `json:"team_id,omitempty"` + Types string `json:"types,omitempty"` +} + +type SlackChannel struct { + ID string `json:"id"` + Name string `json:"name"` + IsPrivate bool `json:"is_private"` + IsIM bool `json:"is_im"` + ContextTeamId string `json:"context_team_id"` } type ProviderRevokeAccessRequest struct { @@ -78,7 +97,12 @@ func (ps providersService) AuthCheck(ctx context.Context, request *ProviderAuthC path := providersAPIPath(request.ProviderName, request.ChannelId) path = fmt.Sprintf("%s/auth_check", path) - req, err := ps.client.newRequest(http.MethodGet, path, nil, nil) + raw, err := json.Marshal(request) + if err != nil { + return nil, errors.Wrap(err, "error creating body for provider auth check") + } + + req, err := ps.client.newRequest(http.MethodGet, path, bytes.NewBuffer(raw), nil) if err != nil { return nil, errors.Wrap(err, "error creating request for provider auth check") } @@ -96,11 +120,46 @@ func (ps providersService) ListChannels(ctx context.Context, request *ProviderLi path := providersAPIPath(request.ProviderName, request.ChannelId) path = fmt.Sprintf("%s/channels", path) - //TODO implement me - panic("implement me") + raw, err := json.Marshal(request) + if err != nil { + return nil, errors.Wrap(err, "error creating body for provider list channels") + } + + req, err := ps.client.newRequest(http.MethodGet, path, bytes.NewBuffer(raw), nil) + if err != nil { + return nil, errors.Wrap(err, "error creating request for provider list channels") + } + + listChannelsResponse := &ProviderListChannelsResponse{} + _, err = ps.client.do(ctx, req, listChannelsResponse) + if err != nil { + return nil, errors.Wrap(err, "error making request for provider list channels") + } + + return listChannelsResponse, nil } func (ps providersService) RevokeAccess(ctx context.Context, request *ProviderRevokeAccessRequest) (bool, error) { - //TODO implement me - panic("implement me") + path := providersAPIPath(request.ProviderName, request.ChannelId) + path = fmt.Sprintf("%s/revoke_access", path) + + raw, err := json.Marshal(request) + if err != nil { + return false, errors.Wrap(err, "error creating body for provider revoke access") + } + + req, err := ps.client.newRequest(http.MethodGet, path, bytes.NewBuffer(raw), nil) + if err != nil { + return false, errors.Wrap(err, "error creating request for provider revoke access") + } + + revokeAccessResponse := struct { + Ok bool `json:"ok"` + }{} + _, err = ps.client.do(ctx, req, revokeAccessResponse) + if err != nil { + return false, errors.Wrap(err, "error making request for provider revoke access") + } + + return revokeAccessResponse.Ok, nil } From e0f3a4c5b7ea8c5b842a6fbddae69769658660ad Mon Sep 17 00:00:00 2001 From: Brad Sickles Date: Tue, 22 Oct 2024 21:52:49 -0400 Subject: [PATCH 3/7] Fixed provider API calls --- knock/providers.go | 86 +++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/knock/providers.go b/knock/providers.go index aa5153d..166b7bc 100644 --- a/knock/providers.go +++ b/knock/providers.go @@ -1,12 +1,13 @@ package knock import ( - "bytes" "context" - "encoding/json" "fmt" + "github.com/google/go-querystring/query" "github.com/pkg/errors" + "log" "net/http" + "net/http/httputil" ) // ProvidersService is an interface for communicating with the Knock @@ -30,37 +31,38 @@ func NewProvidersService(client *Client) *providersService { } // Client structs -type ProviderContext struct { - // ProviderName is included as a path parameter - ProviderName string `json:"-"` - // ChannelId is included as a path parameter - ChannelId string `json:"-"` -} - type ProviderAccessTokenObject struct { - ObjectId string `json:"object_id"` - Collection string `json:"collection"` + ObjectId string `json:"object_id" url:"object_id"` + Collection string `json:"collection" url:"collection"` } type ProviderAuthCheckRequest struct { - ProviderContext `json:"-"` - AccessTokenObject ProviderAccessTokenObject `json:"access_token_object"` + ProviderName string `json:"-" url:"-"` + ChannelId string `json:"channel_id" url:"channel_id"` + AccessTokenObject ProviderAccessTokenObject `json:"access_token_object" url:"access_token_object"` } type ProviderAuthCheckResponse struct { - Ok bool `json:"ok"` - Url string `json:"url"` - Team string `json:"team"` - User string `json:"user"` - TeamId string `json:"team_id"` - UserId string `json:"user_id"` - Error string `json:"error,omitempty"` + Connection ProviderAuthCheckResponseConnection `json:"connection"` + Error string `json:"error,omitempty"` +} + +type ProviderAuthCheckResponseConnection struct { + BotId string `json:"bot_id"` + IsEnterpriseInstall bool `json:"is_enterprise_install"` + Ok bool `json:"ok"` + Team string `json:"team"` + TeamId string `json:"team_id"` + Url string `json:"url"` + User string `json:"user"` + UserId string `json:"user_id"` } type ProviderListChannelsRequest struct { - ProviderContext `json:"-"` - AccessTokenObject ProviderAccessTokenObject `json:"access_token_object"` - SlackQueryOptions *SlackQueryOptions `json:"query_options,omitempty"` + ProviderName string `json:"-" url:"-"` + ChannelId string `json:"channel_id" url:"channel_id"` + AccessTokenObject ProviderAccessTokenObject `json:"access_token_object" url:"access_token_object"` + SlackQueryOptions *SlackQueryOptions `json:"query_options,omitempty" url:"query_options,omitempty"` } type ProviderListChannelsResponse struct { @@ -85,8 +87,9 @@ type SlackChannel struct { } type ProviderRevokeAccessRequest struct { - ProviderContext `json:"-"` - AccessTokenObject ProviderAccessTokenObject `json:"access_token_object"` + ProviderName string `json:"-" url:"-"` + AccessTokenObject ProviderAccessTokenObject `json:"access_token_object" url:"access_token_object"` + ChannelId string `json:"channel_id" url:"channel_id"` } func providersAPIPath(providerName, channelId string) string { @@ -94,15 +97,13 @@ func providersAPIPath(providerName, channelId string) string { } func (ps providersService) AuthCheck(ctx context.Context, request *ProviderAuthCheckRequest) (*ProviderAuthCheckResponse, error) { - path := providersAPIPath(request.ProviderName, request.ChannelId) - path = fmt.Sprintf("%s/auth_check", path) - - raw, err := json.Marshal(request) + queryString, err := query.Values(request) if err != nil { - return nil, errors.Wrap(err, "error creating body for provider auth check") + return nil, errors.Wrap(err, "error parsing query params to provider auth check") } + path := fmt.Sprintf("%s/auth_check?%s", providersAPIPath(request.ProviderName, request.ChannelId), queryString.Encode()) - req, err := ps.client.newRequest(http.MethodGet, path, bytes.NewBuffer(raw), nil) + req, err := ps.client.newRequest(http.MethodGet, path, nil, nil) if err != nil { return nil, errors.Wrap(err, "error creating request for provider auth check") } @@ -117,15 +118,13 @@ func (ps providersService) AuthCheck(ctx context.Context, request *ProviderAuthC } func (ps providersService) ListChannels(ctx context.Context, request *ProviderListChannelsRequest) (*ProviderListChannelsResponse, error) { - path := providersAPIPath(request.ProviderName, request.ChannelId) - path = fmt.Sprintf("%s/channels", path) - - raw, err := json.Marshal(request) + queryString, err := query.Values(request) if err != nil { - return nil, errors.Wrap(err, "error creating body for provider list channels") + return nil, errors.Wrap(err, "error parsing query params to provider auth check") } + path := fmt.Sprintf("%s/channels?%s", providersAPIPath(request.ProviderName, request.ChannelId), queryString.Encode()) - req, err := ps.client.newRequest(http.MethodGet, path, bytes.NewBuffer(raw), nil) + req, err := ps.client.newRequest(http.MethodGet, path, nil, nil) if err != nil { return nil, errors.Wrap(err, "error creating request for provider list channels") } @@ -140,19 +139,20 @@ func (ps providersService) ListChannels(ctx context.Context, request *ProviderLi } func (ps providersService) RevokeAccess(ctx context.Context, request *ProviderRevokeAccessRequest) (bool, error) { - path := providersAPIPath(request.ProviderName, request.ChannelId) - path = fmt.Sprintf("%s/revoke_access", path) - - raw, err := json.Marshal(request) + queryString, err := query.Values(request) if err != nil { - return false, errors.Wrap(err, "error creating body for provider revoke access") + return false, errors.Wrap(err, "error parsing query params to revoke access") } + path := fmt.Sprintf("%s/revoke_access?%s", providersAPIPath(request.ProviderName, request.ChannelId), queryString.Encode()) - req, err := ps.client.newRequest(http.MethodGet, path, bytes.NewBuffer(raw), nil) + req, err := ps.client.newRequest(http.MethodPut, path, nil, nil) if err != nil { return false, errors.Wrap(err, "error creating request for provider revoke access") } + raw, _ := httputil.DumpRequest(req, true) + log.Println(string(raw)) + revokeAccessResponse := struct { Ok bool `json:"ok"` }{} From 3b53f45f60be1dc9c6e262ba1de547ddf11d28eb Mon Sep 17 00:00:00 2001 From: Brad Sickles Date: Tue, 22 Oct 2024 22:19:10 -0400 Subject: [PATCH 4/7] page through channels --- knock/providers.go | 57 ++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/knock/providers.go b/knock/providers.go index 166b7bc..f485523 100644 --- a/knock/providers.go +++ b/knock/providers.go @@ -60,9 +60,9 @@ type ProviderAuthCheckResponseConnection struct { type ProviderListChannelsRequest struct { ProviderName string `json:"-" url:"-"` - ChannelId string `json:"channel_id" url:"channel_id"` - AccessTokenObject ProviderAccessTokenObject `json:"access_token_object" url:"access_token_object"` - SlackQueryOptions *SlackQueryOptions `json:"query_options,omitempty" url:"query_options,omitempty"` + ChannelId string `url:"channel_id"` + AccessTokenObject ProviderAccessTokenObject `url:"access_token_object"` + SlackQueryOptions *SlackQueryOptions `url:"query_options,omitempty"` } type ProviderListChannelsResponse struct { @@ -71,11 +71,11 @@ type ProviderListChannelsResponse struct { } type SlackQueryOptions struct { - Cursor string `json:"cursor,omitempty"` - ExcludeArchived bool `json:"exclude_archived,omitempty"` - Limit int `json:"limit,omitempty"` - TeamId string `json:"team_id,omitempty"` - Types string `json:"types,omitempty"` + Cursor string `url:"cursor,omitempty"` + ExcludeArchived bool `url:"exclude_archived,omitempty"` + Limit int `url:"limit,omitempty"` + TeamId string `url:"team_id,omitempty"` + Types string `url:"types,omitempty"` } type SlackChannel struct { @@ -118,24 +118,31 @@ func (ps providersService) AuthCheck(ctx context.Context, request *ProviderAuthC } func (ps providersService) ListChannels(ctx context.Context, request *ProviderListChannelsRequest) (*ProviderListChannelsResponse, error) { - queryString, err := query.Values(request) - if err != nil { - return nil, errors.Wrap(err, "error parsing query params to provider auth check") + baseUrl := fmt.Sprintf("%s/channels", providersAPIPath(request.ProviderName, request.ChannelId)) + + result := &ProviderListChannelsResponse{} + for { + queryString, err := query.Values(request) + if err != nil { + return nil, errors.Wrap(err, "error parsing query params to provider list channels") + } + path := fmt.Sprintf("%s?%s", baseUrl, queryString.Encode()) + + req, err := ps.client.newRequest(http.MethodGet, path, nil, nil) + if err != nil { + return nil, errors.Wrap(err, "error creating request for provider list channels") + } + + pageResponse := &ProviderListChannelsResponse{} + if _, err = ps.client.do(ctx, req, pageResponse); err != nil { + return nil, errors.Wrap(err, "error making request for provider list channels") + } + + result.SlackChannels = append(result.SlackChannels, pageResponse.SlackChannels...) + if pageResponse.NextCursor == "" { + return result, nil + } } - path := fmt.Sprintf("%s/channels?%s", providersAPIPath(request.ProviderName, request.ChannelId), queryString.Encode()) - - req, err := ps.client.newRequest(http.MethodGet, path, nil, nil) - if err != nil { - return nil, errors.Wrap(err, "error creating request for provider list channels") - } - - listChannelsResponse := &ProviderListChannelsResponse{} - _, err = ps.client.do(ctx, req, listChannelsResponse) - if err != nil { - return nil, errors.Wrap(err, "error making request for provider list channels") - } - - return listChannelsResponse, nil } func (ps providersService) RevokeAccess(ctx context.Context, request *ProviderRevokeAccessRequest) (bool, error) { From cd72104820d9c6c1efa6071d1393d51f5353c3e8 Mon Sep 17 00:00:00 2001 From: Brad Sickles Date: Mon, 4 Nov 2024 14:46:58 -0500 Subject: [PATCH 5/7] Better matching of Content-Type when handling responses --- knock/client.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/knock/client.go b/knock/client.go index 491163f..f7686ce 100644 --- a/knock/client.go +++ b/knock/client.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "net/url" + "strings" "github.com/hashicorp/go-cleanhttp" "github.com/knocklabs/knock-go/knock/internal" @@ -154,7 +155,9 @@ func (c *Client) handleResponse(ctx context.Context, res *http.Response, v inter } // in some scenarios we don't fully control the response, e.g. an ELB 502. - if res.Header.Get("Content-Type") != jsonMediaType { + // Content-Type could be `application/json` or `application/json; charset=utf-8` + // If we match `application/json` at the beginning, that's good enough to be considered good json + if !strings.HasPrefix(res.Header.Get("Content-Type"), jsonMediaType) { return nil, &Error{ msg: "malformed non-json error response body received with status code: " + http.StatusText(res.StatusCode), Code: ErrResponseMalformed, From 83decac47e8ea8e4cbeebc2eea6d989e7113d11e Mon Sep 17 00:00:00 2001 From: Brad Sickles Date: Mon, 4 Nov 2024 14:58:48 -0500 Subject: [PATCH 6/7] drop extra logging --- knock/providers.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/knock/providers.go b/knock/providers.go index f485523..7db8516 100644 --- a/knock/providers.go +++ b/knock/providers.go @@ -5,9 +5,7 @@ import ( "fmt" "github.com/google/go-querystring/query" "github.com/pkg/errors" - "log" "net/http" - "net/http/httputil" ) // ProvidersService is an interface for communicating with the Knock @@ -157,9 +155,6 @@ func (ps providersService) RevokeAccess(ctx context.Context, request *ProviderRe return false, errors.Wrap(err, "error creating request for provider revoke access") } - raw, _ := httputil.DumpRequest(req, true) - log.Println(string(raw)) - revokeAccessResponse := struct { Ok bool `json:"ok"` }{} From 0ae0040f455ebe22233f733a7760d7cc95d72c94 Mon Sep 17 00:00:00 2001 From: Brad Sickles Date: Fri, 31 Jan 2025 11:13:45 -0500 Subject: [PATCH 7/7] Don't page when listing provider channels --- knock/providers.go | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/knock/providers.go b/knock/providers.go index 7db8516..ba5f938 100644 --- a/knock/providers.go +++ b/knock/providers.go @@ -118,29 +118,22 @@ func (ps providersService) AuthCheck(ctx context.Context, request *ProviderAuthC func (ps providersService) ListChannels(ctx context.Context, request *ProviderListChannelsRequest) (*ProviderListChannelsResponse, error) { baseUrl := fmt.Sprintf("%s/channels", providersAPIPath(request.ProviderName, request.ChannelId)) + queryString, err := query.Values(request) + if err != nil { + return nil, errors.Wrap(err, "error parsing query params to provider list channels") + } + path := fmt.Sprintf("%s?%s", baseUrl, queryString.Encode()) + + req, err := ps.client.newRequest(http.MethodGet, path, nil, nil) + if err != nil { + return nil, errors.Wrap(err, "error creating request for provider list channels") + } + result := &ProviderListChannelsResponse{} - for { - queryString, err := query.Values(request) - if err != nil { - return nil, errors.Wrap(err, "error parsing query params to provider list channels") - } - path := fmt.Sprintf("%s?%s", baseUrl, queryString.Encode()) - - req, err := ps.client.newRequest(http.MethodGet, path, nil, nil) - if err != nil { - return nil, errors.Wrap(err, "error creating request for provider list channels") - } - - pageResponse := &ProviderListChannelsResponse{} - if _, err = ps.client.do(ctx, req, pageResponse); err != nil { - return nil, errors.Wrap(err, "error making request for provider list channels") - } - - result.SlackChannels = append(result.SlackChannels, pageResponse.SlackChannels...) - if pageResponse.NextCursor == "" { - return result, nil - } + if _, err = ps.client.do(ctx, req, result); err != nil { + return nil, errors.Wrap(err, "error making request for provider list channels") } + return result, nil } func (ps providersService) RevokeAccess(ctx context.Context, request *ProviderRevokeAccessRequest) (bool, error) {