Goal
In my ideal world, HA would implement a RBAC (as per WTH2 - WTH!? No RBAC - Role Based Access Control? (Users & Groups rights)).
My goal was to provide custom access to specific users, so that they could control a limited set of entities for a limited period of time.
Idea
Given that HA includes some low level policies management (ref: Permissions | Home Assistant Developer Docs ), the idea is to create template entities for my special limited users and to enable/disable them considering a validity time period.
Scenario
My house has been 100% homeassistanted
The usual: sensors, automations, buttons, helpers…
And - relevant for this thread - every entry point can be managed by Home Assistant:
- Garage door
- Entrance gate
- Main door lock (Nuki)
- Alarm system (included in HA thanks to an ESP32 wired to the I/Os available, so that I can control it completely)
From time to time, I would like to grant access to my house to my dad, my wife’s mother, or the nanny.
Granting them full access to HA is not an option… you know: with a great power comes a great responsability!
How to (a.k.a. How I do)
Step 1: user and helpers
Create a user, let’s call it LUser via HA interface.
Create start and end input datetime (actually, when the LUser can access your HA):
limitedpermission_luser_start:
name: LP LUser Inizio
has_date: true
has_time: true
limitedpermission_luser_end:
name: LP LUser Fine
has_date: true
has_time: true
Create an input boolean (the current status):
limitedpermission_luser_isvalid:
name: limitedpermission_luser_isvalid
Create an automation to manage the entities above:
- alias: Limited permissions update isvalid
id: limited_permissions_update_isvalid
trigger:
- platform: time_pattern
seconds: 10
- platform: state
entity_id: input_datetime.limitedpermission_luser_start
- platform: state
entity_id: input_datetime.limitedpermission_luser_end
action:
- if:
- condition: template
value_template: >
{{ state_attr('input_datetime.limitedpermission_luser_start', 'timestamp') < now().timestamp()
and now().timestamp() < state_attr('input_datetime.limitedpermission_luser_end', 'timestamp')}}
then:
- service: input_boolean.turn_on
entity_id: input_boolean.limitedpermission_luser_isvalid
else:
- service: input_boolean.turn_off
entity_id: input_boolean.limitedpermission_luser_isvalid
Step 3: Create the “limited” entities
Add a template alarm control panel for LUser:
- platform: template
panels:
limitedpermission_luser_alarmcontrolpanel:
value_template: >
{% if is_state("input_boolean.limitedpermission_luser_isvalid", "off") -%}
unavailable
{%- elif is_state("alarm_control_panel.allarme_casina", "pending") -%}
pending
{%- elif is_state("binary_sensor.state_is_alarm_armed", "on") -%}
armed_away
{%- elif is_state("binary_sensor.state_is_alarm_armed", "off") -%}
disarmed
{%- else -%}
pending
{%- endif %}
arm_away:
event: limitedpermission_event
event_data:
name: arm_away
type: alarm_control
username: "luser"
disarm:
- event: limitedpermission_event
event_data:
name: attempt_disarm
type: alarm_control
username: "luser"
code: "{{code|string()}}"
- condition: template
value_template: '{{ (code|string()) == "1234" }}'
- event: limitedpermission_event
event_data:
name: disarm
type: alarm_control
username: "luser"
Please note:
- the LUser code for the alarm system is set here
- if the LUser is not in a valid time period, the alarm system status is unavailable
- I am only interested in arm_away and disarm, you can always managed the missing states
Now create two locks, one for the entrance gate, one for the main door:
- platform: template
unique_id: limitedpermission_luser_entrancegate
value_template: >
{% if is_state("input_boolean.limitedpermission_luser_isvalid", "off") -%}
unavailable
{%- else -%}
{{ is_state('binary_sensor.aqara_contact_entrancegate', 'off') }}
{%- endif %}
unlock:
event: limitedpermission_event
event_data:
name: unlock
type: entrance_gate
username: "luser"
lock:
event: limitedpermission_event
event_data:
name: lock
type: entrance_gate
username: "luser"
- platform: template
unique_id: limitedpermission_luser_kitchendoor
value_template: >
{% if is_state("input_boolean.limitedpermission_luser_isvalid", "off") -%}
unavailable
{%- else -%}
{{ states('lock.nuki_porta_casina_lock') }}
{%- endif %}
unlock:
event: limitedpermission_event
event_data:
name: unlock
type: kitchen_door
username: "luser"
lock:
event: limitedpermission_event
event_data:
name: lock
type: kitchen_door
username: "luser"
Step 4: LUser commands
Commands and action for the LUser’s entities are sent via events.
Events are managed by the following automation:
- alias: Limited permissions event catchall
id: limited_permissions_event_catchall
mode: queued
trigger:
- platform: event
event_type: "limitedpermission_event"
condition:
or:
- and:
- condition: template
value_template: "{{ trigger.event.data.username == 'luser' }}"
- condition: state
entity_id: input_boolean.limitedpermission_luser_isvalid
state: "on"
action:
- choose:
- conditions:
- condition: template
value_template: "{{ trigger.event.data.type == 'alarm_control' and trigger.event.data.name == 'arm_away' }}"
sequence:
- service: script.turn_on
data:
entity_id: script.allarme_casina_arm_away
- conditions:
- condition: template
value_template: "{{ trigger.event.data.type == 'alarm_control' and trigger.event.data.name == 'disarm' }}"
sequence:
- service: script.turn_on
data:
entity_id: script.allarme_casina_disarm
- conditions:
- condition: template
value_template: "{{ trigger.event.data.type == 'entrance_gate' and trigger.event.data.name == 'unlock' }}"
sequence:
- service: script.entrance_gate_please_open
data:
caller: "limitedpermissions automation"
- conditions:
- condition: template
value_template: "{{ trigger.event.data.type == 'kitchen_door' and trigger.event.data.name == 'lock' }}"
sequence:
- service: script.turn_on
data:
entity_id: script.nuki_please_lock
- conditions:
- condition: template
value_template: "{{ trigger.event.data.type == 'kitchen_door' and trigger.event.data.name == 'unlock' }}"
sequence:
- service: script.turn_on
data:
entity_id: script.nuki_please_unlock
Step 5: permissions
Edit the file .storage/auth to give LUser permissions only on the created entities.
First, look for the LUser record, and change group_ids to a special custom group (“limitedpermissions-luser” in this example):
{
"id": "bd2233e4b2f04f328107ccdf6ff584e6",
"group_ids": [
"limitedpermissions-luser"
],
"is_owner": false,
"is_active": true,
"name": "luser",
"system_generated": false,
"local_only": false
}
Then, edit this group to include only the required entities:
{
"id": "limitedpermissions-luser",
"name": "limitedpermissions luser",
"policy": {
"entities": {
"entity_ids": {
"input_datetime.limitedpermission_luser_end": {
"read": true
},
"input_datetime.limitedpermission_luser_start": {
"read": true
},
"input_boolean.limitedpermission_luser_isvalid": {
"read": true
},
"alarm_control_panel.limitedpermission_luser_alarmcontrolpanel": true,
"input_text.debug_output": true,
"lock.cancello_di_ingresso_luser": true,
"lock.porta_di_ingresso_luser": true
}
}
}
},
Finally, create a dashboard specific for your LUser:
title: Casina
views:
- theme: Backend-selected
title: Luser
path: luser
badges: []
cards:
- type: conditional
conditions:
- entity: input_boolean.limitedpermission_luser_isvalid
state: 'off'
card:
type: vertical-stack
cards:
- type: custom:mushroom-entity-card
entity: input_boolean.limitedpermission_luser_isvalid
name: Accesso scaduto
layout: vertical
primary_info: name
secondary_info: none
icon: mdi:account-alert
icon_color: red
- type: conditional
conditions:
- entity: input_boolean.limitedpermission_luser_isvalid
state: 'on'
card:
type: vertical-stack
cards:
- type: custom:mushroom-entity-card
entity: input_boolean.limitedpermission_luser_isvalid
name: Accesso valido
layout: vertical
primary_info: name
secondary_info: none
icon: mdi:account-check
icon_color: green
- type: horizontal-stack
cards:
- type: custom:mushroom-lock-card
entity: lock.cancello_di_ingresso_luser
icon: mdi:gate-alert
hold_action:
action: toggle
double_tap_action:
action: none
secondary_info: none
name: Cancello di ingresso
tap_action:
action: none
layout: vertical
- type: custom:mushroom-lock-card
entity: lock.porta_di_ingresso_luser
icon: mdi:door-closed-lock
hold_action:
action: toggle
double_tap_action:
action: none
layout: vertical
name: Porta di ingresso
secondary_info: none
tap_action:
action: none
- type: alarm-panel
states:
- arm_away
entity: alarm_control_panel.limitedpermission_luser_alarmcontrolpanel
name: Allarme
When the user is disabled (i.e.: the current time is not included in the validity time period for this user), the user will get a simple “access expired” message:
When the user is active, he get a simplified yet functional dashboard:
Possible enhancements
Calendar automation
You can use the calendar integration to give the LUser access creating an appointament with some reasonable title/property.
This is the automation I use to handle the nanny user, and granting access 20 minutes before the appointment:
- alias: Calendar Casine - Betty
id: calendar_casine_betty_permissions
mode: queued
trigger:
- platform: calendar
event: start
entity_id: calendar.casine
offset: -00:20:00
- platform: calendar
event: end
entity_id: calendar.casine
condition:
- condition: template
value_template: "{{ 'Betty' in trigger.calendar_event.summary }}"
action:
- if:
- "{{ trigger.event == 'start' }}"
then:
- service: input_text.set_value
data:
entity_id: input_text.debug_output
value: "Calendar Casine - Betty start: {{trigger.calendar_event.summary}} from {{trigger.calendar_event.start}} to {{trigger.calendar_event.end}}"
- service: input_datetime.set_datetime
target:
entity_id: "input_datetime.limitedpermission_tata_elisabetta_start"
data:
timestamp: "{{ now().timestamp() }}"
- service: input_datetime.set_datetime
target:
entity_id: "input_datetime.limitedpermission_tata_elisabetta_end"
data:
timestamp: "{{ as_timestamp(trigger.calendar_event.end) + (60 * 15)}}"
else:
- service: input_text.set_value
data:
entity_id: input_text.debug_output
value: "Calendar Casine - Betty end: {{trigger.calendar_event.summary}} from {{trigger.calendar_event.start}} to {{trigger.calendar_event.end}}"
Telegram “very simplified” board
Just an idea, not implemented that yet. But moving the interaction from HA dashboard to a telegram bot, could make the required access to a specific dashboard… useless.
PROs and CONs
pros!
- It’s a bit of a mess, but it works just fine!
- 100% HA, no external service required
- Every LUser has its own code for alarm unlock
cons
- It’s a bit of a mess, but it works just fine!
- lots of copy-paste to add a new LUser
- (actually the worst, in my hopinion) you have to set the created dashboard as default on your user app/website, otherwise it goes to the default one where you see the real entities… all broken
- as soon as HA implements some real RBAC, all this will become useless in no time
Conclusion
I have used this approach in my scenario and solved one of my biggest problem.
While this way is quite tricky, seems to be flexible and safe enough… until some real solution will be available in HA.
I hope this help someone!
Please feel free to report any error/missing part, I will try my best to fix them asap