1 """The Application Credentials integration.
3 This integration provides APIs for managing local OAuth credentials on behalf
4 of other integrations. Integrations register an authorization server, and then
5 the APIs are used to add one or more client credentials. Integrations may also
6 provide credentials from yaml for backwards compatibility.
9 from __future__
import annotations
11 from dataclasses
import dataclass
13 from typing
import Any, Protocol
15 import voluptuous
as vol
35 async_get_application_credentials,
36 async_get_integration,
41 __all__ = [
"ClientCredential",
"AuthorizationServer",
"async_import_client_credential"]
43 _LOGGER = logging.getLogger(__name__)
45 DOMAIN =
"application_credentials"
49 DATA_COMPONENT: HassKey[ApplicationCredentialsStorageCollection] =
HassKey(DOMAIN)
50 CONF_AUTH_DOMAIN =
"auth_domain"
51 DEFAULT_IMPORT_NAME =
"Import from configuration.yaml"
53 CREATE_FIELDS: VolDictType = {
54 vol.Required(CONF_DOMAIN): cv.string,
55 vol.Required(CONF_CLIENT_ID): vol.All(cv.string, vol.Strip),
56 vol.Required(CONF_CLIENT_SECRET): vol.All(cv.string, vol.Strip),
57 vol.Optional(CONF_AUTH_DOMAIN): cv.string,
58 vol.Optional(CONF_NAME): cv.string,
60 UPDATE_FIELDS: VolDictType = {}
62 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
67 """Represent an OAuth client credential."""
71 name: str |
None =
None
76 """Represent an OAuth2 Authorization Server."""
83 """Application credential collection stored in storage."""
85 CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
88 """Validate the config is valid."""
90 domain = result[CONF_DOMAIN]
92 raise ValueError(f
"No application_credentials platform for {domain}")
97 """Suggest an ID based on the config."""
98 return f
"{info[CONF_DOMAIN]}.{info[CONF_CLIENT_ID]}"
101 self, item: dict[str, str], update_data: dict[str, str]
103 """Return a new updated data object."""
104 raise ValueError(
"Updates not supported")
107 """Delete item, verifying credential is not in use."""
108 if item_id
not in self.data:
109 raise collection.ItemNotFound(item_id)
112 current = self.data[item_id]
113 entries = self.hass.config_entries.async_entries(current[CONF_DOMAIN])
114 for entry
in entries:
115 if entry.data.get(
"auth_implementation") == item_id:
117 f
"Cannot delete credential in use by integration {entry.domain}"
123 """Import an yaml credential if it does not already exist."""
125 if self.id_manager.has_id(
slugify(suggested_id)):
127 await self.async_create_item(info)
130 """Return ClientCredentials in storage for the specified domain."""
132 for item
in self.async_items():
133 if item[CONF_DOMAIN] != domain:
135 auth_domain = item.get(CONF_AUTH_DOMAIN, item[CONF_ID])
137 client_id=item[CONF_CLIENT_ID],
138 client_secret=item[CONF_CLIENT_SECRET],
139 name=item.get(CONF_NAME),
144 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
145 """Set up Application Credentials."""
146 hass.data[DOMAIN] = {}
148 id_manager = collection.IDManager()
150 Store(hass, STORAGE_VERSION, STORAGE_KEY),
153 await storage_collection.async_load()
154 hass.data[DATA_COMPONENT] = storage_collection
156 collection.DictStorageCollectionWebsocket(
157 storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
160 websocket_api.async_register_command(hass, handle_integration_list)
161 websocket_api.async_register_command(hass, handle_config_entry)
163 config_entry_oauth2_flow.async_add_implementation_provider(
164 hass, DOMAIN, _async_provide_implementation
173 credential: ClientCredential,
174 auth_domain: str |
None =
None,
176 """Import an existing credential from configuration.yaml."""
177 if DOMAIN
not in hass.data:
178 raise ValueError(
"Integration 'application_credentials' not setup")
181 CONF_CLIENT_ID: credential.client_id,
182 CONF_CLIENT_SECRET: credential.client_secret,
183 CONF_AUTH_DOMAIN: auth_domain
if auth_domain
else domain,
185 item[CONF_NAME] = credential.name
if credential.name
else DEFAULT_IMPORT_NAME
186 await hass.data[DATA_COMPONENT].async_import_item(item)
190 """Application Credentials local oauth2 implementation."""
196 credential: ClientCredential,
197 authorization_server: AuthorizationServer,
199 """Initialize AuthImplementation."""
203 credential.client_id,
204 credential.client_secret,
205 authorization_server.authorize_url,
206 authorization_server.token_url,
212 """Name of the implementation."""
213 return self.
_name_name
or self.client_id
217 hass: HomeAssistant, domain: str
218 ) -> list[config_entry_oauth2_flow.AbstractOAuth2Implementation]:
219 """Return registered OAuth implementations."""
225 credentials = hass.data[DATA_COMPONENT].async_client_credentials(domain)
226 if hasattr(platform,
"async_get_auth_implementation"):
228 await platform.async_get_auth_implementation(hass, auth_domain, credential)
229 for auth_domain, credential
in credentials.items()
231 authorization_server = await platform.async_get_authorization_server(hass)
234 for auth_domain, credential
in credentials.items()
240 config_entry: ConfigEntry,
242 """Return the item id of an application credential for an existing ConfigEntry."""
243 if not await
_get_platform(hass, config_entry.domain)
or not (
244 auth_domain := config_entry.data.get(
"auth_implementation")
248 for item
in hass.data[DATA_COMPONENT].async_items():
249 item_id = item[CONF_ID]
251 item[CONF_DOMAIN] == config_entry.domain
252 and item.get(CONF_AUTH_DOMAIN, item_id) == auth_domain
259 """Define the format that application_credentials platforms may have.
261 Most platforms typically just implement async_get_authorization_server, and
262 the default oauth implementation will be used. Otherwise a platform may
263 implement async_get_auth_implementation to give their use a custom
264 AbstractOAuth2Implementation.
268 self, hass: HomeAssistant
269 ) -> AuthorizationServer:
270 """Return authorization server, for the default auth implementation."""
273 self, hass: HomeAssistant, auth_domain: str, credential: ClientCredential
274 ) -> config_entry_oauth2_flow.AbstractOAuth2Implementation:
275 """Return a custom auth implementation."""
278 self, hass: HomeAssistant
280 """Return description placeholders for the credentials dialog."""
284 hass: HomeAssistant, integration_domain: str
285 ) -> ApplicationCredentialsProtocol |
None:
286 """Register an application_credentials platform."""
289 except IntegrationNotFound
as err:
290 _LOGGER.debug(
"Integration '%s' does not exist: %s", integration_domain, err)
293 platform = await integration.async_get_platform(
"application_credentials")
294 except ImportError
as err:
296 "Integration '%s' does not provide application_credentials: %s",
301 if not hasattr(platform,
"async_get_authorization_server")
and not hasattr(
302 platform,
"async_get_auth_implementation"
305 f
"Integration '{integration_domain}' platform {DOMAIN} did not implement"
306 " 'async_get_authorization_server' or 'async_get_auth_implementation'"
313 if platform
and hasattr(platform,
"async_get_description_placeholders"):
314 placeholders = await platform.async_get_description_placeholders(hass)
315 return {
"description_placeholders": placeholders}
319 @websocket_api.websocket_command(
{vol.Required("type"):
"application_credentials/config"}
321 @websocket_api.async_response
323 hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
325 """Handle integrations command."""
333 connection.send_result(msg[
"id"], result)
336 @websocket_api.websocket_command(
{
vol.Required("type"):
"application_credentials/config_entry",
337 vol.Required(
"config_entry_id"): str,
340 @websocket_api.async_response
342 hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
344 """Return application credentials information for a config entry."""
345 entry_id = msg[
"config_entry_id"]
346 config_entry = hass.config_entries.async_get_entry(entry_id)
348 connection.send_error(
350 "invalid_config_entry_id",
351 f
"Config entry not found: {entry_id}",
358 result[
"application_credentials_id"] = application_credentials_id
359 connection.send_result(msg[
"id"], result)
360
config_entry_oauth2_flow.AbstractOAuth2Implementation async_get_auth_implementation(self, HomeAssistant hass, str auth_domain, ClientCredential credential)
dict[str, str] async_get_description_placeholders(self, HomeAssistant hass)
AuthorizationServer async_get_authorization_server(self, HomeAssistant hass)
None async_delete_item(self, str item_id)
str _get_suggested_id(self, dict[str, str] info)
None async_import_item(self, dict[str, str] info)
dict[str, str] _update_data(self, dict[str, str] item, dict[str, str] update_data)
dict[str, ClientCredential] async_client_credentials(self, str domain)
dict[str, str] _process_create_data(self, dict[str, str] data)
None __init__(self, HomeAssistant hass, str auth_domain, ClientCredential credential, AuthorizationServer authorization_server)
None handle_config_entry(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
bool async_setup(HomeAssistant hass, ConfigType config)
list[config_entry_oauth2_flow.AbstractOAuth2Implementation] _async_provide_implementation(HomeAssistant hass, str domain)
ApplicationCredentialsProtocol|None _get_platform(HomeAssistant hass, str integration_domain)
dict[str, Any] _async_integration_config(HomeAssistant hass, str domain)
None async_import_client_credential(HomeAssistant hass, str domain, ClientCredential credential, str|None auth_domain=None)
str|None _async_config_entry_app_credentials(HomeAssistant hass, ConfigEntry config_entry)
None handle_integration_list(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
list[str] async_get_application_credentials(HomeAssistant hass)
Integration async_get_integration(HomeAssistant hass, str domain)
str slugify(str|None text, *str separator="_")