Skip to content

Limit a user's v2 subscriptions to 100 per replica. #2310

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 9, 2019
1 change: 1 addition & 0 deletions dss-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,7 @@ paths:
- invalid_jmespath
- unprocessable
- read_only
- not_acceptable
required:
- code
/subscriptions/{uuid}:
Expand Down
21 changes: 14 additions & 7 deletions dss/api/subscriptions_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
from dss.config import Replica
from dss.error import DSSException
from dss.util import security
from dss.config import SUBSCRIPTION_LIMIT
from dss.subscriptions_v2 import (SubscriptionData,
get_subscription,
put_subscription,
delete_subscription,
get_subscriptions_for_owner)
get_subscriptions_for_owner,
count_subscriptions_for_owner)


@security.authorized_group_required(['hca', 'public'])
Expand All @@ -29,16 +31,21 @@ def get(uuid: str, replica: str):
@security.authorized_group_required(['hca', 'public'])
def find(replica: str):
owner = security.get_token_email(request.token_info)
subs = [s for s in get_subscriptions_for_owner(Replica[replica], owner) if owner == s['owner']]
for s in subs:
s['replica'] = Replica[replica].name
if 'hmac_secret_key' in s:
s.pop('hmac_secret_key')
return {'subscriptions': subs}, requests.codes.ok
subscriptions = get_subscriptions_for_owner(Replica[replica], owner)
for subscription in subscriptions:
subscription['replica'] = Replica[replica].name
if 'hmac_secret_key' in subscription:
subscription.pop('hmac_secret_key')
return {'subscriptions': subscriptions}, requests.codes.ok


@security.authorized_group_required(['hca', 'public'])
def put(json_request_body: dict, replica: str):
owner = security.get_token_email(request.token_info)
if count_subscriptions_for_owner(Replica[replica], owner) > SUBSCRIPTION_LIMIT:
raise DSSException(requests.codes.not_acceptable, "not_acceptable",
f"Users cannot exceed {SUBSCRIPTION_LIMIT} subscriptions!")

subscription_doc = json_request_body.copy()
subscription_doc[SubscriptionData.OWNER] = security.get_token_email(request.token_info)
subscription_uuid = str(uuid4())
Expand Down
16 changes: 16 additions & 0 deletions dss/dynamodb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ def get_primary_key_items(*, table: str, key: str, return_key: str='body') -> Ge
yield item[return_key]['S']


def get_primary_key_count(*, table: str, key: str) -> int:
"""
Returns the number of values associated with a primary key from a dynamoDB table.

:param table: Name of the table in AWS.
:param str key: 1st primary key that can be used to fetch associated sort_keys and values.
:return: Int
"""
res = db.query(TableName=table,
KeyConditionExpression="#hash_key=:key",
ExpressionAttributeNames={'#hash_key': "hash_key"},
ExpressionAttributeValues={':key': {'S': key}},
Select='COUNT')
return res['Count']


def get_all_table_items(*, table: str, both_keys: bool=False):
"""
Return all items from a dynamoDB table.
Expand Down
5 changes: 5 additions & 0 deletions dss/subscriptions_v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ def get_subscriptions_for_owner(replica: Replica, owner: str) -> list:
return [json.loads(item) for item in items]


def count_subscriptions_for_owner(replica: Replica, owner: str) -> int:
return dynamodb.get_primary_key_count(table=subscription_db_table.format(replica.name),
key=owner)


def get_subscriptions_for_replica(replica: Replica) -> list:
items = dynamodb.get_all_table_items(table=subscription_db_table.format(replica.name))
return [json.loads(item) for item in items]
Expand Down
10 changes: 7 additions & 3 deletions tests/infra/base_smoketest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
"""
A basic integration test of the DSS. This can also be invoked via `make smoketest`.
"""
import os, sys, argparse, time, uuid, json, shutil, tempfile, unittest
import os
import sys
import argparse
import time
import json
import tempfile
import unittest
import subprocess

import boto3
import botocore
from cloud_blobstore import BlobStore
from itertools import product

pkg_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) # noqa
sys.path.insert(0, pkg_root) # noqa
Expand Down
25 changes: 25 additions & 0 deletions tests/test_subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
sys.path.insert(0, pkg_root) # noqa

import dss
from dss.config import Replica
from dss.index.es import ElasticsearchClient
from dss.index.es.document import BundleDocument
from dss.index.es.manager import IndexManager
from dss.notify.notification import Endpoint
from dss.logging import configure_test_logging
from dss.util import UrlBuilder
from dss.subscriptions_v2 import count_subscriptions_for_owner, put_subscription, delete_subscription, SubscriptionData
from tests import get_auth_header, get_bundle_fqid
from tests.infra import DSSAssertMixin, testmode, TestAuthMixin
from tests.infra.elasticsearch_test_case import ElasticsearchTestCase
Expand Down Expand Up @@ -96,6 +98,28 @@ def test_put(self):
finally:
self._cleanup_subscription(uuid_)

def test_db_count_subscriptions_for_owner(self):
"""Test dynamoDB helper functions used to store and retrieve subscription information."""
owner = 'email@email.com'
subscription_uuid = str(uuid.uuid4())

for r in Replica:
replica = r.name
subscription_doc = {SubscriptionData.OWNER: owner,
SubscriptionData.UUID: subscription_uuid,
SubscriptionData.REPLICA: replica}

with self.subTest(f'DynamoDB: Counting initial (zero) subscriptions for {owner} in {replica}.'):
assert count_subscriptions_for_owner(Replica[replica], owner) == 0

with self.subTest(f'DynamoDB: put_subscription for {owner} in {replica} and check count is one.'):
put_subscription(subscription_doc)
assert count_subscriptions_for_owner(Replica[replica], owner) == 1

with self.subTest(f'DynamoDB: Counting (zero) subscriptions for {owner} in {replica} after deletion.'):
delete_subscription(Replica[replica], owner=owner, uuid=subscription_uuid)
assert count_subscriptions_for_owner(Replica[replica], owner) == 0

def test_validation(self):
with self.subTest("Missing URL"):
self._put_subscription(expect_code=400,
Expand Down Expand Up @@ -255,6 +279,7 @@ class TestGCPSubscription(TestSubscriptionsBase):
class TestAWSSubscription(TestSubscriptionsBase):
replica = dss.Replica.aws


# Prevent unittest's discovery from attempting to discover the base test class. The alterative, not inheriting
# TestCase in the base class, is too inconvenient because it interferes with auto-complete and generates PEP-8
# warnings about the camel case methods.
Expand Down