Skip to content
Open
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
52 changes: 52 additions & 0 deletions tests/test_api_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# type: ignore
import pytest

from tests.utils.fixtures.mock_api_key import MockApiKey
from tests.utils.syncify import syncify
from workos.api_keys import API_KEY_VALIDATION_PATH, ApiKeys, AsyncApiKeys
from workos.exceptions import AuthenticationException


@pytest.mark.sync_and_async(ApiKeys, AsyncApiKeys)
class TestApiKeys:
@pytest.fixture
def mock_api_key(self):
return MockApiKey().dict()

@pytest.fixture
def api_key(self):
return "sk_my_api_key"

def test_validate_api_key_with_valid_key(
self,
module_instance,
api_key,
mock_api_key,
capture_and_mock_http_client_request,
):
response_body = {"api_key": mock_api_key}
request_kwargs = capture_and_mock_http_client_request(
module_instance._http_client, response_body, 200
)

api_key_details = syncify(module_instance.validate_api_key(value=api_key))

assert request_kwargs["url"].endswith(API_KEY_VALIDATION_PATH)
assert request_kwargs["method"] == "post"
assert api_key_details.id == mock_api_key["id"]
assert api_key_details.name == mock_api_key["name"]
assert api_key_details.object == "api_key"

def test_validate_api_key_with_invalid_key(
self,
module_instance,
mock_http_client_with_response,
):
mock_http_client_with_response(
module_instance._http_client,
{"message": "Invalid API key", "error": "invalid_api_key"},
401,
)

with pytest.raises(AuthenticationException):
syncify(module_instance.validate_api_key(value="invalid-key"))
6 changes: 6 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ def test_client_with_api_key_and_client_id_environment_variables(self):
os.environ.pop("WORKOS_API_KEY")
os.environ.pop("WORKOS_CLIENT_ID")

def test_initialize_api_keys(self, default_client):
assert bool(default_client.api_keys)

def test_initialize_sso(self, default_client):
assert bool(default_client.sso)

Expand Down Expand Up @@ -112,6 +115,9 @@ def test_client_with_api_key_and_client_id_environment_variables(self):
os.environ.pop("WORKOS_API_KEY")
os.environ.pop("WORKOS_CLIENT_ID")

def test_initialize_api_keys(self, default_client):
assert bool(default_client.api_keys)

def test_initialize_directory_sync(self, default_client):
assert bool(default_client.directory_sync)

Expand Down
19 changes: 19 additions & 0 deletions tests/utils/fixtures/mock_api_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import datetime

from workos.types.api_keys import ApiKey


class MockApiKey(ApiKey):
def __init__(self, id="api_key_01234567890"):
now = datetime.datetime.now().isoformat()
super().__init__(
object="api_key",
id=id,
owner={"type": "organization", "id": "org_1337"},
name="Development API Key",
obfuscated_value="api_..0",
permissions=[],
last_used_at=now,
created_at=now,
updated_at=now,
)
16 changes: 10 additions & 6 deletions workos/_base_client.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
from abc import abstractmethod
import os
from abc import abstractmethod
from typing import Optional
from workos.__about__ import __version__

from workos._client_configuration import ClientConfiguration
from workos.fga import FGAModule
from workos.utils._base_http_client import DEFAULT_REQUEST_TIMEOUT
from workos.utils.http_client import HTTPClient
from workos.api_keys import ApiKeysModule
from workos.audit_logs import AuditLogsModule
from workos.directory_sync import DirectorySyncModule
from workos.events import EventsModule
from workos.fga import FGAModule
from workos.mfa import MFAModule
from workos.organizations import OrganizationsModule
from workos.organization_domains import OrganizationDomainsModule
from workos.organizations import OrganizationsModule
from workos.passwordless import PasswordlessModule
from workos.portal import PortalModule
from workos.sso import SSOModule
from workos.user_management import UserManagementModule
from workos.utils._base_http_client import DEFAULT_REQUEST_TIMEOUT
from workos.webhooks import WebhooksModule


Expand Down Expand Up @@ -65,6 +65,10 @@ def __init__(
else int(os.getenv("WORKOS_REQUEST_TIMEOUT", DEFAULT_REQUEST_TIMEOUT))
)

@property
@abstractmethod
def api_keys(self) -> ApiKeysModule: ...

@property
@abstractmethod
def audit_logs(self) -> AuditLogsModule: ...
Expand Down
54 changes: 54 additions & 0 deletions workos/api_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from typing import Protocol

from workos.types.api_keys import ApiKey
from workos.typing.sync_or_async import SyncOrAsync
from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient
from workos.utils.request_helper import REQUEST_METHOD_POST

API_KEY_VALIDATION_PATH = "api_keys/validations"


class ApiKeysModule(Protocol):
def validate_api_key(self, *, value: str) -> SyncOrAsync[ApiKey]:
"""Validates the configured API key.

Returns:
ApiKey: The validated API key details containing
information about the key's name and usage

Raises:
AuthenticationException: If the API key is invalid or
unauthorized (401)
NotFoundException: If the API key is not found (404)
ServerException: If the API server encounters an error
(5xx)
"""
...


class ApiKeys(ApiKeysModule):
_http_client: SyncHTTPClient

def __init__(self, http_client: SyncHTTPClient):
self._http_client = http_client

def validate_api_key(self, *, value: str) -> ApiKey:
response = self._http_client.request(
API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={
"value": value}
)
return ApiKey.model_validate(response["api_key"])
Copy link
Member

Choose a reason for hiding this comment

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

When I passed an invalid value into this method, I saw this error message:

Input should be a valid dictionary or instance of ApiKey

From the docs:

If the key is invalid, the endpoint returns null for the api_key field.

Do we need to handle the invalid case differently?



class AsyncApiKeys(ApiKeysModule):
_http_client: AsyncHTTPClient

def __init__(self, http_client: AsyncHTTPClient):
self._http_client = http_client

async def validate_api_key(self, *, value: str) -> ApiKey:
response = await self._http_client.request(
API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={
"value": value}
)
return ApiKey.model_validate(response["api_key"])
7 changes: 7 additions & 0 deletions workos/async_client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Optional
from workos.__about__ import __version__
from workos._base_client import BaseClient
from workos.api_keys import AsyncApiKeys
from workos.audit_logs import AuditLogsModule
from workos.directory_sync import AsyncDirectorySync
from workos.events import AsyncEvents
Expand Down Expand Up @@ -45,6 +46,12 @@ def __init__(
timeout=self.request_timeout,
)

@property
def api_keys(self) -> AsyncApiKeys:
if not getattr(self, "_api_keys", None):
self._api_keys = AsyncApiKeys(self._http_client)
return self._api_keys

@property
def sso(self) -> AsyncSSO:
if not getattr(self, "_sso", None):
Expand Down
7 changes: 7 additions & 0 deletions workos/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Optional
from workos.__about__ import __version__
from workos._base_client import BaseClient
from workos.api_keys import ApiKeys
from workos.audit_logs import AuditLogs
from workos.directory_sync import DirectorySync
from workos.fga import FGA
Expand Down Expand Up @@ -45,6 +46,12 @@ def __init__(
timeout=self.request_timeout,
)

@property
def api_keys(self) -> ApiKeys:
if not getattr(self, "_api_keys", None):
self._api_keys = ApiKeys(self._http_client)
return self._api_keys

@property
def sso(self) -> SSO:
if not getattr(self, "_sso", None):
Expand Down
1 change: 1 addition & 0 deletions workos/types/api_keys/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .api_keys import ApiKey as ApiKey # noqa: F401
20 changes: 20 additions & 0 deletions workos/types/api_keys/api_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import Literal

from workos.types.workos_model import WorkOSModel


class ApiKeyOwner(WorkOSModel):
type: str
id: str


class ApiKey(WorkOSModel):
object: Literal["api_key"]
id: str
owner: ApiKeyOwner
name: str
obfuscated_value: str
last_used_at: str | None = None
Copy link
Member

Choose a reason for hiding this comment

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

This line caused some issues for me with a test project. I see a different approach in another type file, would this work here?

permissions: list[str]
created_at: str
updated_at: str
Loading