Skip to content

Commit

Permalink
Add rate limiter for /api/v1 (#27)
Browse files Browse the repository at this point in the history
* add rate limiter for /api/v1

* add licence

Co-authored-by: Victor Varza <[email protected]>
  • Loading branch information
victorvarza and Victor Varza authored May 9, 2022
1 parent d4c3187 commit 3968b97
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 43 deletions.
2 changes: 2 additions & 0 deletions local/.env.local
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ export OIDC_ISSUER_URL="http://fake-oidc-provider"
export OIDC_CLIENT_ID="fake-oidc-client-id"
export IMAGE_OIDC="nginx"
export IMAGE_PERFORMANCE_TESTS="ghcr.io/adobe/performance-tests"
export IMAGE_PERFORMANCE_TESTS="ghcr.io/adobe/performance-tests"
export API_RATE_LIMITER_ENABLED="true"
2 changes: 1 addition & 1 deletion pkg/apiserver/web/handler/v1/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (h *handler) Register(v1 *echo.Group) {
if err != nil {
log.Fatalf("Failed to initialize authenticator: %v", err)
}
clusters := v1.Group("/clusters", a.VerifyToken())
clusters := v1.Group("/clusters", a.VerifyToken(), web.RateLimiter(h.appConfig))
clusters.GET("/:name", h.GetCluster)
clusters.GET("", h.ListClusters)
}
Expand Down
47 changes: 47 additions & 0 deletions pkg/apiserver/web/rate_limiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
Copyright 2021 Adobe. All rights reserved.
This file is licensed to you 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 REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

package web

import (
"net/http"
"time"

"github.com/adobe/cluster-registry/pkg/config"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

// RateLimiter returns a middleware.RateLimiterWithConfig
func RateLimiter(appConfig *config.AppConfig) echo.MiddlewareFunc {
return middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
Skipper: func(c echo.Context) bool {
return appConfig.ApiRateLimiterEnabled
},
Store: middleware.NewRateLimiterMemoryStoreWithConfig(
middleware.RateLimiterMemoryStoreConfig{Rate: 2, Burst: 120, ExpiresIn: 1 * time.Minute},
),
IdentifierExtractor: func(ctx echo.Context) (string, error) {
oid, ok := ctx.Get("oid").(string)
if !ok {
return "00000000-0000-0000-0000-000000000000", nil
}
return oid, nil
},
ErrorHandler: func(context echo.Context, err error) error {
return context.JSON(http.StatusForbidden, nil)
},
DenyHandler: func(context echo.Context, identifier string, err error) error {
return context.JSON(http.StatusTooManyRequests, nil)
},
})
}
2 changes: 2 additions & 0 deletions pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ func (a *Authenticator) VerifyToken() echo.MiddlewareFunc {
return c.JSON(http.StatusForbidden, NewError(err))
}

c.Set("oid", claims.Oid)

log.Info("Identity logged in: ", claims.Oid)
return next(c)
}
Expand Down
48 changes: 28 additions & 20 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@ import (
)

type AppConfig struct {
AwsRegion string
DbEndpoint string
DbAwsRegion string
DbTableName string
DbIndexName string
SqsEndpoint string
SqsAwsRegion string
SqsQueueName string
OidcClientId string
OidcIssuerUrl string
AwsRegion string
DbEndpoint string
DbAwsRegion string
DbTableName string
DbIndexName string
SqsEndpoint string
SqsAwsRegion string
SqsQueueName string
OidcClientId string
OidcIssuerUrl string
ApiRateLimiterEnabled bool
}

func LoadApiConfig() (*AppConfig, error) {
Expand Down Expand Up @@ -78,17 +79,24 @@ func LoadApiConfig() (*AppConfig, error) {
return nil, fmt.Errorf("Environment variable OIDC_ISSUER_URL is not set.")
}

apiRateLimiterEnabled := false
configApiRateLimiterEnabled := getEnv("API_RATE_LIMITER_ENABLED", "")
if configApiRateLimiterEnabled == "true" {
apiRateLimiterEnabled = true
}

return &AppConfig{
AwsRegion: awsRegion,
DbEndpoint: dbEndpoint,
DbAwsRegion: dbAwsRegion,
DbTableName: dbTableName,
DbIndexName: dbIndexName,
SqsEndpoint: sqsEndpoint,
SqsAwsRegion: sqsAwsRegion,
SqsQueueName: sqsQueueName,
OidcClientId: oidcClientId,
OidcIssuerUrl: oidcIssuerUrl,
AwsRegion: awsRegion,
DbEndpoint: dbEndpoint,
DbAwsRegion: dbAwsRegion,
DbTableName: dbTableName,
DbIndexName: dbIndexName,
SqsEndpoint: sqsEndpoint,
SqsAwsRegion: sqsAwsRegion,
SqsQueueName: sqsQueueName,
OidcClientId: oidcClientId,
OidcIssuerUrl: oidcIssuerUrl,
ApiRateLimiterEnabled: apiRateLimiterEnabled,
}, nil
}

