Skip to content
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

Enforce reasonable limits on API keys and Sessions per Principal #252

Merged
merged 4 commits into from
Jul 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions tiled/_tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ..client import from_config
from ..client.context import CannotRefreshAuthentication
from ..client.utils import ClientError
from ..server import authentication

arr = ArrayAdapter.from_array(numpy.ones((5, 5)))

Expand Down Expand Up @@ -328,3 +329,35 @@ def test_api_keys(enter_password, config):
time.sleep(2)
with fail_with_status_code(401):
from_config(config, api_key=user_key_info["secret"])


def test_api_key_limit(enter_password, config):
# Decrease the limit so this test runs faster.
original_limit = authentication.API_KEY_LIMIT
authentication.API_KEY_LIMIT = 10
try:
with enter_password("secret2"):
user_client = from_config(config, username="bob", token_cache={})

for i in range(authentication.API_KEY_LIMIT):
user_client.context.create_api_key(note=f"key {i}")
# Hit API key limit.
with fail_with_status_code(400):
user_client.context.create_api_key(note="one key too many")
finally:
authentication.API_KEY_LIMIT = original_limit


def test_session_limit(enter_password, config):
# Decrease the limit so this test runs faster.
original_limit = authentication.SESSION_LIMIT
authentication.SESSION_LIMIT = 10
try:
with enter_password("secret1"):
for _ in range(authentication.SESSION_LIMIT):
from_config(config, username="alice", token_cache={})
# Hit Session limit.
with fail_with_status_code(400):
from_config(config, username="alice", token_cache={})
finally:
authentication.SESSION_LIMIT = original_limit
34 changes: 34 additions & 0 deletions tiled/server/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@
ALGORITHM = "HS256"
UNIT_SECOND = timedelta(seconds=1)

# Max API keys and Sessions allowed to Principal.
# This is here for at least two reasons:
# 1. Ensure that the routes which list API keys and sessions, which are
# not paginated, returns in a reasonable time.
# 2. Avoid unintentional or intentional abuse.
API_KEY_LIMIT = 100
SESSION_LIMIT = 200


def utcnow():
"UTC now with second resolution"
Expand Down Expand Up @@ -336,6 +344,19 @@ def create_session(settings, identity_provider, id):
else:
identity.latest_login = now
principal = identity.principal
session_count = (
db.query(orm.Session)
.join(orm.Principal)
.filter(orm.Principal.id == principal.id)
.count()
)
if session_count >= SESSION_LIMIT:
raise HTTPException(
400,
f"This Principal already has {session_count} sessions which is greater "
f"than or equal to the maximum number allowed, {SESSION_LIMIT}. "
"Some Sessions must be closed before creating new ones.",
)
session = orm.Session(
principal_id=principal.id,
expiration_time=utcnow() + settings.session_max_age,
Expand Down Expand Up @@ -444,6 +465,19 @@ def generate_apikey(db, principal, apikey_params, request):
# plus 4 more for extra safety since we store the first eight HEX chars.
secret = secrets.token_bytes(4 + 32)
hashed_secret = hashlib.sha256(secret).digest()
keys_count = (
db.query(orm.APIKey)
.join(orm.Principal)
.filter(orm.Principal.id == principal.id)
.count()
)
if keys_count >= API_KEY_LIMIT:
raise HTTPException(
400,
f"This Principal already has {keys_count} API keys which is greater "
f"than or equal to the maximum number allowed, {API_KEY_LIMIT}. "
"Some API keys must be deleted before creating new ones.",
)
new_key = orm.APIKey(
principal_id=principal.id,
expiration_time=expiration_time,
Expand Down