From 505fb3738f51476db512b01f51c3644d3628b39e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 21 Sep 2024 10:56:13 -0700 Subject: [PATCH] Update the Google Photos integration to limit scope to Home Assistant created content (#126398) --- .../components/google_photos/const.py | 7 ++----- .../components/google_photos/media_source.py | 17 +++-------------- .../google_photos/test_config_flow.py | 18 ++++++------------ .../google_photos/test_media_source.py | 6 ++---- .../components/google_photos/test_services.py | 4 ++-- 5 files changed, 15 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/google_photos/const.py b/homeassistant/components/google_photos/const.py index c629e6feb27..9c623ed7819 100644 --- a/homeassistant/components/google_photos/const.py +++ b/homeassistant/components/google_photos/const.py @@ -6,12 +6,9 @@ OAUTH2_AUTHORIZE = "https://accounts.google.com/o/oauth2/v2/auth" OAUTH2_TOKEN = "https://oauth2.googleapis.com/token" UPLOAD_SCOPE = "https://www.googleapis.com/auth/photoslibrary.appendonly" -READ_SCOPES = [ - "https://www.googleapis.com/auth/photoslibrary.readonly", - "https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata", -] +READ_SCOPE = "https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata" OAUTH2_SCOPES = [ - *READ_SCOPES, + READ_SCOPE, UPLOAD_SCOPE, "https://www.googleapis.com/auth/userinfo.profile", ] diff --git a/homeassistant/components/google_photos/media_source.py b/homeassistant/components/google_photos/media_source.py index 63d66d5a82b..2388869d75b 100644 --- a/homeassistant/components/google_photos/media_source.py +++ b/homeassistant/components/google_photos/media_source.py @@ -19,11 +19,10 @@ from homeassistant.components.media_source import ( from homeassistant.core import HomeAssistant from . import GooglePhotosConfigEntry -from .const import DOMAIN, READ_SCOPES +from .const import DOMAIN, READ_SCOPE _LOGGER = logging.getLogger(__name__) -MAX_RECENT_PHOTOS = 100 MEDIA_ITEMS_PAGE_SIZE = 100 ALBUM_PAGE_SIZE = 50 @@ -38,16 +37,12 @@ class SpecialAlbumDetails: path: str title: str list_args: dict[str, Any] - max_photos: int | None class SpecialAlbum(Enum): """Special Album types.""" - RECENT = SpecialAlbumDetails("recent", "Recent Photos", {}, MAX_RECENT_PHOTOS) - FAVORITE = SpecialAlbumDetails( - "favorites", "Favorite Photos", {"favorites": True}, None - ) + UPLOADED = SpecialAlbumDetails("uploaded", "Uploaded", {}) @classmethod def of(cls, path: str) -> Self | None: @@ -247,12 +242,6 @@ class GooglePhotosMediaSource(MediaSource): **list_args, page_size=MEDIA_ITEMS_PAGE_SIZE ): media_items.extend(media_item_result.media_items) - if ( - special_album - and (max_photos := special_album.value.max_photos) - and len(media_items) > max_photos - ): - break except GooglePhotosApiError as err: raise BrowseError(f"Error listing media items: {err}") from err @@ -270,7 +259,7 @@ class GooglePhotosMediaSource(MediaSource): entries = [] for entry in self.hass.config_entries.async_loaded_entries(DOMAIN): scopes = entry.data["token"]["scope"].split(" ") - if any(scope in scopes for scope in READ_SCOPES): + if READ_SCOPE in scopes: entries.append(entry) return entries diff --git a/tests/components/google_photos/test_config_flow.py b/tests/components/google_photos/test_config_flow.py index 48c8723df3c..4896f82effb 100644 --- a/tests/components/google_photos/test_config_flow.py +++ b/tests/components/google_photos/test_config_flow.py @@ -92,8 +92,7 @@ async def test_full_flow( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}" - "&scope=https://www.googleapis.com/auth/photoslibrary.readonly" - "+https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata" + "&scope=https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata" "+https://www.googleapis.com/auth/photoslibrary.appendonly" "+https://www.googleapis.com/auth/userinfo.profile" "&access_type=offline&prompt=consent" @@ -121,8 +120,7 @@ async def test_full_flow( "refresh_token": FAKE_REFRESH_TOKEN, "type": "Bearer", "scope": ( - "https://www.googleapis.com/auth/photoslibrary.readonly" - " https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata" + "https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata" " https://www.googleapis.com/auth/photoslibrary.appendonly" " https://www.googleapis.com/auth/userinfo.profile" ), @@ -163,8 +161,7 @@ async def test_api_not_enabled( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}" - "&scope=https://www.googleapis.com/auth/photoslibrary.readonly" - "+https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata" + "&scope=https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata" "+https://www.googleapis.com/auth/photoslibrary.appendonly" "+https://www.googleapis.com/auth/userinfo.profile" "&access_type=offline&prompt=consent" @@ -203,8 +200,7 @@ async def test_general_exception( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}" - "&scope=https://www.googleapis.com/auth/photoslibrary.readonly" - "+https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata" + "&scope=https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata" "+https://www.googleapis.com/auth/photoslibrary.appendonly" "+https://www.googleapis.com/auth/userinfo.profile" "&access_type=offline&prompt=consent" @@ -288,8 +284,7 @@ async def test_reauth( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}" - "&scope=https://www.googleapis.com/auth/photoslibrary.readonly" - "+https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata" + "&scope=https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata" "+https://www.googleapis.com/auth/photoslibrary.appendonly" "+https://www.googleapis.com/auth/userinfo.profile" "&access_type=offline&prompt=consent" @@ -321,8 +316,7 @@ async def test_reauth( "refresh_token": FAKE_REFRESH_TOKEN, "type": "Bearer", "scope": ( - "https://www.googleapis.com/auth/photoslibrary.readonly" - " https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata" + "https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata" " https://www.googleapis.com/auth/photoslibrary.appendonly" " https://www.googleapis.com/auth/userinfo.profile" ), diff --git a/tests/components/google_photos/test_media_source.py b/tests/components/google_photos/test_media_source.py index 762a4d5ebd1..9d287998fa8 100644 --- a/tests/components/google_photos/test_media_source.py +++ b/tests/components/google_photos/test_media_source.py @@ -66,8 +66,7 @@ async def test_no_read_scopes( @pytest.mark.parametrize( ("album_path", "expected_album_title"), [ - (f"{CONFIG_ENTRY_ID}/a/recent", "Recent Photos"), - (f"{CONFIG_ENTRY_ID}/a/favorites", "Favorite Photos"), + (f"{CONFIG_ENTRY_ID}/a/uploaded", "Uploaded Photos"), (f"{CONFIG_ENTRY_ID}/a/album-media-id-1", "Album title"), ], ) @@ -109,8 +108,7 @@ async def test_browse_albums( assert browse.identifier == CONFIG_ENTRY_ID assert browse.title == "Account Name" assert [(child.identifier, child.title) for child in browse.children] == [ - (f"{CONFIG_ENTRY_ID}/a/recent", "Recent Photos"), - (f"{CONFIG_ENTRY_ID}/a/favorites", "Favorite Photos"), + (f"{CONFIG_ENTRY_ID}/a/uploaded", "Uploaded"), (f"{CONFIG_ENTRY_ID}/a/album-media-id-1", "Album title"), ] diff --git a/tests/components/google_photos/test_services.py b/tests/components/google_photos/test_services.py index 10d57e1d178..eaf7163f62b 100644 --- a/tests/components/google_photos/test_services.py +++ b/tests/components/google_photos/test_services.py @@ -11,7 +11,7 @@ from google_photos_library_api.model import ( ) import pytest -from homeassistant.components.google_photos.const import DOMAIN, READ_SCOPES +from homeassistant.components.google_photos.const import DOMAIN, READ_SCOPE from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -225,7 +225,7 @@ async def test_upload_service_fails_create( @pytest.mark.parametrize( ("scopes"), [ - READ_SCOPES, + [READ_SCOPE], ], ) async def test_upload_service_no_scope(