mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 06:07:17 +00:00
Update the Google Photos integration to limit scope to Home Assistant created content (#126398)
This commit is contained in:
parent
556deb4f77
commit
505fb3738f
@ -6,12 +6,9 @@ OAUTH2_AUTHORIZE = "https://accounts.google.com/o/oauth2/v2/auth"
|
|||||||
OAUTH2_TOKEN = "https://oauth2.googleapis.com/token"
|
OAUTH2_TOKEN = "https://oauth2.googleapis.com/token"
|
||||||
|
|
||||||
UPLOAD_SCOPE = "https://www.googleapis.com/auth/photoslibrary.appendonly"
|
UPLOAD_SCOPE = "https://www.googleapis.com/auth/photoslibrary.appendonly"
|
||||||
READ_SCOPES = [
|
READ_SCOPE = "https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata"
|
||||||
"https://www.googleapis.com/auth/photoslibrary.readonly",
|
|
||||||
"https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata",
|
|
||||||
]
|
|
||||||
OAUTH2_SCOPES = [
|
OAUTH2_SCOPES = [
|
||||||
*READ_SCOPES,
|
READ_SCOPE,
|
||||||
UPLOAD_SCOPE,
|
UPLOAD_SCOPE,
|
||||||
"https://www.googleapis.com/auth/userinfo.profile",
|
"https://www.googleapis.com/auth/userinfo.profile",
|
||||||
]
|
]
|
||||||
|
@ -19,11 +19,10 @@ from homeassistant.components.media_source import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import GooglePhotosConfigEntry
|
from . import GooglePhotosConfigEntry
|
||||||
from .const import DOMAIN, READ_SCOPES
|
from .const import DOMAIN, READ_SCOPE
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
MAX_RECENT_PHOTOS = 100
|
|
||||||
MEDIA_ITEMS_PAGE_SIZE = 100
|
MEDIA_ITEMS_PAGE_SIZE = 100
|
||||||
ALBUM_PAGE_SIZE = 50
|
ALBUM_PAGE_SIZE = 50
|
||||||
|
|
||||||
@ -38,16 +37,12 @@ class SpecialAlbumDetails:
|
|||||||
path: str
|
path: str
|
||||||
title: str
|
title: str
|
||||||
list_args: dict[str, Any]
|
list_args: dict[str, Any]
|
||||||
max_photos: int | None
|
|
||||||
|
|
||||||
|
|
||||||
class SpecialAlbum(Enum):
|
class SpecialAlbum(Enum):
|
||||||
"""Special Album types."""
|
"""Special Album types."""
|
||||||
|
|
||||||
RECENT = SpecialAlbumDetails("recent", "Recent Photos", {}, MAX_RECENT_PHOTOS)
|
UPLOADED = SpecialAlbumDetails("uploaded", "Uploaded", {})
|
||||||
FAVORITE = SpecialAlbumDetails(
|
|
||||||
"favorites", "Favorite Photos", {"favorites": True}, None
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def of(cls, path: str) -> Self | None:
|
def of(cls, path: str) -> Self | None:
|
||||||
@ -247,12 +242,6 @@ class GooglePhotosMediaSource(MediaSource):
|
|||||||
**list_args, page_size=MEDIA_ITEMS_PAGE_SIZE
|
**list_args, page_size=MEDIA_ITEMS_PAGE_SIZE
|
||||||
):
|
):
|
||||||
media_items.extend(media_item_result.media_items)
|
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:
|
except GooglePhotosApiError as err:
|
||||||
raise BrowseError(f"Error listing media items: {err}") from err
|
raise BrowseError(f"Error listing media items: {err}") from err
|
||||||
|
|
||||||
@ -270,7 +259,7 @@ class GooglePhotosMediaSource(MediaSource):
|
|||||||
entries = []
|
entries = []
|
||||||
for entry in self.hass.config_entries.async_loaded_entries(DOMAIN):
|
for entry in self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||||
scopes = entry.data["token"]["scope"].split(" ")
|
scopes = entry.data["token"]["scope"].split(" ")
|
||||||
if any(scope in scopes for scope in READ_SCOPES):
|
if READ_SCOPE in scopes:
|
||||||
entries.append(entry)
|
entries.append(entry)
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
@ -92,8 +92,7 @@ async def test_full_flow(
|
|||||||
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||||
"&redirect_uri=https://example.com/auth/external/callback"
|
"&redirect_uri=https://example.com/auth/external/callback"
|
||||||
f"&state={state}"
|
f"&state={state}"
|
||||||
"&scope=https://www.googleapis.com/auth/photoslibrary.readonly"
|
"&scope=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/photoslibrary.appendonly"
|
||||||
"+https://www.googleapis.com/auth/userinfo.profile"
|
"+https://www.googleapis.com/auth/userinfo.profile"
|
||||||
"&access_type=offline&prompt=consent"
|
"&access_type=offline&prompt=consent"
|
||||||
@ -121,8 +120,7 @@ async def test_full_flow(
|
|||||||
"refresh_token": FAKE_REFRESH_TOKEN,
|
"refresh_token": FAKE_REFRESH_TOKEN,
|
||||||
"type": "Bearer",
|
"type": "Bearer",
|
||||||
"scope": (
|
"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/photoslibrary.appendonly"
|
||||||
" https://www.googleapis.com/auth/userinfo.profile"
|
" 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}"
|
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||||
"&redirect_uri=https://example.com/auth/external/callback"
|
"&redirect_uri=https://example.com/auth/external/callback"
|
||||||
f"&state={state}"
|
f"&state={state}"
|
||||||
"&scope=https://www.googleapis.com/auth/photoslibrary.readonly"
|
"&scope=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/photoslibrary.appendonly"
|
||||||
"+https://www.googleapis.com/auth/userinfo.profile"
|
"+https://www.googleapis.com/auth/userinfo.profile"
|
||||||
"&access_type=offline&prompt=consent"
|
"&access_type=offline&prompt=consent"
|
||||||
@ -203,8 +200,7 @@ async def test_general_exception(
|
|||||||
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||||
"&redirect_uri=https://example.com/auth/external/callback"
|
"&redirect_uri=https://example.com/auth/external/callback"
|
||||||
f"&state={state}"
|
f"&state={state}"
|
||||||
"&scope=https://www.googleapis.com/auth/photoslibrary.readonly"
|
"&scope=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/photoslibrary.appendonly"
|
||||||
"+https://www.googleapis.com/auth/userinfo.profile"
|
"+https://www.googleapis.com/auth/userinfo.profile"
|
||||||
"&access_type=offline&prompt=consent"
|
"&access_type=offline&prompt=consent"
|
||||||
@ -288,8 +284,7 @@ async def test_reauth(
|
|||||||
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||||
"&redirect_uri=https://example.com/auth/external/callback"
|
"&redirect_uri=https://example.com/auth/external/callback"
|
||||||
f"&state={state}"
|
f"&state={state}"
|
||||||
"&scope=https://www.googleapis.com/auth/photoslibrary.readonly"
|
"&scope=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/photoslibrary.appendonly"
|
||||||
"+https://www.googleapis.com/auth/userinfo.profile"
|
"+https://www.googleapis.com/auth/userinfo.profile"
|
||||||
"&access_type=offline&prompt=consent"
|
"&access_type=offline&prompt=consent"
|
||||||
@ -321,8 +316,7 @@ async def test_reauth(
|
|||||||
"refresh_token": FAKE_REFRESH_TOKEN,
|
"refresh_token": FAKE_REFRESH_TOKEN,
|
||||||
"type": "Bearer",
|
"type": "Bearer",
|
||||||
"scope": (
|
"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/photoslibrary.appendonly"
|
||||||
" https://www.googleapis.com/auth/userinfo.profile"
|
" https://www.googleapis.com/auth/userinfo.profile"
|
||||||
),
|
),
|
||||||
|
@ -66,8 +66,7 @@ async def test_no_read_scopes(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("album_path", "expected_album_title"),
|
("album_path", "expected_album_title"),
|
||||||
[
|
[
|
||||||
(f"{CONFIG_ENTRY_ID}/a/recent", "Recent Photos"),
|
(f"{CONFIG_ENTRY_ID}/a/uploaded", "Uploaded Photos"),
|
||||||
(f"{CONFIG_ENTRY_ID}/a/favorites", "Favorite Photos"),
|
|
||||||
(f"{CONFIG_ENTRY_ID}/a/album-media-id-1", "Album title"),
|
(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.identifier == CONFIG_ENTRY_ID
|
||||||
assert browse.title == "Account Name"
|
assert browse.title == "Account Name"
|
||||||
assert [(child.identifier, child.title) for child in browse.children] == [
|
assert [(child.identifier, child.title) for child in browse.children] == [
|
||||||
(f"{CONFIG_ENTRY_ID}/a/recent", "Recent Photos"),
|
(f"{CONFIG_ENTRY_ID}/a/uploaded", "Uploaded"),
|
||||||
(f"{CONFIG_ENTRY_ID}/a/favorites", "Favorite Photos"),
|
|
||||||
(f"{CONFIG_ENTRY_ID}/a/album-media-id-1", "Album title"),
|
(f"{CONFIG_ENTRY_ID}/a/album-media-id-1", "Album title"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from google_photos_library_api.model import (
|
|||||||
)
|
)
|
||||||
import pytest
|
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.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
@ -225,7 +225,7 @@ async def test_upload_service_fails_create(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("scopes"),
|
("scopes"),
|
||||||
[
|
[
|
||||||
READ_SCOPES,
|
[READ_SCOPE],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_upload_service_no_scope(
|
async def test_upload_service_no_scope(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user