Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion knock/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"net/http"
"net/url"
"strings"

"github.com/hashicorp/go-cleanhttp"
"github.com/knocklabs/knock-go/knock/internal"
Expand Down Expand Up @@ -44,6 +45,7 @@ type Client struct {
Tenants TenantsService
Users UsersService
Workflows WorkflowsService
Providers ProvidersService
}

// ClientOption provides a variadic option for configuring the client
Expand Down Expand Up @@ -117,6 +119,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
}
Expand Down Expand Up @@ -152,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,
Expand Down
160 changes: 160 additions & 0 deletions knock/providers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package knock

import (
"context"
"fmt"
"github.com/google/go-querystring/query"
"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 ProviderAccessTokenObject struct {
ObjectId string `json:"object_id" url:"object_id"`
Collection string `json:"collection" url:"collection"`
}

type ProviderAuthCheckRequest struct {
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 {
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 {
ProviderName string `json:"-" url:"-"`
ChannelId string `url:"channel_id"`
AccessTokenObject ProviderAccessTokenObject `url:"access_token_object"`
SlackQueryOptions *SlackQueryOptions `url:"query_options,omitempty"`
}

type ProviderListChannelsResponse struct {
SlackChannels []SlackChannel `json:"slack_channels"`
NextCursor string `json:"next_cursor"`
}

type SlackQueryOptions struct {
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 {
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 {
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 {
return fmt.Sprintf("v1/providers/%s/%s", providerName, channelId)
}

func (ps providersService) AuthCheck(ctx context.Context, request *ProviderAuthCheckRequest) (*ProviderAuthCheckResponse, error) {
queryString, err := query.Values(request)
if err != nil {
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, 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) {
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{}
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) {
queryString, err := query.Values(request)
if err != nil {
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.MethodPut, path, nil, 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
}