Skip to main content
The Home Assistant permissions system provides fine-grained access control for users based on their group memberships and policies. The system uses a policy-based approach where permissions are defined in policies attached to groups.

Architecture

Location: homeassistant/auth/permissions/__init__.py The permissions system consists of several key components:
  • Policies: Define what actions are allowed
  • Groups: Collections of users with shared policies
  • Permissions Objects: Evaluate access based on policies
  • Permission Lookup: Provides entity and device metadata for policy evaluation

Permission Classes

AbstractPermissions

Location: homeassistant/auth/permissions/__init__.py:29 Base class for all permission implementations:
class AbstractPermissions:
    """Default permissions class."""
    
    _cached_entity_func: Callable[[str, str], bool] | None = None
    
    def _entity_func(self) -> Callable[[str, str], bool]:
        """Return a function that can test entity access."""
        raise NotImplementedError
    
    def access_all_entities(self, key: str) -> bool:
        """Check if we have a certain access to all entities."""
        raise NotImplementedError
    
    def check_entity(self, entity_id: str, key: str) -> bool:
        """Check if we can access entity."""
        if (entity_func := self._cached_entity_func) is None:
            entity_func = self._cached_entity_func = self._entity_func()
        return entity_func(entity_id, key)
Access Keys:
  • "read": View entity state
  • "control": Change entity state
  • "edit": Modify entity configuration

OwnerPermissions

Location: homeassistant/auth/permissions/__init__.py:71 Special permissions object for owners with full access:
class _OwnerPermissions(AbstractPermissions):
    """Owner permissions."""
    
    def access_all_entities(self, key: str) -> bool:
        """Check if we have a certain access to all entities."""
        return True
    
    def _entity_func(self) -> Callable[[str, str], bool]:
        """Return a function that can test entity access."""
        return lambda entity_id, key: True

OwnerPermissions = _OwnerPermissions()  # Singleton instance
Owners have unrestricted access to all resources.

PolicyPermissions

Location: homeassistant/auth/permissions/__init__.py:50 Policy-based permissions for regular users:
class PolicyPermissions(AbstractPermissions):
    """Handle permissions."""
    
    def __init__(self, policy: PolicyType, perm_lookup: PermissionLookup):
        self._policy = policy
        self._perm_lookup = perm_lookup
    
    def access_all_entities(self, key: str) -> bool:
        """Check if we have a certain access to all entities."""
        return test_all(self._policy.get(CAT_ENTITIES), key)
    
    def _entity_func(self) -> Callable[[str, str], bool]:
        """Return a function that can test entity access."""
        return compile_entities(self._policy.get(CAT_ENTITIES), self._perm_lookup)
Evaluates permissions based on the merged policy from user’s groups.

Policy Structure

Location: homeassistant/auth/permissions/__init__.py:16
POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA})
Policies are dictionaries with category keys. Currently, only the entities category is supported:
{
    "entities": {
        "entity_ids": {
            "light.bedroom": {"read": true, "control": true},
            "switch.kitchen": {"read": true}
        },
        "domains": {
            "light": {"read": true, "control": true},
            "switch": {"read": true}
        },
        "areas": {
            "bedroom": {"read": true, "control": true}
        },
        "labels": {
            "personal": {"read": true, "control": true}
        },
        "all": {"read": true}  # Access to all entities
    }
}

Entity Policy Schema

Location: homeassistant/auth/permissions/entities.py
ENTITY_POLICY_SCHEMA = vol.Schema({
    vol.Optional("entity_ids"): {str: ENTITY_PERMISSIONS_SCHEMA},
    vol.Optional("domains"): {str: ENTITY_PERMISSIONS_SCHEMA},
    vol.Optional("areas"): {str: ENTITY_PERMISSIONS_SCHEMA},
    vol.Optional("labels"): {str: ENTITY_PERMISSIONS_SCHEMA},
    vol.Optional("all"): ENTITY_PERMISSIONS_SCHEMA,
})

ENTITY_PERMISSIONS_SCHEMA = vol.Schema({
    vol.Optional("read"): bool,
    vol.Optional("control"): bool,
    vol.Optional("edit"): bool,
})

Policy Selectors

Policies can specify permissions using multiple selectors:

Entity IDs

Direct entity ID matching:
"entity_ids": {
    "light.bedroom": {"read": True, "control": True},
    "climate.living_room": {"read": True},
}

Domains

