Skip to content

Commit

Permalink
πŸ”‘->πŸ—‘οΈ Enable Tiled Admins to delete API Keys belonging to other user'…
Browse files Browse the repository at this point in the history
…s principals (#786)

* πŸ” The actual logic for loading and performing deletion.

* βœ… Finish task by adding a dedicated test, extending context.admin

* πŸ—œοΈ Rework namespaces for `revoke_api_key`

* 🏷️ More susinct method name

* πŸ“‹οΈ Check off all testing and renaming revisions

* πŸ“πŸ“πŸ”΅πŸ“ I guess we doin' `enter_username_password` now (upstream breaking API change)

* Truncate first_eight client-side.

* Refine docstrings.

---------

Co-authored-by: Dan Allan <dallan@bnl.gov>
  • Loading branch information
Kezzsim and danielballan authored Oct 3, 2024
1 parent 3fda02e commit 5c30719
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 2 deletions.
31 changes: 31 additions & 0 deletions tiled/_tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,3 +655,34 @@ def test_api_key_bypass_scopes(enter_username_password, principals_context):
context.http_client.get(
resource, params=query_params
).raise_for_status()


def test_admin_delete_principal_apikey(
enter_username_password,
principals_context,
):
"""
Admin can delete API keys for any prinicipal, revoking access.
"""
with principals_context["context"] as context:
# Log in as Bob (Ordinary user)
with enter_username_password("bob", "secret2"):
context.authenticate(username="bob")

# Create an ordinary user API Key
principal_uuid = principals_context["uuid"]["bob"]
api_key_info = context.create_api_key(scopes=["read:data"])
context.logout()

# Log in as Alice (Admin)
with enter_username_password("alice", "secret1"):
context.authenticate(username="alice")

# Delete the created API Key via service principal
context.admin.revoke_api_key(principal_uuid, api_key_info["first_eight"])
context.logout()

# Try to use the revoked API Key
context.api_key = api_key_info["secret"]
with fail_with_status_code(HTTP_401_UNAUTHORIZED):
context.whoami()
42 changes: 40 additions & 2 deletions tiled/client/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,10 @@ def which_api_key(self):

def create_api_key(self, scopes=None, expires_in=None, note=None):
"""
Generate a new API for the currently-authenticated user.
Generate a new API key.
Users with administrative scopes may use ``Context.admin.revoke_api_key``
to create API keys on behalf of other users or services.
Parameters
----------
Expand All @@ -425,11 +428,24 @@ def create_api_key(self, scopes=None, expires_in=None, note=None):
).json()

def revoke_api_key(self, first_eight):
"""
Revoke an API key.
The API key must belong to the currently-authenticated user or service.
Users with administrative scopes may use ``Context.admin.revoke_api_key``
to revoke API keys belonging to other users.
Parameters
----------
first_eight : str
Identify the API key to be deleted by passing its first 8 characters.
(Any additional characters passed will be truncated.)
"""
handle_error(
self.http_client.delete(
self.server_info["authentication"]["links"]["apikey"],
headers={"x-csrf": self.http_client.cookies["tiled_csrf"]},
params={"first_eight": first_eight},
params={"first_eight": first_eight[:8]},
)
)

Expand Down Expand Up @@ -851,6 +867,28 @@ def create_service_principal(
)
).json()

def revoke_api_key(self, uuid, first_eight=None):
"""
Revoke an API key belonging to any user or service.
Parameters
----------
uuid : str
Identify the principal whose API key will be deleted. This is
required in order to reduce the chance of accidentally revoking
the wrong key.
first_eight : str
Identify the API key to be deleted by passing its first 8 characters.
(Any additional characters passed will be truncated.)
"""
return handle_error(
self.context.http_client.delete(
f"{self.base_url}/auth/principal/{uuid}/apikey",
headers={"Accept": MSGPACK_MIME_TYPE},
params={"first_eight": first_eight[:8]},
)
)


class CannotPrompt(Exception):
pass
Expand Down
29 changes: 29 additions & 0 deletions tiled/server/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,35 @@ async def principal(
)


@base_authentication_router.delete(
"/principal/{uuid}/apikey",
response_model=schemas.Principal,
)
async def revoke_apikey_for_principal(
request: Request,
uuid: uuid_module.UUID,
first_eight: str,
principal=Security(get_current_principal, scopes=["admin:apikeys"]),
db=Depends(get_database_session),
):
"Allow Tiled Admins to delete any user's apikeys e.g."
request.state.endpoint = "auth"
api_key_orm = (
await db.execute(
select(orm.APIKey).filter(orm.APIKey.first_eight == first_eight[:8])
)
).scalar()
if (api_key_orm is None) or (api_key_orm.principal.uuid != uuid):
raise HTTPException(
404,
f"The principal {uuid} has no such API key.",
)
await db.delete(api_key_orm)
await db.commit()

return Response(status_code=HTTP_204_NO_CONTENT)


@base_authentication_router.post(
"/principal/{uuid}/apikey",
response_model=schemas.APIKeyWithSecret,
Expand Down

0 comments on commit 5c30719

Please sign in to comment.