Skip to content

Commit b6504fc

Browse files
authored
wrap errors when errors with 200 status code (#67)
* add grpc code mapping when 200 code and "ok": false in the body * refactor request some more * include slack-go library errors * wrap slack-go errors * remove old scim functions * remove repeated code * add better error detection * make more readable
1 parent fd722f5 commit b6504fc

File tree

7 files changed

+151
-152
lines changed

7 files changed

+151
-152
lines changed

pkg/connector/client/helpers.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@ package client
22

33
import (
44
"context"
5+
"encoding/json"
6+
"fmt"
57
"net/http"
8+
"strings"
69

10+
"github.com/conductorone/baton-sdk/pkg/uhttp"
711
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
812
"go.uber.org/zap"
13+
"google.golang.org/grpc/codes"
914
)
1015

1116
func logBody(ctx context.Context, response *http.Response) {
@@ -29,3 +34,103 @@ func logBody(ctx context.Context, response *http.Response) {
2934
}
3035
l.Info("response body", zap.String("body", string(body)))
3136
}
37+
38+
// Slack API may return errors in the response body even when the HTTP status code is 200.
39+
//
40+
// extracts a Slack error from a Go error and wraps it with the appropriate gRPC code.
41+
// This is useful when working with the slack-go library which returns plain Go errors.
42+
func WrapError(err error, contextMsg string) error {
43+
if err == nil {
44+
return nil
45+
}
46+
47+
grpcCode := MapSlackErrorToGRPCCode(err.Error())
48+
return uhttp.WrapErrors(grpcCode, contextMsg, err)
49+
}
50+
51+
func containsAny(s string, substrs ...string) bool {
52+
for _, substr := range substrs {
53+
if strings.Contains(s, substr) {
54+
return true
55+
}
56+
}
57+
return false
58+
}
59+
60+
// maps Slack error strings to gRPC codes.
61+
func MapSlackErrorToGRPCCode(slackError string) codes.Code {
62+
err := strings.ToLower(slackError)
63+
64+
if containsAny(err, "invalid_auth", "token_revoked", "token_expired", "not_authed", "account_inactive") {
65+
return codes.Unauthenticated
66+
}
67+
68+
if containsAny(err, "missing_scope", "access_denied", "not_allowed_token_type",
69+
"team_access_not_granted", "no_permission", "ekm_access_denied") {
70+
return codes.PermissionDenied
71+
}
72+
73+
if containsAny(err, "ratelimited") {
74+
return codes.Unavailable
75+
}
76+
77+
if containsAny(err, "user_not_found") {
78+
return codes.NotFound
79+
}
80+
81+
if containsAny(err, "user_already_team_member") {
82+
return codes.AlreadyExists
83+
}
84+
85+
if containsAny(err, "invalid_arguments", "missing_argument", "invalid_arg_name",
86+
"invalid_array_arg", "invalid_charset", "invalid_form_data", "invalid_post_type",
87+
"missing_post_type", "limit_required") {
88+
return codes.InvalidArgument
89+
}
90+
91+
if containsAny(err, "user_already_deleted", "two_factor_setup_required") {
92+
return codes.FailedPrecondition
93+
}
94+
95+
if containsAny(err, "internal_error", "service_unavailable", "request_timeout") {
96+
return codes.Unavailable
97+
}
98+
99+
if containsAny(err, "fatal_error") {
100+
return codes.Internal
101+
}
102+
103+
if containsAny(err, "method_deprecated", "deprecated_endpoint") {
104+
return codes.Unimplemented
105+
}
106+
107+
return codes.Unknown
108+
}
109+
110+
// Slack API may return errors in the response body even when the HTTP status code is 200.
111+
//
112+
// examples:
113+
//
114+
// {"ok":false,"error":"invalid_auth"}
115+
// {"ok":false,"error": "user_not_found"}
116+
func ErrorWithGrpcCodeFromBytes(bodyBytes []byte) error {
117+
var baseCheck struct {
118+
Ok bool `json:"ok"`
119+
Error string `json:"error"`
120+
}
121+
122+
if err := json.Unmarshal(bodyBytes, &baseCheck); err != nil {
123+
return fmt.Errorf("error parsing Slack API response: %w", err)
124+
}
125+
126+
if !baseCheck.Ok && baseCheck.Error != "" {
127+
grpcCode := MapSlackErrorToGRPCCode(baseCheck.Error)
128+
return uhttp.WrapErrors(
129+
grpcCode,
130+
fmt.Sprintf("Slack API error: %s", baseCheck.Error),
131+
fmt.Errorf("slack error: %s", baseCheck.Error),
132+
)
133+
}
134+
135+
return nil
136+
}

pkg/connector/client/request.go

Lines changed: 6 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -83,43 +83,6 @@ func (c *Client) post(
8383
)
8484
}
8585