Match all entities in a domain:
"domains": {
    "light": {"read": True, "control": True},
    "switch": {"read": True},
}

Areas

Match all entities in an area:
"areas": {
    "bedroom": {"read": True, "control": True},
    "garage": {"read": True},
}

Labels

Match all entities with a label:
"labels": {
    "personal": {"read": True, "control": True},
    "secure": {"read": True},
}

All Entities

Grant access to all entities:
"all": {"read": True}

Permission Lookup

Location: homeassistant/auth/permissions/models.py:14
@attr.s(slots=True)
class PermissionLookup:
    """Class to hold data for permission lookups."""
    
    entity_registry: er.EntityRegistry = attr.ib()
    device_registry: dr.DeviceRegistry = attr.ib()
The PermissionLookup provides access to entity and device registries for resolving area and label memberships during policy evaluation.

System Policies

Location: homeassistant/auth/permissions/system_policies.py Home Assistant defines several built-in system policies:

Admin Policy

Full access to all entities:
ADMIN_POLICY: PolicyType = {
    CAT_ENTITIES: {
        "all": {
            "read": True,
            "control": True,
            "edit": True,
        }
    }
}

User Policy

Read and control access to all entities:
USER_POLICY: PolicyType = {
    CAT_ENTITIES: {
        "all": {
            "read": True,
            "control": True,
            "edit": False,
        }
    }
}

Read-Only Policy

Read-only access to all entities:
READ_ONLY_POLICY: PolicyType = {
    CAT_ENTITIES: {
        "all": {
            "read": True,
            "control": False,
            "edit": False,
        }
    }
}

Policy Merging

Location: homeassistant/auth/permissions/merge.py When a user belongs to multiple groups, their policies are merged:
def merge_policies(policies: list[PolicyType]) -> PolicyType:
    """Merge multiple policies."""
    # Policies are merged using OR logic
    # If any policy grants access, access is granted
Merge strategy:
  1. Start with empty policy (no access)
  2. For each group policy, merge permissions
  3. Use OR logic: if any policy grants a permission, it’s granted
  4. More specific selectors take precedence
Example: User in two groups:
  • Group A: {"domains": {"light": {"read": True}}}
  • Group B: {"entity_ids": {"light.bedroom": {"control": True}}}
Merged policy:
{
    "domains": {"light": {"read": True}},
    "entity_ids": {"light.bedroom": {"read": True, "control": True}}
}

User Permission Access

Location: homeassistant/auth/models.py:82 Users have a cached permissions property:
@cached_property
def permissions(self) -> perm_mdl.AbstractPermissions:
    """Return permissions object for user."""
    if self.is_owner:
        return perm_mdl.OwnerPermissions
    return perm_mdl.PolicyPermissions(
        perm_mdl.merge_policies([group.policy for group in self.groups]),
        self.perm_lookup,
    )
Important: Call user.invalidate_cache() after modifying groups or policies.

Checking Permissions

Check Entity Access

# Check if user can read an entity
if user.permissions.check_entity("light.bedroom", "read"):
    # Show entity state
    pass

# Check if user can control an entity
if user.permissions.check_entity("light.bedroom", "control"):
    # Allow turning on/off
    pass

# Check if user can edit an entity
if user.permissions.check_entity("light.bedroom", "edit"):
    # Allow configuration changes
    pass

Check All Entities Access

# Check if user has read access to all entities
if user.permissions.access_all_entities("read"):
    # User can see all entity states
    pass

# Check if user has control access to all entities
if user.permissions.access_all_entities("control"):
    # User can control all entities
    pass

Owner Check

if user.is_owner:
    # Owner has full access to everything
    pass

Admin Check

Location: homeassistant/auth/models.py:92
@cached_property
def is_admin(self) -> bool:
    """Return if user is part of the admin group."""
    return self.is_owner or (
        self.is_active and any(gr.id == GROUP_ID_ADMIN for gr in self.groups)
    )

Policy Compilation

Location: homeassistant/auth/permissions/entities.py For performance, policies are compiled into efficient evaluation functions:
def compile_entities(
    policy: dict | None,
    perm_lookup: PermissionLookup
) -> Callable[[str, str], bool]:
    """Compile an entity policy into a validation function."""
    # Returns a function that efficiently checks entity access
    # Uses compiled matchers for fast evaluation
The compiled function:
  1. Checks direct entity ID matches first (fastest)
  2. Checks domain matches
  3. Looks up entity in registry for area/label matching
  4. Falls back to “all” selector

