Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Omit API keys from pickled StripeObjects
semver_level: major
---
StripeObject pickles no longer preserve API keys. Applications making object-based requests after cache or queue round trips must supply credentials, either through global configuration or per call, e.g. `customer.refresh(api_key=api_key)`. `refresh()`, `refresh_async()`, `save()`, and `auto_paging_iter()` on list and search result objects now accept an `api_key` keyword argument for this. This is a breaking authentication change. See the [v17 migration guide](https://github.com/stripe/stripe-python/blob/master/.hark/migration-guides/v17.md#api-keys-are-no-longer-preserved-in-pickled-stripeobjects).
27 changes: 27 additions & 0 deletions .hark/migration-guides/v17.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Migration guide for v17
## API keys are no longer preserved in pickled StripeObjects
This change affects integrations that persist StripeObjects in caches or queues and then make requests through the restored objects.

New pickles omit API keys from StripeObjects, including nested objects. Restored objects keep their connected-account routing and API version. Before making object-based requests, pass a key from secure configuration in the method call:
```python
# Before
customer = pickle.loads(trusted_cached_bytes)
customer.refresh()
# After
customer = pickle.loads(trusted_cached_bytes)
customer.refresh(api_key=api_key)
customer.save(api_key=api_key)

customers = pickle.loads(trusted_cached_list_bytes)
for customer in customers.auto_paging_iter(api_key=api_key):
...
```
`refresh()`, `refresh_async()`, `save()`, and `auto_paging_iter()` on `ListObject`, `SearchResultObject`, and their V2 equivalents now accept an `api_key` keyword argument for this. Other instance methods, such as `delete()` and resource actions like `cancel()`, already accept request options like `api_key`. Passing a key does not change `stripe.api_key`.

### Global fallback
Plain `pickle.loads()` retains the existing fallback to global API-key configuration. If that is the intended credential, configuring it before making requests is an alternative to passing a key per call:
```python
stripe.api_key = api_key
customer = pickle.loads(trusted_cached_bytes)
customer.refresh()
```
21 changes: 15 additions & 6 deletions stripe/_api_resource.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from typing_extensions import Literal, Self, deprecated
from typing_extensions import Literal, Self, Unpack, deprecated

from stripe._error import InvalidRequestError
from stripe._stripe_object import StripeObject
from stripe._request_options import extract_options_from_dict
from stripe._request_options import (
RequestOptions,
extract_options_from_dict,
)
from stripe._api_mode import ApiMode
from stripe._base_address import BaseAddress
from stripe._api_requestor import _APIRequestor
Expand Down Expand Up @@ -33,12 +36,18 @@ def retrieve(cls, id, **params) -> T:
instance.refresh()
return cast(T, instance)

def refresh(self) -> Self:
return self._request_and_refresh("get", self.instance_url())
def refresh(self, **params: Unpack[RequestOptions]) -> Self:
return self._request_and_refresh(
"get",
self.instance_url(),
params={**self._retrieve_params, **params},
)

async def refresh_async(self) -> Self:
async def refresh_async(self, **params: Unpack[RequestOptions]) -> Self:
return await self._request_and_refresh_async(
"get", self.instance_url()
"get",
self.instance_url(),
params={**self._retrieve_params, **params},
)

@classmethod
Expand Down
22 changes: 13 additions & 9 deletions stripe/_list_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,15 @@ def __len__(self) -> int:
def __reversed__(self) -> Iterator[T]:
return getattr(self, "data", []).__reversed__()

def auto_paging_iter(self) -> AnyIterator[T]:
def auto_paging_iter(
self, **params: Unpack[RequestOptions]
) -> AnyIterator[T]:
return AnyIterator(
self._auto_paging_iter(),
self._auto_paging_iter_async(),
self._auto_paging_iter(params),
self._auto_paging_iter_async(params),
)

def _auto_paging_iter(self) -> Iterator[T]:
def _auto_paging_iter(self, params: RequestOptions) -> Iterator[T]:
page = self

while True:
Expand All @@ -132,16 +134,18 @@ def _auto_paging_iter(self) -> Iterator[T]:
):
for item in reversed(page):
yield item
page = page.previous_page()
page = page.previous_page(**params)
else:
for item in page:
yield item
page = page.next_page()
page = page.next_page(**params)

if page.is_empty:
break

async def _auto_paging_iter_async(self) -> AsyncIterator[T]:
async def _auto_paging_iter_async(
self, params: RequestOptions
) -> AsyncIterator[T]:
page = self

while True:
Expand All @@ -151,11 +155,11 @@ async def _auto_paging_iter_async(self) -> AsyncIterator[T]:
):
for item in reversed(page):
yield item
page = await page.previous_page_async()
page = await page.previous_page_async(**params)
else:
for item in page:
yield item
page = await page.next_page_async()
page = await page.next_page_async(**params)

if page.is_empty:
break
Expand Down
17 changes: 11 additions & 6 deletions stripe/_search_result_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,29 +86,34 @@ def __iter__(self) -> Iterator[T]:
def __len__(self) -> int:
return getattr(self, "data", []).__len__()

def _auto_paging_iter(self) -> Iterator[T]:
def _auto_paging_iter(self, params: RequestOptions) -> Iterator[T]:
page = self

while True:
for item in page:
yield item
page = page.next_search_result_page()
page = page.next_search_result_page(**params)

if page.is_empty:
break

