Skip to content

Commit

Permalink
fix: rate limit admin endpoints (#2703)
Browse files Browse the repository at this point in the history
  • Loading branch information
gagantrivedi committed Oct 18, 2023
1 parent bbae09c commit b0ef013
Show file tree
Hide file tree
Showing 13 changed files with 96 additions and 5 deletions.
4 changes: 4 additions & 0 deletions api/api_keys/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ def __init__(self, key: MasterAPIKey):
def is_authenticated(self) -> bool:
return True

@property
def pk(self) -> str:
return self.key.id

@property
def is_master_api_key_user(self) -> bool:
return True
Expand Down
3 changes: 3 additions & 0 deletions api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@

LOGIN_THROTTLE_RATE = env("LOGIN_THROTTLE_RATE", "20/min")
SIGNUP_THROTTLE_RATE = env("SIGNUP_THROTTLE_RATE", "10000/min")
USER_THROTTLE_RATE = env("USER_THROTTLE_RATE", "500/min")
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
"DEFAULT_AUTHENTICATION_CLASSES": (
Expand All @@ -227,11 +228,13 @@
"PAGE_SIZE": 10,
"UNICODE_JSON": False,
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"DEFAULT_THROTTLE_CLASSES": ["rest_framework.throttling.UserRateThrottle"],
"DEFAULT_THROTTLE_RATES": {
"login": LOGIN_THROTTLE_RATE,
"signup": SIGNUP_THROTTLE_RATE,
"mfa_code": "5/min",
"invite": "10/min",
"user": USER_THROTTLE_RATE,
},
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
"DEFAULT_RENDERER_CLASSES": [
Expand Down
1 change: 1 addition & 0 deletions api/app/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
"mfa_code": "5/min",
"invite": "10/min",
"signup": "100/min",
"user": "100000/day",
}
18 changes: 18 additions & 0 deletions api/environments/identities/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@
from util.tests import Helper


def test_get_identities_is_not_throttled_by_user_throttle(
environment, feature, identity, api_client, settings
):
# Given
settings.REST_FRAMEWORK = {"DEFAULT_THROTTLE_RATES": {"user": "1/minute"}}

api_client.credentials(HTTP_X_ENVIRONMENT_KEY=environment.api_key)
base_url = reverse("api-v1:sdk-identities")
url = f"{base_url}?identifier={identity.identifier}"

# When
for _ in range(10):
response = api_client.get(url)

# Then
assert response.status_code == status.HTTP_200_OK


@pytest.mark.django_db
class IdentityTestCase(TestCase):
identifier = "user1"
Expand Down
25 changes: 25 additions & 0 deletions api/environments/identities/traits/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,3 +775,28 @@ def test_delete_trait_only_deletes_traits_in_current_environment(self):

# and
assert Trait.objects.filter(pk=trait_2.id).exists()


def test_set_trait_for_an_identity_is_not_throttled_by_user_throttle(
settings, identity, environment, api_client
):
# Given
settings.REST_FRAMEWORK = {"DEFAULT_THROTTLE_RATES": {"user": "1/minute"}}

api_client.credentials(HTTP_X_ENVIRONMENT_KEY=environment.api_key)

url = reverse("api-v1:sdk-traits-list")
data = {
"identity": {"identifier": identity.identifier},
"trait_key": "key",
"trait_value": "value",
}

# When
for _ in range(10):
res = api_client.post(
url, data=json.dumps(data), content_type="application/json"
)

# Then
assert res.status_code == status.HTTP_200_OK
1 change: 1 addition & 0 deletions api/environments/identities/traits/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ def post(self, request, identifier, trait_key, *args, **kwargs):
class SDKTraits(mixins.CreateModelMixin, viewsets.GenericViewSet):
permission_classes = (EnvironmentKeyPermissions, TraitPersistencePermissions)
authentication_classes = (EnvironmentKeyAuthentication,)
throttle_classes = []

def get_serializer_class(self):
if self.action == "increment_value":
Expand Down
1 change: 1 addition & 0 deletions api/environments/identities/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def get(self, request, identifier, *args, **kwargs):
class SDKIdentities(SDKAPIView):
serializer_class = IdentifyWithTraitsSerializer
pagination_class = None # set here to ensure documentation is correct
throttle_classes = []

@swagger_auto_schema(
responses={200: SDKIdentitiesResponseSerializer()},
Expand Down
2 changes: 1 addition & 1 deletion api/environments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def clone(self, name: str, api_key: str = None) -> "Environment":
clone.api_key = api_key if api_key else create_hash()
clone.save()

# Since identities are closely tied to the enviroment
# Since identities are closely tied to the environment
# it does not make much sense to clone them, hence
# only clone feature states without identities
for feature_state in self.feature_states.filter(identity=None):
Expand Down
1 change: 1 addition & 0 deletions api/environments/sdk/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

class SDKEnvironmentAPIView(APIView):
permission_classes = (EnvironmentKeyPermissions,)
throttle_classes = []

def get_authenticators(self):
return [EnvironmentKeyAuthentication(required_key_prefix="ser.")]
Expand Down
17 changes: 17 additions & 0 deletions api/features/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,3 +816,20 @@ def test_create_segment_override(admin_client, feature, segment, environment):
assert created_override is not None
assert created_override.enabled is enabled
assert created_override.get_feature_state_value() == string_value


def test_get_flags_is_not_throttled_by_user_throttle(
api_client, environment, feature, settings
):
# Given
settings.REST_FRAMEWORK = {"DEFAULT_THROTTLE_RATES": {"user": "1/minute"}}
api_client.credentials(HTTP_X_ENVIRONMENT_KEY=environment.api_key)

url = reverse("api-v1:flags")

# When
for _ in range(10):
response = api_client.get(url)

# Then
assert response.status_code == status.HTTP_200_OK
1 change: 1 addition & 0 deletions api/features/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ class SDKFeatureStates(GenericAPIView):
permission_classes = (EnvironmentKeyPermissions,)
authentication_classes = (EnvironmentKeyAuthentication,)
renderer_classes = [JSONRenderer]
throttle_classes = []
pagination_class = None

@swagger_auto_schema(
Expand Down
8 changes: 4 additions & 4 deletions api/tests/integration/environments/test_clone_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
def test_clone_environment_clones_feature_states_with_value(
client, project, environment, environment_api_key, feature
):
# Firstly, let's update feature state value of the source enviroment
# Firstly, let's update feature state value of the source environment
# fetch the feature state id to update
feature_state = get_env_feature_states_list_with_api(
client, {"environment": environment, "feature": feature}
Expand Down Expand Up @@ -52,7 +52,7 @@ def test_clone_environment_clones_feature_states_with_value(
client, {"environment": environment}
)

# Now, fetch the feature states of the clone enviroment
# Now, fetch the feature states of the clone environment
clone_env_feature_states = get_env_feature_states_list_with_api(
client, {"environment": res.json()["id"]}
)
Expand Down Expand Up @@ -81,13 +81,13 @@ def test_clone_environment_clones_feature_states_with_value(
def test_clone_environment_creates_admin_permission_with_the_current_user(
admin_user, admin_client, environment, environment_api_key
):
# Firstly, let's create the clone of the enviroment
# Firstly, let's create the clone of the environment
env_name = "Cloned env"
url = reverse("api-v1:environments:environment-clone", args=[environment_api_key])
res = admin_client.post(url, {"name": env_name})
clone_env_api_key = res.json()["api_key"]

# Now, fetch the permission of the newly creatd enviroment
# Now, fetch the permission of the newly creatd environment
perm_url = reverse(
"api-v1:environments:environment-user-permissions-list",
args=[clone_env_api_key],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,22 @@ def test_get_environment_document_fails_with_invalid_key(
# We get a 403 since only the server side API keys are able to access the
# environment document
assert response.status_code == status.HTTP_403_FORBIDDEN


def test_get_environment_document_is_not_throttled_by_user_throttle(
environment, feature, settings, environment_api_key
):
# Given
settings.REST_FRAMEWORK = {"DEFAULT_THROTTLE_RATES": {"user": "1/minute"}}

client = APIClient()
client.credentials(HTTP_X_ENVIRONMENT_KEY=environment_api_key.key)

url = reverse("api-v1:environment-document")

# When
for _ in range(10):
response = client.get(url)

# Then
assert response.status_code == status.HTTP_200_OK

3 comments on commit b0ef013

@vercel
Copy link

@vercel vercel bot commented on b0ef013 Oct 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on b0ef013 Oct 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on b0ef013 Oct 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./docs

docs.bullet-train.io
docs-flagsmith.vercel.app
docs-git-main-flagsmith.vercel.app
docs.flagsmith.com

Please sign in to comment.