Groups and Policies

Location: homeassistant/auth/auth_store.py

Built-in Groups

Administrators (GROUP_ID_ADMIN: system-admin):
  • Policy: Full access (read, control, edit)
  • For trusted users who manage the system
Users (GROUP_ID_USER: system-users):
  • Policy: Read and control access
  • For regular users
Read Only (GROUP_ID_READ_ONLY: system-read-only):
  • Policy: Read-only access
  • For monitoring/display purposes

Creating Custom Groups

# Create a custom group with limited access
group = models.Group(
    name="Guest",
    policy={
        "entities": {
            "domains": {
                "light": {"read": True, "control": True}
            },
            "areas": {
                "living_room": {"read": True, "control": True}
            }
        }
    },
    system_generated=False
)

Assigning Users to Groups

# When creating a user
user = await auth_manager.async_create_user(
    name="Guest User",
    group_ids=[guest_group.id]
)

# When updating a user
await auth_manager.async_update_user(
    user,
    group_ids=[guest_group.id, another_group.id]
)

Permission Utilities

Location: homeassistant/auth/permissions/util.py

test_all()

def test_all(policy: dict | None, key: str) -> bool:
    """Test if a policy grants access to all entities."""
    if policy is None:
        return False
    
    all_perms = policy.get("all")
    if all_perms is None:
        return False
    
    return all_perms.get(key, False)

WebSocket API Integration

The permissions system integrates with the WebSocket API to filter results:
# In a WebSocket command handler
@websocket_api.require_admin  # Requires admin group
async def handle_command(hass, connection, msg):
    pass

# Check permissions manually
@websocket_api.websocket_command({vol.Required("type"): "get_entity"})
async def handle_get_entity(hass, connection, msg):
    entity_id = msg["entity_id"]
    
    if not connection.user.permissions.check_entity(entity_id, "read"):
        connection.send_error(
            msg["id"],
            "unauthorized",
            "Insufficient permissions"
        )
        return
    
    # Return entity data

HTTP API Integration

The HTTP API validates permissions using decorators:
from homeassistant.components.http import require_admin

class MyView(HomeAssistantView):
    @require_admin
    async def post(self, request):
        # Only admins can access
        pass

Local-Only Users

Location: homeassistant/auth/models.py:68 Users can be marked as local-only:
user = await auth_manager.async_create_user(
    name="Local User",
    local_only=True
)
Local-only users:
  • Cannot authenticate via cloud connections
  • Restricted to local network access
  • Useful for guest accounts or limited access users

Best Practices

  1. Use groups instead of per-user policies
  2. Start restrictive and grant permissions as needed
  3. Invalidate cache after permission changes:
    user.invalidate_cache()
    
  4. Check permissions before sensitive operations
  5. Use owner check sparingly - prefer granular permissions
  6. Test policy changes thoroughly before deployment
  7. Document custom policies for maintainability
  8. Use area/label selectors for logical grouping
  9. Avoid excessive entity_ids - use domains when possible
  10. Consider local_only for untrusted users

Performance Considerations

  • Permission checks are cached at the user level
  • Policy compilation happens once per user session
  • Entity function is cached after first use
  • Direct entity ID lookups are fastest
  • Registry lookups (area/label) are slower

Security Notes

  • Owner status cannot be restricted by policies
  • System-generated users have special permission handling
  • Inactive users have no effective permissions
  • Permission changes require cache invalidation
  • Policies use OR logic (permissive merging)

Example: Custom Permission Policy

# Create a policy for a smart home guest
guest_policy = {
    "entities": {
        # Can read all entities
        "all": {"read": True},
        # Can control lights and media players
        "domains": {
            "light": {"control": True},
            "media_player": {"control": True},
        },
        # Can control everything in guest bedroom
        "areas": {
            "guest_bedroom": {"control": True},
        },
        # Cannot control security devices
        "entity_ids": {
            "alarm_control_panel.home": {"read": True, "control": False},
            "lock.front_door": {"read": True, "control": False},
        },
    }
}

# Create guest group
guest_group = models.Group(
    name="Guests",
    policy=guest_policy,
    system_generated=False
)

# Assign user to guest group
user = await auth_manager.async_create_user(
    name="Guest User",
    group_ids=[guest_group.id],
    local_only=True  # Only local access
)

Build docs developers (and LLMs) love