def auto_paging_iter(self) -> AnyIterator[T]:
def auto_paging_iter(
self, **params: Unpack[RequestOptions]
) -> AnyIterator[T]:
return AnyIterator(
self._auto_paging_iter(), self._auto_paging_iter_async()
self._auto_paging_iter(params),
self._auto_paging_iter_async(params),
)

async def _auto_paging_iter_async(self) -> AsyncIterator[T]:
async def _auto_paging_iter_async(
self, params: RequestOptions
) -> AsyncIterator[T]:
page = self

while True:
for item in page:
yield item
page = await page.next_search_result_page_async()
page = await page.next_search_result_page_async(**params)

if page.is_empty:
break
Expand Down
2 changes: 1 addition & 1 deletion stripe/_stripe_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ def __reduce__(self) -> Tuple[Any, ...]:
type(self), # callable
( # args
getattr(self, "id", None),
self.api_key,
None, # Never persist SDK authentication credentials.
self.stripe_version,
self.stripe_account,
),
Expand Down
3 changes: 2 additions & 1 deletion stripe/_updateable_api_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ def modify(cls, sid, **params) -> T:
@deprecated(
"The `save` method is deprecated and will be removed in a future major version of the library. Use the class method `modify` on the resource instead."
)
def save(self, idempotency_key=None):
def save(self, idempotency_key=None, *, api_key=None):
updated_params = self.serialize(None)
if updated_params:
updated_params["idempotency_key"] = idempotency_key
updated_params["api_key"] = api_key
self._request_and_refresh(
"post",
self.instance_url(),
Expand Down
19 changes: 14 additions & 5 deletions stripe/v2/_list_object.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from typing_extensions import Unpack

from stripe._any_iterator import AnyIterator
from stripe._request_options import RequestOptions
from stripe._stripe_object import StripeObject
from typing import AsyncIterator, Iterator, List, Optional, TypeVar, Generic

Expand Down Expand Up @@ -41,13 +44,15 @@ def __len__(self):
def __reversed__(self):
return getattr(self, "data", []).__reversed__()

def auto_paging_iter(self) -> AnyIterator[T]:
def auto_paging_iter(
self, **params: Unpack[RequestOptions]
) -> AnyIterator[T]:
return AnyIterator(
self._auto_paging_iter(),
self._auto_paging_iter_async(),
self._auto_paging_iter(params),
self._auto_paging_iter_async(params),
)

def _auto_paging_iter(self) -> Iterator[T]:
def _auto_paging_iter(self, params: RequestOptions) -> Iterator[T]:
page = self.data
next_page_url = self.next_page_url
while True:
Expand All @@ -59,13 +64,16 @@ def _auto_paging_iter(self) -> Iterator[T]:
result = self._request(
"get",
next_page_url,
params={**self._retrieve_params, **params},
base_address="api",
)
assert isinstance(result, ListObject)
page = result.data
next_page_url = result.next_page_url

async def _auto_paging_iter_async(self) -> AsyncIterator[T]:
async def _auto_paging_iter_async(
self, params: RequestOptions
) -> AsyncIterator[T]:
page = self.data
next_page_url = self.next_page_url
while True:
Expand All @@ -77,6 +85,7 @@ async def _auto_paging_iter_async(self) -> AsyncIterator[T]:
result = await self._request_async(
"get",
next_page_url,
params={**self._retrieve_params, **params},
base_address="api",
)
assert isinstance(result, ListObject)
Expand Down
20 changes: 14 additions & 6 deletions stripe/v2/_search_result_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
TypeVar,
)

from typing_extensions import Unpack

from stripe._any_iterator import AnyIterator
from stripe._request_options import RequestOptions
from stripe._stripe_object import StripeObject


Expand All @@ -32,9 +35,12 @@ def __iter__(self) -> Iterator[T]:
def __len__(self) -> int:
return getattr(self, "data", []).__len__()

def auto_paging_iter(self) -> AnyIterator[T]:
def auto_paging_iter(
self, **params: Unpack[RequestOptions]
) -> AnyIterator[T]:
return AnyIterator(
self._auto_paging_iter(), self._auto_paging_iter_async()
self._auto_paging_iter(params),
self._auto_paging_iter_async(params),
)

def _original_params(self) -> Mapping[str, Any]:
Expand All @@ -45,7 +51,7 @@ def _body_params(self, params: Mapping[str, Any]) -> Mapping[str, Any]:
body_params.pop("limit", None)
return body_params

def _auto_paging_iter(self) -> Iterator[T]:
def _auto_paging_iter(self, options: RequestOptions) -> Iterator[T]:
page: SearchResultObject[T] = self
params = self._original_params()
while True:
Expand All @@ -56,13 +62,15 @@ def _auto_paging_iter(self) -> Iterator[T]:
result = self._request(
"post",
page.next_page_url,
params=self._body_params(params),
params={**self._body_params(params), **options},
base_address="api",
)
assert isinstance(result, SearchResultObject)
page = result

async def _auto_paging_iter_async(self) -> AsyncIterator[T]:
async def _auto_paging_iter_async(
self, options: RequestOptions
) -> AsyncIterator[T]:
page: SearchResultObject[T] = self
params = self._original_params()
while True:
Expand All @@ -73,7 +81,7 @@ async def _auto_paging_iter_async(self) -> AsyncIterator[T]:
result = await self._request_async(
"post",
page.next_page_url,
params=self._body_params(params),
params={**self._body_params(params), **options},
base_address="api",
)
assert isinstance(result, SearchResultObject)
Expand Down
Loading
Loading