8.8 KiB
title |
---|
Permissions |
:::info This is an experimental feature that is not enabled or enforced yet :::
Permissions limit the things a user has access to or can control. Permissions are attached to groups, of which a user can be a member. The combined permissions of all groups a user is a member of decides what a user can and cannot see or control.
Permissions do not apply to the user that is flagged as "owner". This user will always have access to everything.
General permission structure
Policies are dictionaries that at the root level consist of different categories of permissions. In the current implementation this is limited to just entities.
{
"entities": {
# …
}
}
Each category can further split into subcategories that describe parts of that category.
{
"entities": {
"domains": {
# …
},
"entity_ids": {
# …
},
}
}
If a category is omitted, the user will not have permission to that category.
When defining a policy, any dictionary value at any place can be replaced with True
or None
. True
means that permission is granted and None
means use default, which is deny access.
Entities
Entity permissions can be set on a per entity and per domain basis using the subcategories entity_ids
, device_ids
, area_ids
and domains
. You can either grant all access by setting the value to True
, or you can specify each entity individually using the "read", "control", "edit" permissions.
The system will return the first matching result, based on the order: entity_ids
, device_ids
, area_ids
, domains
, all
.
{
"entities": {
"domains": {
"switch": true
},
"entity_ids": {
"light.kitchen": {
"read": true,
"control": true
}
}
}
}
Merging policies
If a user is a member of multiple groups, the groups permission policies will be combined into a single policy at runtime. When merging policies, we will look at each level of the dictionary and compare the values for each source using the following methodology:
- If any of the values is
True
, the merged value becomesTrue
. - If any value is a dictionary, the merged value becomes a dictionary created by recursively checking each value using this methodology.
- If all values are
None
, the merged value becomesNone
.
Let's look at an example:
{
"entities": {
"entity_ids": {
"light.kitchen": True
}
}
}
{
"entities": {
"entity_ids": True
}
}
Once merged becomes
{
"entities": {
"entity_ids": True
}
}
Checking permissions
We currently have two different permission checks: can the user do the read/control/edit operation on an entity, and is the user an admin and thus allowed to change this configuration setting.
Certain APIs will always be accessible to all users, but might offer a limited scope based on the permissions, like rendering a template.
Checking permissions
To check a permission, you will need to have access to the user object. Once you have the user object, checking the permission is easy.
from homeassistant.exceptions import Unauthorized
from homeassistant.permissions.const import POLICY_READ, POLICY_CONTROL, POLICY_EDIT
# Raise error if user is not an admin
if not user.is_admin:
raise Unauthorized()
# Raise error if user does not have access to control an entity
# Available policies: POLICY_READ, POLICY_CONTROL, POLICY_EDIT
if not user.permissions.check_entity(entity_id, POLICY_CONTROL):
raise Unauthorized()
The context object
All service actions, fired events and states in Home Assistant have a context object. This object allows us to attribute changes to events and actions. These context objects also contain a user id, which is used for checking the permissions.
It's crucial for permission checking that actions taken on behalf of the user are done with a context containing the user ID. If you are in a service action handler, you should re-use the incoming context call.context
. If you are inside a WebSocket API or Rest API endpoint, you should create a context with the correct user:
from homeassistant.core import Context
await hass.services.async_call(
"homeassistant", "stop", context=Context(user_id=user.id), blocking=True
)
If a permission check fails
When you detect an unauthorized action, you should raise the homeassistant.exceptions.Unauthorized
exception. This exception will cancel the current action and notifies the user that their action is unauthorized.
The Unauthorized
exception has various parameters, to identify the permission check that failed. All fields are optional.
| # Not all actions have an ID (like adding config entry) | # We then use this fallback to know what category was unauth
Parameter | Description |
---|---|
context | The context of the current call. |
user_id | The user ID that we tried to operate on. |
entity_id | The entity ID that we tried to operate on. |
config_entry_id | The config entry ID that we tried to operate on. |
perm_category | The permission category that we tested. Only necessary if we don't have an object ID that the user tried to operate on (like when we create a config entry). |
permission | The permission that we tested, ie POLICY_READ . |
Securing a service action handler
Actions allow a user to control entities or with the integration as a whole. A service action uses the attached context to see which user invoked the command. Because context is used, it is important that you also pass the call context to all service action.
All service actions that are registered via the entity component (component.async_register_entity_service()
) will automatically have their permissions checked.
Checking entity permissions
Your service action handler will need to check the permissions for each entity that it will act on.
from homeassistant.exceptions import Unauthorized, UnknownUser
from homeassistant.auth.permissions.const import POLICY_CONTROL
async def handle_entity_service(call):
"""Handle a service action call."""
entity_ids = call.data["entity_id"]
for entity_id in entity_ids:
if call.context.user_id:
user = await hass.auth.async_get_user(call.context.user_id)
if user is None:
raise UnknownUser(
context=call.context,
entity_id=entity_id,
permission=POLICY_CONTROL,
)
if not user.permissions.check_entity(entity_id, POLICY_CONTROL):
raise Unauthorized(
context=call.context,
entity_id=entity_id,
permission=POLICY_CONTROL,
)
# Do action on entity
async def async_setup(hass, config):
hass.services.async_register(DOMAIN, "my_service", handle_entity_service)
return True
Checking admin permission
Starting Home Assistant 0.90, there is a special decorator to help protect service actions that require admin access.
# New in Home Assistant 0.90
async def handle_admin_service(call):
"""Handle a service action call."""
# Do admin action
async def async_setup(hass, config):
hass.helpers.service.async_register_admin_service(
DOMAIN, "my_service", handle_admin_service, vol.Schema({})
)
return True
Securing a REST API endpoint
from homeassistant.core import Context
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.exceptions import Unauthorized
class MyView(HomeAssistantView):
"""View to handle Status requests."""
url = "/api/my-component/my-api"
name = "api:my-component:my-api"
async def post(self, request):
"""Notify that the API is running."""
hass = request.app["hass"]
user = request["hass_user"]
if not user.is_admin:
raise Unauthorized()
hass.bus.async_fire(
"my-component-api-running", context=Context(user_id=user.id)
)
return self.json_message("Done.")
Securing a Websocket API endpoint
Verifying permissions in a Websocket API endpoint can be done by accessing the
user via connection.user
. If you need to check admin access, you can use the
built-in @require_admin
decorator.
from homeassistant.components import websocket_api
async def async_setup(hass, config):
websocket_api.async_register_command(hass, websocket_create)
return True
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
{vol.Required("type"): "my-component/my-action",}
)
async def websocket_create(hass, connection, msg):
"""Create a user."""
# Do action