Expand Down
44 changes: 23 additions & 21 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,30 +68,32 @@ func TestLoadApiConfig(t *testing.T) {
expectedError error
}{
{
name: "valid app config",
name: "valid api config",
envVars: map[string]string{
"AWS_REGION": "aws-region",
"DB_ENDPOINT": "http://localhost:8000",
"DB_AWS_REGION": "db-aws-region",
"DB_TABLE_NAME": "cluster-registry-local",
"DB_INDEX_NAME": "search-index-local",
"SQS_ENDPOINT": "http://localhost:9324",
"SQS_AWS_REGION": "sqs-aws-region",
"SQS_QUEUE_NAME": "cluster-registry-local",
"OIDC_CLIENT_ID": "oidc-client-id",
"OIDC_ISSUER_URL": "http://fake-oidc-provider",
"AWS_REGION": "aws-region",
"DB_ENDPOINT": "http://localhost:8000",
"DB_AWS_REGION": "db-aws-region",
"DB_TABLE_NAME": "cluster-registry-local",
"DB_INDEX_NAME": "search-index-local",
"SQS_ENDPOINT": "http://localhost:9324",
"SQS_AWS_REGION": "sqs-aws-region",
"SQS_QUEUE_NAME": "cluster-registry-local",
"OIDC_CLIENT_ID": "oidc-client-id",
"OIDC_ISSUER_URL": "http://fake-oidc-provider",
"API_RATE_LIMITER_ENABLED": "true",
},
expectedAppConfig: &AppConfig{
AwsRegion: "aws-region",
DbEndpoint: "http://localhost:8000",
DbAwsRegion: "db-aws-region",
DbTableName: "cluster-registry-local",
DbIndexName: "search-index-local",
SqsEndpoint: "http://localhost:9324",
SqsAwsRegion: "sqs-aws-region",
SqsQueueName: "cluster-registry-local",
OidcClientId: "oidc-client-id",
OidcIssuerUrl: "http://fake-oidc-provider",
AwsRegion: "aws-region",
DbEndpoint: "http://localhost:8000",
DbAwsRegion: "db-aws-region",
DbTableName: "cluster-registry-local",
DbIndexName: "search-index-local",
SqsEndpoint: "http://localhost:9324",
SqsAwsRegion: "sqs-aws-region",
SqsQueueName: "cluster-registry-local",
OidcClientId: "oidc-client-id",
OidcIssuerUrl: "http://fake-oidc-provider",
ApiRateLimiterEnabled: true,
},
expectedError: nil,
},
Expand Down
44 changes: 44 additions & 0 deletions test/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,47 @@ func (s *e2eTestSuite) TBD_Test_EndToEnd_UpdateCluster() {
s.Assert().Equal(resp.StatusCode, http.StatusOK)
s.Assert().Equal(inputCluster.Spec.Status, outputCluster.Status)
}

func (s *e2eTestSuite) Test_EndToEnd_RateLimiter() {

appConfig, err := config.LoadApiConfig()
if err != nil {
s.T().Fatalf("Cannot load the api configuration: '%v'", err.Error())
}

jwtToken := jwt.GenerateDefaultSignedToken(appConfig)
bearer := "Bearer " + jwtToken

req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d/api/v1/clusters", s.apiPort), nil)
if err != nil {
s.T().Fatalf("Cannot build http request: %v", err.Error())
}

req.Header.Add("Authorization", bearer)
client := &http.Client{}

statusOK := 0
statusTooManyRequests := 0
requests_nr := 200
expectedMaxStatusOK := 150

for i := 0; i < requests_nr; i++ {
resp, err := client.Do(req)
if err != nil {
s.T().Fatalf("Cannot make http request: %v", err.Error())
}
defer resp.Body.Close()

s.NoError(err)

if resp.StatusCode == http.StatusOK {
statusOK += 1
} else if resp.StatusCode == http.StatusTooManyRequests {
statusTooManyRequests += 1
} else {
s.T().Errorf("Unexpected status code: %d", resp.StatusCode)
}
}

s.Assert().LessOrEqual(statusOK, expectedMaxStatusOK)
}
2 changes: 1 addition & 1 deletion test/jwt/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const (
dummySigningKeyType = "RSA PRIVATE KEY"

authScheme = "Bearer"
dummyOid = "00000000-0000-0000-0000-000000000000"
dummyOid = "00000000-0000-0000-0000-000000000001"
expiredDate = "2021-03-11T00:00:00Z"
)

Expand Down

0 comments on commit 3968b97

Please sign in to comment.