mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-19 01:58:44 +08:00
SCIM Client rate limiting error (#67313)
* SCIM Client rate limiting error * update
This commit is contained in:
+52
-14
@@ -16,14 +16,46 @@ package scim
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
||||
scimpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/scim/v1"
|
||||
)
|
||||
|
||||
// RateLimitError is returned by SCIM client methods when the server signals a
|
||||
// rate or concurrency limit. It wraps a [trace.LimitExceededError] and carries
|
||||
// the retry-after delay in seconds extracted from the gRPC response trailer.
|
||||
type RateLimitError struct {
|
||||
// RetryAfterSeconds is the value of the "retry-after" gRPC trailer, or 0 if
|
||||
// the trailer was absent.
|
||||
RetryAfterSeconds int64
|
||||
// Err is the underlying LimitExceeded trace error.
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *RateLimitError) Error() string { return e.Err.Error() }
|
||||
func (e *RateLimitError) Unwrap() error { return e.Err }
|
||||
|
||||
// wrapRateLimitErr converts a gRPC error into a [*RateLimitError] when the
|
||||
// error is a limit-exceeded error, extracting the retry-after value from the
|
||||
// supplied trailer.
|
||||
func wrapRateLimitErr(trailer metadata.MD, err error) error {
|
||||
if err == nil || !trace.IsLimitExceeded(err) {
|
||||
return err
|
||||
}
|
||||
rlErr := &RateLimitError{Err: err}
|
||||
if vals := trailer.Get("retry-after"); len(vals) > 0 {
|
||||
if n, parseErr := strconv.ParseInt(vals[0], 10, 64); parseErr == nil {
|
||||
rlErr.RetryAfterSeconds = n
|
||||
}
|
||||
}
|
||||
return rlErr
|
||||
}
|
||||
|
||||
// Client wraps the underlying GRPC client with some more human-friendly tooling
|
||||
type Client struct {
|
||||
grpcClient scimpb.SCIMServiceClient
|
||||
@@ -37,30 +69,33 @@ func NewClient(grpcClient scimpb.SCIMServiceClient) *Client {
|
||||
return &Client{grpcClient: grpcClient}
|
||||
}
|
||||
|
||||
// List fetches all (or a subset of all) resources resources of a given type
|
||||
// ListSCIMResources fetches resources of a given type.
|
||||
func (c *Client) ListSCIMResources(ctx context.Context, req *scimpb.ListSCIMResourcesRequest) (*scimpb.ResourceList, error) {
|
||||
resp, err := c.grpcClient.ListSCIMResources(ctx, req)
|
||||
var trailer metadata.MD
|
||||
resp, err := c.grpcClient.ListSCIMResources(ctx, req, grpc.Trailer(&trailer))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "handling SCIM list request")
|
||||
return nil, trace.Wrap(wrapRateLimitErr(trailer, err), "handling SCIM list request")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetSCIMResource fetches a single SCIM resource from the server by name
|
||||
func (c *Client) GetSCIMResource(ctx context.Context, req *scimpb.GetSCIMResourceRequest) (*scimpb.Resource, error) {
|
||||
resp, err := c.grpcClient.GetSCIMResource(ctx, req)
|
||||
var trailer metadata.MD
|
||||
resp, err := c.grpcClient.GetSCIMResource(ctx, req, grpc.Trailer(&trailer))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "handling SCIM get request")
|
||||
return nil, trace.Wrap(wrapRateLimitErr(trailer, err), "handling SCIM get request")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CreateSCIResource creates a new SCIM resource based on a supplied
|
||||
// CreateSCIMResource creates a new SCIM resource based on a supplied
|
||||
// resource description
|
||||
func (c *Client) CreateSCIMResource(ctx context.Context, req *scimpb.CreateSCIMResourceRequest) (*scimpb.Resource, error) {
|
||||
resp, err := c.grpcClient.CreateSCIMResource(ctx, req)
|
||||
var trailer metadata.MD
|
||||
resp, err := c.grpcClient.CreateSCIMResource(ctx, req, grpc.Trailer(&trailer))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "handling SCIM create request")
|
||||
return nil, trace.Wrap(wrapRateLimitErr(trailer, err), "handling SCIM create request")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -68,27 +103,30 @@ func (c *Client) CreateSCIMResource(ctx context.Context, req *scimpb.CreateSCIMR
|
||||
// UpdateSCIMResource handles a request to update a resource, returning a
|
||||
// representation of the updated resource
|
||||
func (c *Client) UpdateSCIMResource(ctx context.Context, req *scimpb.UpdateSCIMResourceRequest) (*scimpb.Resource, error) {
|
||||
res, err := c.grpcClient.UpdateSCIMResource(ctx, req)
|
||||
var trailer metadata.MD
|
||||
res, err := c.grpcClient.UpdateSCIMResource(ctx, req, grpc.Trailer(&trailer))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "handling SCIM update request")
|
||||
return nil, trace.Wrap(wrapRateLimitErr(trailer, err), "handling SCIM update request")
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// DeleteSCIMResource handles a request to delete a resource.
|
||||
func (c *Client) DeleteSCIMResource(ctx context.Context, req *scimpb.DeleteSCIMResourceRequest) (*emptypb.Empty, error) {
|
||||
res, err := c.grpcClient.DeleteSCIMResource(ctx, req)
|
||||
var trailer metadata.MD
|
||||
res, err := c.grpcClient.DeleteSCIMResource(ctx, req, grpc.Trailer(&trailer))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "handling SCIM delete request")
|
||||
return nil, trace.Wrap(wrapRateLimitErr(trailer, err), "handling SCIM delete request")
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// PatchSCIMResource handles a request to patch a resource.
|
||||
func (c *Client) PatchSCIMResource(ctx context.Context, request *scimpb.PatchSCIMResourceRequest) (*scimpb.Resource, error) {
|
||||
resp, err := c.grpcClient.PatchSCIMResource(ctx, request)
|
||||
var trailer metadata.MD
|
||||
resp, err := c.grpcClient.PatchSCIMResource(ctx, request, grpc.Trailer(&trailer))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "handling SCIM patch request")
|
||||
return nil, trace.Wrap(wrapRateLimitErr(trailer, err), "handling SCIM patch request")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2026 Gravitational, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package scim
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestWrapRateLimitErr(t *testing.T) {
|
||||
t.Run("nil error passthrough", func(t *testing.T) {
|
||||
got := wrapRateLimitErr(metadata.MD{}, nil)
|
||||
require.NoError(t, got)
|
||||
})
|
||||
|
||||
t.Run("non-limit error passthrough", func(t *testing.T) {
|
||||
err := trace.NotFound("not found")
|
||||
got := wrapRateLimitErr(metadata.MD{}, err)
|
||||
require.ErrorIs(t, got, err)
|
||||
require.NotErrorAs(t, err, new(*RateLimitError))
|
||||
})
|
||||
|
||||
t.Run("limit exceeded without retry-after trailer", func(t *testing.T) {
|
||||
err := trace.LimitExceeded("too many requests")
|
||||
got := wrapRateLimitErr(metadata.MD{}, err)
|
||||
var rlErr *RateLimitError
|
||||
require.ErrorAs(t, got, &rlErr)
|
||||
require.Equal(t, int64(0), rlErr.RetryAfterSeconds)
|
||||
require.True(t, trace.IsLimitExceeded(got))
|
||||
})
|
||||
|
||||
t.Run("limit exceeded with retry-after trailer", func(t *testing.T) {
|
||||
err := trace.LimitExceeded("too many requests")
|
||||
trailer := metadata.MD{"retry-after": []string{"42"}}
|
||||
got := wrapRateLimitErr(trailer, err)
|
||||
var rlErr *RateLimitError
|
||||
require.ErrorAs(t, got, &rlErr)
|
||||
require.Equal(t, int64(42), rlErr.RetryAfterSeconds)
|
||||
require.True(t, trace.IsLimitExceeded(got))
|
||||
})
|
||||
|
||||
t.Run("limit exceeded with unparseable retry-after trailer", func(t *testing.T) {
|
||||
err := trace.LimitExceeded("too many requests")
|
||||
trailer := metadata.MD{"retry-after": []string{"not-a-number"}}
|
||||
got := wrapRateLimitErr(trailer, err)
|
||||
var rlErr *RateLimitError
|
||||
require.ErrorAs(t, got, &rlErr)
|
||||
require.Equal(t, int64(0), rlErr.RetryAfterSeconds)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user