Skip to content

API keys Management #1961

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 2 commits into from
Apr 3, 2025
Merged

API keys Management #1961

merged 2 commits into from
Apr 3, 2025

Conversation

paulnoirel
Copy link
Contributor

@paulnoirel paulnoirel commented Mar 26, 2025

Description

Initially, this was a critical feature request for Optum.
The core feature is to create API keys for different users.

Notes

Client only contains wrappers for static methods in ApiKey.
client.create_api_key() shows a warning to emphasize its alpha status.

This PR brings the following changes to the SDK:

Classes

ApiKey: A class to describe API keys

Enum

TimeUnit: An enum to describe the validation time (number of seconds) of an API key as seen in the UI

Functions

client.create_api_key(
    name: str,
    user: Union[Any, str],
    role: Union[Any, str],
    validity: int = 0,
    time_unit: TimeUnit = TimeUnit.SECOND,
) -> Dict[str, str]:

client.get_api_key(api_key_id: str) -> Optional["ApiKey"]

client.get_api_keys(include_expired: bool = False) -> List["ApiKey"]

ApiKey.revoke() -> Dict[str, Any]:

Example:

# Create a new API key
> new_ak_id_token = client.create_api_key("Test New API key", "pnoirel+test@labelbox.com", "Data Admin", 3, TimeUnit.HOUR)
> new_ak_id_token
{'id': 'cm8qgj3v201yq07wl71am15gy',
 'jwt': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbGVrZm4wN3AxNmExMDd4bzUzZGQ2anRuIiwib3JnYW5pemF0aW9uSWQiOiJja3FzYWYwdWRiN3c2MHlib2RyaGMxbjBwIiwiYXBpS2V5SWQiOiJjbThxZ2ozdjIwMXlxMDd3bDcxYW0xNWd5Iiwic2VjcmV0IjoiNjE3YmY0YmJhMjE4YzIxZGE0YTI2ODEyZTg0NjRmNjAiLCJpYXQiOjE3NDMwMjU3NTQsImV4cCI6MTc0MzAzNjU1NH0.eaGR4wwKG3KU-L_ef34rLaPp1JT_7re9aLKGKBNoMTw'}

Retrieve API keys (one/all)

> client.get_api_keys()
[<ApiKey ID: cm4lcpdf9010h07z74e8yfydz>,
 <ApiKey ID: clyfl1tst019p071f7i644jc8>,
 <ApiKey ID: clwj4megk002407zi0s9v44ed>,
 <ApiKey ID: clvnsp1ml0578072b741v1gq8>,
 <ApiKey ID: cluv41rtb01ke0731babo26te>,
 <ApiKey ID: clutybm9u01e407ykgxns7gzi>,
 <ApiKey ID: clusmn71204n007xw0y9b1xwl>,
 <ApiKey ID: clj2xw6m702rw07tofvn6dfga>,
 <ApiKey ID: cm8qgj3v201yq07wl71am15gy>,
 <ApiKey ID: cm8q244sh037807xeb04k4xq5>]

> client.get_api_keys(include_expired=True)
[<ApiKey ID: cm8pzdwpg04fi07xo2pvr3a80>,
 <ApiKey ID: cm8pwewe40wav070h7w9q7fch>,
 <ApiKey ID: cm8ptb9jy0akj07yn3ra8fo82>,
 <ApiKey ID: cm4lcpdf9010h07z74e8yfydz>,
 <ApiKey ID: clyfl1tst019p071f7i644jc8>,
 <ApiKey ID: clwj4megk002407zi0s9v44ed>,
 <ApiKey ID: clvnsp1ml0578072b741v1gq8>,
 <ApiKey ID: cluv41rtb01ke0731babo26te>,
 <ApiKey ID: clutybm9u01e407ykgxns7gzi>,
 <ApiKey ID: clusmn71204n007xw0y9b1xwl>,
 <ApiKey ID: clj2xw6m702rw07tofvn6dfga>,
 <ApiKey ID: cm8qgj3v201yq07wl71am15gy>,
 <ApiKey ID: cm8q244sh037807xeb04k4xq5>,
 <ApiKey ID: cm8pzfzhb0ef307ynhzay52c9>]

Get one API key

> new_ak = client.get_api_key(new_ak_id_token["id"])
> new_ak
<ApiKey ID: cm8qgj3v201yq07wl71am15gy>

> new_ak.expired_at
datetime.datetime(2025, 3, 27, 0, 49, 15, tzinfo=datetime.timezone.utc)

> new_ak.created_by.email
pnoirel@labelbox.com

> new_ak.created_for.email
pnoirel+test@labelbox.com

Revoke an API key

> ak_to_remove.revoke()
{'updateApiKey': {'id': 'cm8qgj3v201yq07wl71am15gy'}}

> ApiKey.get_api_key(client, "cm8qgj3v201yq07wl71am15gy") is None
True

Fixes # (issue)

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Document change (fix typo or modifying any markdown files, code comments or anything in the examples folder only)

All Submissions

  • Have you followed the guidelines in our Contributing document?
  • Have you provided a description?
  • Are your changes properly formatted?

New Feature Submissions

  • Does your submission pass tests?
  • Have you added thorough tests for your new feature?
  • Have you commented your code, particularly in hard-to-understand areas?
  • Have you added a Docstring?

Changes to Core Features

  • Have you written new tests for your core changes, as applicable?
  • Have you successfully run tests with your changes locally?
  • Have you updated any code comments, as applicable?

@paulnoirel paulnoirel force-pushed the pno/PLT-2462_api_key_management branch from 917d43b to 784487b Compare March 26, 2025 23:29
@paulnoirel paulnoirel marked this pull request as ready for review March 26, 2025 23:50
@paulnoirel paulnoirel requested a review from a team as a code owner March 26, 2025 23:50
Comment on lines +316 to +330
user_id = ApiKey._get_user(client, user_email)
if not user_id:
raise ValueError(
f"User with email '{user_email}' does not exist in the organization"
)

role_name = role.name if hasattr(role, "name") else role
if not role_name or not isinstance(role_name, str):
raise ValueError("role must be a Role object or a valid role name")

allowed_roles = ApiKey._get_available_api_key_roles(client)
if role_name not in allowed_roles:
raise ValueError(
f"Invalid role specified. Allowed roles are: {allowed_roles}"
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

are these checks really necessary? I would imagine that backend already has these checks in place and client.execute() would just return appropriate error. Saving client extra bandwidth

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These tests are meant to guide the user in case of errors.
The testing for roles can be simplified thanks to ApiKeyPermissionsNotSubset = '173',
As for the error tests, the GraphQL call will do its best to be successful. See tests below:
image
image
image
image
image

Comment on lines 333 to 350
if validity > 0:
if not isinstance(time_unit, TimeUnit):
raise ValueError(
"time_unit must be a valid TimeUnit enum value"
)

validity_seconds = validity * time_unit.value

if validity_seconds < TimeUnit.MINUTE.value:
raise ValueError("Minimum validity period is 1 minute")

max_seconds = 25 * TimeUnit.WEEK.value
if validity_seconds > max_seconds:
raise ValueError(
"Maximum validity period is 6 months (or 25 weeks)"
)
else:
raise ValueError("validity must be a positive integer")
Copy link
Collaborator

Choose a reason for hiding this comment

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

could just flip logic here and get rid of nested ifs for better code readability. Otherwise, too much mental gymnastic to perform for simple logic

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We still support 3.9. Otherwise, I would use pattern matching to simplify the structure for the bunch of tests.
I will update the test for "validity".

Comment on lines 370 to 392
try:
result = client.execute(query_str, params)
api_key_result = result.get("createApiKey")

if not api_key_result:
raise LabelboxError(
"Failed to create API key. No data returned from the server."
)

return api_key_result

except Exception as e:
if (
"permission" in str(e).lower()
or "unauthorized" in str(e).lower()
):
raise LabelboxError(
f"Permission denied: You don't have sufficient permissions to create API keys. Original error: {str(e)}"
)
else:
error_message = f"Failed to create API key: {str(e)}"
logger.error(error_message)
raise LabelboxError(error_message) from e
Copy link
Collaborator

Choose a reason for hiding this comment

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

here again I think most of that already handled by .execute() method and backend. It performs lots of extra checks and throws errors. So i think it is fine to just call client.execute() and return necessary data. But obviously should check before though 😬

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The exception handling was simplified.

@paulnoirel paulnoirel force-pushed the pno/PLT-2462_api_key_management branch from d0e87e5 to 5458071 Compare March 27, 2025 17:51
@paulnoirel paulnoirel requested a review from mihhail-m March 27, 2025 18:05
)

validity_seconds = 0
if validity < 0:
Copy link
Collaborator

Choose a reason for hiding this comment

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

you probably wanna have <= here. Otherwise 0 is a valid expiration value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It will caught by another test:

        if validity_seconds < TimeUnit.MINUTE.value:
            raise ValueError("Minimum validity period is 1 minute")
            ```

Comment on lines +368 to +380
try:
result = client.execute(query_str, params)
api_key_result = result.get("createApiKey")

if not api_key_result:
raise LabelboxError(
"Failed to create API key. No data returned from the server."
)

return api_key_result

except Exception as e:
raise LabelboxError(str(e)) from e
Copy link
Collaborator

Choose a reason for hiding this comment

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

i think .execute() would already throw error here if something is not right. So you could simplified this further by removing try/catch block.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are 2 things:

  • execute() didn't manage to create an API key and this justifies to raise an exception (execute wouldn't fail)
  • execute()threw an exception and I want it to be propagated.

@paulnoirel paulnoirel merged commit 3027b8b into develop Apr 3, 2025
26 checks passed
@paulnoirel paulnoirel deleted the pno/PLT-2462_api_key_management branch April 3, 2025 08:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants