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
12 changes: 6 additions & 6 deletions .castiron.stats.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
schema_version: 1
generation_id: 37557256-538a-4089-92c7-6f39d87dca31
openapi_spec_hash: b6c325f19993092615621b2fa55ace03
openapi_transformed_spec_hash: 3dae5e7c6a7cc3df593c09d61bc62634
generation_id: 5db14cf2-9ae8-425c-9adc-367863b168c1
openapi_spec_hash: 474a22aec52c2f891cc87351f0026848
openapi_transformed_spec_hash: 859d8a9b3cd9a44e73c8240fbcf371e8
config_hash: 47b922b06d16fe0e7595c5eacbbb5495
codegen_sha: 4385b339c0a495b9fcc22b913b174478a699d891
codegen_hash: 58038e1d56dbd392400609695a51879d4434626f20175b12c5d93d6f77f06388
public_codegen_sha: 63c09929569758367e2d413b5fa67c005171f861
codegen_sha: 082683eb0d9ef5eea11546327118139cd7f65e29
codegen_hash: f4645293de1b8dc17969ad8d0bade011436e1612b19999801b3c53035eadd6cd
public_codegen_sha: dc64d4377b79cb45c27d9728c9a5cd23d2502e66
29 changes: 23 additions & 6 deletions api_reference/openapi.transformed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12959,9 +12959,19 @@ paths:
- AdminApiKeyAuth: []
summary: Update project API key
description: |
Updates the expiration of an existing API key in the project, including a
Updates the scopes and/or expiration of an existing API key in the project, including a
service-account-owned key. Requires permission to write project API keys.

Provide at least one of `scopes` or `expires_in_seconds`. Omitted fields stay
unchanged; neither field accepts `null`. Both changes are saved together.

`scopes` replaces the key's scope restrictions using the same scope identifiers
as the dashboard. An empty array means All permissions, subject to the owner's
effective access; it does not mean no access. Use `["api.all.read"]` for read-only
access or specific scopes such as `["api.files.read"]` for restricted access.
This does not change user or service-account roles or grant admin permissions.
Scope changes follow the same authentication-cache propagation as dashboard edits.

Provide `expires_in_seconds` to set the expiration relative to this request.
The value must satisfy the organization's and project's expiry policies.
Expired keys cannot be updated. Retrying resets the expiration relative to
Expand All @@ -12988,15 +12998,18 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/UpdateProjectApiKeyBody'
example:
scopes:
- api.all.read
responses:
'200':
description: Project API key expiration updated successfully.
description: Project API key updated successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/ProjectApiKey'
'400':
description: The project is archived, the key is expired, or the requested expiration is invalid.
description: The project is archived, the key is expired, or the requested scopes or expiration are invalid.
content:
application/json:
schema:
Expand Down Expand Up @@ -78500,10 +78513,14 @@ components:
type: integer
maximum: 31536000
minimum: 1
description: Number of seconds from this request until the API key expires. Must comply with the organization and project expiry policies. Expired keys cannot be updated. Retrying resets the expiry relative to the new request.
description: Number of seconds from this request until the API key expires. Must comply with the organization and project expiry policies. Expired keys cannot be updated. Retrying resets the expiry relative to the new request. Omit to keep the current expiration.
scopes:
items:
type: string
type: array
description: Replace the API key scope restrictions. Omit to leave scopes unchanged. An empty array removes key restrictions (All permissions), subject to the owner's effective permissions. Use ["api.all.read"] for read-only access. Scopes do not change the owner's roles or grant administrative permissions.
type: object
required:
- expires_in_seconds
required: []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce a non-empty API-key update

With both properties optional and no object-level constraint, this schema accepts {}, so the generated clients expose update(api_key_id=..., project_id=...) and the new tests incorrectly treat that request as successful. This contradicts the endpoint contract that at least one of scopes or expires_in_seconds must be supplied and causes such calls to send an invalid no-op request that the real API rejects; encode the at-least-one constraint or validate it in both client methods.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed: this SDK currently permits an update with neither field and dispatches a request that the server rejects. The generated mock test does not establish that the real API accepts an empty update. The CLI's local guard does not fix this SDK.

The at-least-one schema/compiler constraint remains deferred while we preserve existing public request types. The smallest proposed behavioral correction is an endpoint-specific check of the effective request body before dispatch, with tests for an empty request and the valid expiry-only, scope-only, combined, and empty-scope-list cases. That proposal must also preserve supported request-body overrides; checking only named arguments would be insufficient.

No correction has been made to this SDK yet. Leaving this finding open and holding this PR from the merge queue pending agreement on that additional runtime validation versus an explicitly accepted server-validation disposition.

SpecificProgrammaticToolCallingParam:
properties:
type:
Expand Down
70 changes: 59 additions & 11 deletions src/openai/resources/admin/organization/projects/api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import httpx2

from ..... import _legacy_response
from ....._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
from ....._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
from ....._utils import path_template, maybe_transform, async_maybe_transform
from ....._compat import cached_property
from ....._resource import SyncAPIResource, AsyncAPIResource
Expand Down Expand Up @@ -90,7 +90,8 @@ def update(
api_key_id: str,
*,
project_id: str,
expires_in_seconds: int,
expires_in_seconds: int | Omit = omit,
scopes: SequenceNotStr[str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
Expand All @@ -99,8 +100,20 @@ def update(
timeout: float | httpx2.Timeout | None | NotGiven = not_given,
) -> ProjectAPIKey:
"""
Updates the expiration of an existing API key in the project, including a
service-account-owned key. Requires permission to write project API keys.
Updates the scopes and/or expiration of an existing API key in the project,
including a service-account-owned key. Requires permission to write project API
keys.
Provide at least one of `scopes` or `expires_in_seconds`. Omitted fields stay
unchanged; neither field accepts `null`. Both changes are saved together.
`scopes` replaces the key's scope restrictions using the same scope identifiers
as the dashboard. An empty array means All permissions, subject to the owner's
effective access; it does not mean no access. Use `["api.all.read"]` for
read-only access or specific scopes such as `["api.files.read"]` for restricted
access. This does not change user or service-account roles or grant admin
permissions. Scope changes follow the same authentication-cache propagation as
dashboard edits.
Provide `expires_in_seconds` to set the expiration relative to this request.
The value must satisfy the organization's and project's expiry policies.
Expand All @@ -110,7 +123,13 @@ def update(
Args:
expires_in_seconds: Number of seconds from this request until the API key expires. Must comply with
the organization and project expiry policies. Expired keys cannot be updated.
Retrying resets the expiry relative to the new request.
Retrying resets the expiry relative to the new request. Omit to keep the current
expiration.
scopes: Replace the API key scope restrictions. Omit to leave scopes unchanged. An empty
array removes key restrictions (All permissions), subject to the owner's
effective permissions. Use ["api.all.read"] for read-only access. Scopes do not
change the owner's roles or grant administrative permissions.
extra_headers: Send extra headers
Expand All @@ -130,7 +149,13 @@ def update(
project_id=project_id,
api_key_id=api_key_id,
),
body=maybe_transform({"expires_in_seconds": expires_in_seconds}, api_key_update_params.APIKeyUpdateParams),
body=maybe_transform(
{
"expires_in_seconds": expires_in_seconds,
"scopes": scopes,
},
api_key_update_params.APIKeyUpdateParams,
),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
Expand Down Expand Up @@ -321,7 +346,8 @@ async def update(
api_key_id: str,
*,
project_id: str,
expires_in_seconds: int,
expires_in_seconds: int | Omit = omit,
scopes: SequenceNotStr[str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
Expand All @@ -330,8 +356,20 @@ async def update(
timeout: float | httpx2.Timeout | None | NotGiven = not_given,
) -> ProjectAPIKey:
"""
Updates the expiration of an existing API key in the project, including a
service-account-owned key. Requires permission to write project API keys.
Updates the scopes and/or expiration of an existing API key in the project,
including a service-account-owned key. Requires permission to write project API
keys.
Provide at least one of `scopes` or `expires_in_seconds`. Omitted fields stay
unchanged; neither field accepts `null`. Both changes are saved together.
`scopes` replaces the key's scope restrictions using the same scope identifiers
as the dashboard. An empty array means All permissions, subject to the owner's
effective access; it does not mean no access. Use `["api.all.read"]` for
read-only access or specific scopes such as `["api.files.read"]` for restricted
access. This does not change user or service-account roles or grant admin
permissions. Scope changes follow the same authentication-cache propagation as
dashboard edits.
Provide `expires_in_seconds` to set the expiration relative to this request.
The value must satisfy the organization's and project's expiry policies.
Expand All @@ -341,7 +379,13 @@ async def update(
Args:
expires_in_seconds: Number of seconds from this request until the API key expires. Must comply with
the organization and project expiry policies. Expired keys cannot be updated.
Retrying resets the expiry relative to the new request.
Retrying resets the expiry relative to the new request. Omit to keep the current
expiration.
scopes: Replace the API key scope restrictions. Omit to leave scopes unchanged. An empty
array removes key restrictions (All permissions), subject to the owner's
effective permissions. Use ["api.all.read"] for read-only access. Scopes do not
change the owner's roles or grant administrative permissions.
extra_headers: Send extra headers
Expand All @@ -362,7 +406,11 @@ async def update(
api_key_id=api_key_id,
),
body=await async_maybe_transform(
{"expires_in_seconds": expires_in_seconds}, api_key_update_params.APIKeyUpdateParams
{
"expires_in_seconds": expires_in_seconds,
"scopes": scopes,
},
api_key_update_params.APIKeyUpdateParams,
),
options=make_request_options(
extra_headers=extra_headers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,27 @@

from typing_extensions import Required, TypedDict

from ....._types import SequenceNotStr

__all__ = ["APIKeyUpdateParams"]


class APIKeyUpdateParams(TypedDict, total=False):
project_id: Required[str]

expires_in_seconds: Required[int]
expires_in_seconds: int
"""Number of seconds from this request until the API key expires.
Must comply with the organization and project expiry policies. Expired keys
cannot be updated. Retrying resets the expiry relative to the new request.
cannot be updated. Retrying resets the expiry relative to the new request. Omit
to keep the current expiration.
"""

scopes: SequenceNotStr[str]
"""Replace the API key scope restrictions.
Omit to leave scopes unchanged. An empty array removes key restrictions (All
permissions), subject to the owner's effective permissions. Use ["api.all.read"]
for read-only access. Scopes do not change the owner's roles or grant
administrative permissions.
"""
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,19 @@ def test_path_params_retrieve(self, client: OpenAI) -> None:

@parametrize
def test_method_update(self, client: OpenAI) -> None:
api_key = client.admin.organization.projects.api_keys.update(
api_key_id="api_key_id",
project_id="project_id",
)
assert_matches_type(ProjectAPIKey, api_key, path=["response"])

@parametrize
def test_method_update_with_all_params(self, client: OpenAI) -> None:
api_key = client.admin.organization.projects.api_keys.update(
api_key_id="api_key_id",
project_id="project_id",
expires_in_seconds=1,
scopes=["api.all.read"],
)
assert_matches_type(ProjectAPIKey, api_key, path=["response"])

Expand All @@ -83,7 +92,6 @@ def test_raw_response_update(self, client: OpenAI) -> None:
response = client.admin.organization.projects.api_keys.with_raw_response.update(
api_key_id="api_key_id",
project_id="project_id",
expires_in_seconds=1,
)

assert response.is_closed is True
Expand All @@ -96,7 +104,6 @@ def test_streaming_response_update(self, client: OpenAI) -> None:
with client.admin.organization.projects.api_keys.with_streaming_response.update(
api_key_id="api_key_id",
project_id="project_id",
expires_in_seconds=1,
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
Expand All @@ -112,14 +119,12 @@ def test_path_params_update(self, client: OpenAI) -> None:
client.admin.organization.projects.api_keys.with_raw_response.update(
api_key_id="api_key_id",
project_id="",
expires_in_seconds=1,
)

with pytest.raises(ValueError, match=r"Expected a non-empty value for `api_key_id` but received ''"):
client.admin.organization.projects.api_keys.with_raw_response.update(
api_key_id="",
project_id="project_id",
expires_in_seconds=1,
)

@parametrize
Expand Down Expand Up @@ -274,10 +279,19 @@ async def test_path_params_retrieve(self, async_client: AsyncOpenAI) -> None:

@parametrize
async def test_method_update(self, async_client: AsyncOpenAI) -> None:
api_key = await async_client.admin.organization.projects.api_keys.update(
api_key_id="api_key_id",
project_id="project_id",
)
assert_matches_type(ProjectAPIKey, api_key, path=["response"])

@parametrize
async def test_method_update_with_all_params(self, async_client: AsyncOpenAI) -> None:
api_key = await async_client.admin.organization.projects.api_keys.update(
api_key_id="api_key_id",
project_id="project_id",
expires_in_seconds=1,
scopes=["api.all.read"],
)
assert_matches_type(ProjectAPIKey, api_key, path=["response"])

Expand All @@ -286,7 +300,6 @@ async def test_raw_response_update(self, async_client: AsyncOpenAI) -> None:
response = await async_client.admin.organization.projects.api_keys.with_raw_response.update(
api_key_id="api_key_id",
project_id="project_id",
expires_in_seconds=1,
)

assert response.is_closed is True
Expand All @@ -299,7 +312,6 @@ async def test_streaming_response_update(self, async_client: AsyncOpenAI) -> Non
async with async_client.admin.organization.projects.api_keys.with_streaming_response.update(
api_key_id="api_key_id",
project_id="project_id",
expires_in_seconds=1,
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
Expand All @@ -315,14 +327,12 @@ async def test_path_params_update(self, async_client: AsyncOpenAI) -> None:
await async_client.admin.organization.projects.api_keys.with_raw_response.update(
api_key_id="api_key_id",
project_id="",
expires_in_seconds=1,
)

with pytest.raises(ValueError, match=r"Expected a non-empty value for `api_key_id` but received ''"):
await async_client.admin.organization.projects.api_keys.with_raw_response.update(
api_key_id="",
project_id="project_id",
expires_in_seconds=1,
)

@parametrize
Expand Down
Loading