86-
func (c *Client) getScim(
87-
ctx context.Context,
88-
path string,
89-
target interface{},
90-
queryParameters map[string]interface{},
91-
) (
92-
*v2.RateLimitDescription,
93-
error,
94-
) {
95-
return c.doRequest(
96-
ctx,
97-
http.MethodGet,
98-
c.getUrl(path, queryParameters, true),
99-
&target,
100-
WithBearerToken(c.token),
101-
)
102-
}
103-
104-
func (c *Client) patchScim(
105-
ctx context.Context,
106-
path string,
107-
target interface{},
108-
payload map[string]any,
109-
) (
110-
*v2.RateLimitDescription,
111-
error,
112-
) {
113-
return c.doRequest(
114-
ctx,
115-
http.MethodPatch,
116-
c.getUrl(path, nil, true),
117-
&target,
118-
WithBearerToken(c.token),
119-
uhttp.WithJSONBody(payload),
120-
)
121-
}
122-
12386
func (c *Client) doRequest(
12487
ctx context.Context,
12588
method string,
@@ -168,96 +131,18 @@ func (c *Client) doRequest(
168131
return &ratelimitData, fmt.Errorf("reading response body: %w", err)
169132
}
170133

171-
if response.StatusCode != http.StatusNoContent && len(bodyBytes) > 0 {
172-
if err := json.Unmarshal(bodyBytes, &target); err != nil {
173-
logBody(ctx, response)
174-
return nil, fmt.Errorf("unmarshaling response: %w", err)
175-
}
176-
}
177-
178-
return &ratelimitData, nil
179-
}
180-
181-
func (c *Client) doScimRequest(
182-
ctx context.Context,
183-
method string,
184-
path string,
185-
target interface{},
186-
payload interface{},
187-
) (
188-
*v2.RateLimitDescription,
189-
error,
190-
) {
191-
options := []uhttp.RequestOption{
192-
WithBearerToken(c.token),
193-
}
194-
195-
if payload != nil {
196-
options = append(options, uhttp.WithJSONBody(payload))
197-
}
198-
199-
return c.doRequest(
200-
ctx,
201-
method,
202-
c.getUrl(path, nil, true),
203-
target,
204-
options...,
205-
)
206-
}
207-
208-
func (c *Client) deleteScim(
209-
ctx context.Context,
210-
path string,
211-
) (
212-
*v2.RateLimitDescription,
213-
error,
214-
) {
215-
logger := ctxzap.Extract(ctx)
216-
logger.Debug(
217-
"making request",
218-
zap.String("method", http.MethodDelete),
219-
zap.String("url", c.getUrl(path, nil, true).String()),
220-
)
221-
222-
request, err := c.wrapper.NewRequest(
223-
ctx,
224-
http.MethodDelete,
225-
c.getUrl(path, nil, true),
226-
WithBearerToken(c.token),
227-
uhttp.WithAcceptJSONHeader(),
228-
)
229-
if err != nil {
230-
return nil, fmt.Errorf("creating SCIM delete request: %w", err)
231-
}
232-
233-
var ratelimitData v2.RateLimitDescription
234-
response, err := c.wrapper.Do(
235-
request,
236-
uhttp.WithRatelimitData(&ratelimitData),
237-
)
238-
if err != nil {
239-
logBody(ctx, response)
240-
return &ratelimitData, err
241-
}
242-
defer response.Body.Close()
243-
244-
if response.StatusCode == http.StatusNoContent {
134+
if response.StatusCode == http.StatusNoContent || len(bodyBytes) == 0 {
245135
return &ratelimitData, nil
246136
}
247137

248-
bodyBytes, err := io.ReadAll(response.Body)
249-
if err != nil {
138+
if err := json.Unmarshal(bodyBytes, &target); err != nil {
250139
logBody(ctx, response)
251-
return &ratelimitData, fmt.Errorf("reading SCIM error response body: %w", err)
140+
return nil, fmt.Errorf("unmarshaling response: %w", err)
252141
}
253142

254-
if len(bodyBytes) > 0 {
255-
var errorResponse map[string]interface{}
256-
if err := json.Unmarshal(bodyBytes, &errorResponse); err != nil {
257-
return &ratelimitData, fmt.Errorf("parsing SCIM error response: %w", err)
258-
}
259-
if detail, ok := errorResponse["detail"].(string); ok {
260-
return &ratelimitData, fmt.Errorf("SCIM API error: %s", detail)
143+
if response.StatusCode == http.StatusOK {
144+
if err := ErrorWithGrpcCodeFromBytes(bodyBytes); err != nil {
145+
return &ratelimitData, err
261146
}
262147
}
263148

pkg/connector/client/slack.go

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -258,14 +258,15 @@ func (c *Client) ListIDPGroups(
258258
) {
259259
var response SCIMResponse[GroupResource]
260260
urlPathIDPGroups := fmt.Sprintf(UrlPathIDPGroups, c.scimVersion)
261-
ratelimitData, err := c.getScim(
261+
ratelimitData, err := c.doRequest(
262262
ctx,
263-
urlPathIDPGroups,
264-
&response,
265-
map[string]interface{}{
263+
http.MethodGet,
264+
c.getUrl(urlPathIDPGroups, map[string]interface{}{
266265
"startIndex": startIndex,
267266
"count": count,
268-
},
267+
}, true),
268+
&response,
269+
WithBearerToken(c.token),
269270
)
270271
if err != nil {
271272
return nil, ratelimitData, fmt.Errorf("error fetching IDP groups: %w", err)
@@ -286,14 +287,15 @@ func (c *Client) ListIDPUsers(
286287
) {
287288
var response SCIMResponse[UserResource]
288289
urlPathIDPUsers := fmt.Sprintf(UrlPathIDPUsers, c.scimVersion)
289-
ratelimitData, err := c.getScim(
290+
ratelimitData, err := c.doRequest(
290291
ctx,
291-
urlPathIDPUsers,
292-
&response,
293-
map[string]interface{}{
292+
http.MethodGet,
293+
c.getUrl(urlPathIDPUsers, map[string]interface{}{
294294
"startIndex": startIndex,
295295
"count": count,
296-
},
296+
}, true),
297+
&response,
298+
WithBearerToken(c.token),
297299
)
298300
if err != nil {
299301
return nil, ratelimitData, fmt.Errorf("error fetching IDP users: %w", err)
@@ -312,11 +314,12 @@ func (c *Client) GetIDPGroup(
312314
error,
313315
) {
314316
var response GroupResource
315-
ratelimitData, err := c.getScim(
317+
ratelimitData, err := c.doRequest(
316318
ctx,
317-
fmt.Sprintf(UrlPathIDPGroup, c.scimVersion, groupID),
319+
http.MethodGet,
320+
c.getUrl(fmt.Sprintf(UrlPathIDPGroup, c.scimVersion, groupID), nil, true),
318321
&response,
319-
nil,
322+
WithBearerToken(c.token),
320323
)
321324
if err != nil {
322325
return nil, ratelimitData, fmt.Errorf("error fetching IDP group: %w", err)
@@ -429,12 +432,13 @@ func (c *Client) patchGroup(
429432
error,
430433
) {
431434
var response *GroupResource
432-
ratelimitData, err := c.doScimRequest(
435+
ratelimitData, err := c.doRequest(
433436
ctx,
434437
http.MethodPatch,
435-
fmt.Sprintf(UrlPathIDPGroup, c.scimVersion, groupID),
438+
c.getUrl(fmt.Sprintf(UrlPathIDPGroup, c.scimVersion, groupID), nil, true),
436439
&response,
437-
requestBody,
440+
WithBearerToken(c.token),
441+
uhttp.WithJSONBody(requestBody),
438442
)
439443
if err != nil {
440444
return ratelimitData, fmt.Errorf("error patching IDP group: %w", err)
@@ -458,9 +462,13 @@ func (c *Client) DisableUser(
458462
*v2.RateLimitDescription,
459463
error,
460464
) {
461-
ratelimitData, err := c.deleteScim(
465+
var response any
466+
ratelimitData, err := c.doRequest(
462467
ctx,
463-
fmt.Sprintf(UrlPathIDPUser, c.scimVersion, userID),
468+
http.MethodDelete,
469+
c.getUrl(fmt.Sprintf(UrlPathIDPUser, c.scimVersion, userID), nil, true),
470+
&response,
471+
WithBearerToken(c.token),
464472
)
465473
if err != nil {
466474
return ratelimitData, fmt.Errorf("error disabling user: %w", err)
@@ -488,12 +496,14 @@ func (c *Client) EnableUser(
488496
},
489497
}
490498

491-
var response *UserResource
492-
ratelimitData, err := c.patchScim(
499+
var response any
500+
ratelimitData, err := c.doRequest(
493501
ctx,
494-
fmt.Sprintf(UrlPathIDPUser, c.scimVersion, userID),
502+
http.MethodPatch,
503+
c.getUrl(fmt.Sprintf(UrlPathIDPUser, c.scimVersion, userID), nil, true),
495504
&response,
496-
requestBody,
505+
WithBearerToken(c.token),
506+
uhttp.WithJSONBody(requestBody),
497507
)
498508
if err != nil {
499509
return ratelimitData, fmt.Errorf("error enabling user: %w", err)

pkg/connector/connector.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@ func (c *Slack) Metadata(ctx context.Context) (*v2.ConnectorMetadata, error) {
7272
func (s *Slack) Validate(ctx context.Context) (annotations.Annotations, error) {
7373
res, err := s.client.AuthTestContext(ctx)
7474
if err != nil {
75-
return nil, fmt.Errorf("authenticating: %w", err)
75+
return nil, client.WrapError(err, "authenticating")
7676
}
7777

7878
user, err := s.client.GetUserInfoContext(ctx, res.UserID)
7979
if err != nil {
80-
return nil, fmt.Errorf("retrieving authenticated user: %w", err)
80+
return nil, client.WrapError(err, "retrieving authenticated user")
8181
}
8282

8383
isValidUser := user.IsAdmin || user.IsOwner || user.IsPrimaryOwner || user.IsBot

0 commit comments

Comments
 (0)