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

Test creation of API keys for other principals #609

Merged
merged 7 commits into from
Nov 28, 2023
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
149 changes: 149 additions & 0 deletions tiled/_tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,3 +449,152 @@ def test_sticky_identity(enter_password, config):
# Clear the default.
clear_default_identity(context.api_uri)
get_default_identity(context.api_uri) is None


@pytest.fixture
def principals_context(enter_password, config):
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved
"""
Fetch UUID for an admin and an ordinary user; include the client context.
"""
# Make alice an admin. Leave bob as a user.
config["authentication"]["tiled_admins"] = [{"provider": "toy", "id": "alice"}]

with Context.from_app(build_app_from_config(config)) as context:
# Log in as Alice and retrieve admin UUID for later use
with enter_password("secret1"):
context.authenticate(username="alice")

principal = context.whoami()
assert "admin" in (role["name"] for role in principal["roles"])
admin_uuid = principal["uuid"]
context.logout()

# Log in as Bob and retrieve Bob's UUID for later use
with enter_password("secret2"):
context.authenticate(username="bob")

principal = context.whoami()
assert "admin" not in (role["name"] for role in principal["roles"])
bob_uuid = principal["uuid"]
context.logout()

yield {
"uuid": {"alice": admin_uuid, "bob": bob_uuid},
"context": context,
}


@pytest.mark.parametrize(
"username, scopes, resource",
(
("alice", ["read:principals"], "/api/v1/auth/principal"),
("bob", ["read:data"], "/api/v1/array/full/A1"),
),
)
def test_admin_api_key_any_principal(
enter_password, principals_context, username, scopes, resource
):
"""
Admin can create usable API keys for any prinicipal, within that principal's scopes.
"""
with principals_context["context"] as context:
# Log in as Alice, create and use API key after logout
with enter_password("secret1"):
context.authenticate(username="alice")

principal_uuid = principals_context["uuid"][username]
api_key = _create_api_key_other_principal(
context=context, uuid=principal_uuid, scopes=scopes
)
assert api_key
context.logout()

context.api_key = api_key
context.http_client.get(resource).raise_for_status()
context.api_key = None
# The same endpoint fails without an API key
with fail_with_status_code(401):
context.http_client.get(resource).raise_for_status()


def test_admin_api_key_any_principal_exceeds_scopes(enter_password, principals_context):
"""
Admin cannot create API key that exceeds scopes for another principal.
"""
with principals_context["context"] as context:
# Log in as Alice, create and use API key after logout
with enter_password("secret1"):
context.authenticate(username="alice")

principal_uuid = principals_context["uuid"]["bob"]
with fail_with_status_code(400) as fail_info:
_create_api_key_other_principal(
context=context, uuid=principal_uuid, scopes=["read:principals"]
)
fail_message = " must be a subset of the principal's scopes "
assert fail_message in fail_info.response.text
context.logout()


@pytest.mark.parametrize("username", ("alice", "bob"))
def test_api_key_any_principal(enter_password, principals_context, username):
"""
Ordinary user cannot create API key for another principal.
"""
with principals_context["context"] as context:
# Log in as Bob, this API endpoint is unauthorized
with enter_password("secret2"):
context.authenticate(username="bob")

principal_uuid = principals_context["uuid"][username]
with fail_with_status_code(401):
_create_api_key_other_principal(
context=context, uuid=principal_uuid, scopes=["read:metadata"]
)


def test_api_key_bypass_scopes(enter_password, principals_context):
"""
Ordinary user cannot create API key that bypasses a scopes restriction.
"""
with principals_context["context"] as context:
# Log in as Bob, create API key with empty scopes
with enter_password("secret2"):
context.authenticate(username="bob")

response = context.http_client.post(
"/api/v1/auth/apikey", json={"expires_in": None, "scopes": []}
)
response.raise_for_status()
api_key = response.json()["secret"]
assert api_key
context.logout()

# Try the new API key with admin and normal resources
for resource in ("/api/v1/auth/principal", "/api/v1/array/full/A1"):
# Try with/without key, with/without empty scopes
for query_params in (
{"api_key": api_key},
{"scopes": []},
{"api_key": api_key, "scopes": []},
):
context.api_key = query_params.pop("api_key", None)
with fail_with_status_code(401):
context.http_client.get(
resource, params=query_params
).raise_for_status()


def _create_api_key_other_principal(context, uuid, scopes=None):
"""
Return api_key or raise error.
"""
response = context.http_client.post(
f"/api/v1/auth/principal/{uuid}/apikey",
json={"expires_in": None, "scopes": scopes or []},
)
response.raise_for_status()
api_key_info = response.json()
api_key = api_key_info["secret"]

return api_key
2 changes: 1 addition & 1 deletion tiled/_tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@contextlib.contextmanager
def fail_with_status_code(status_code):
with pytest.raises(httpx.HTTPStatusError) as info:
yield
yield info
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved
assert info.value.response.status_code == status_code


Expand Down