Simplified limited permissions for user/time period

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 :grin:
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:
image

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!

  1. It’s a bit of a mess, but it works just fine!
  2. 100% HA, no external service required
  3. Every LUser has its own code for alarm unlock

cons :frowning:

  1. It’s a bit of a mess, but it works just fine!
  2. lots of copy-paste to add a new LUser
  3. (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
  4. 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 :wink:

Unfortunately no one can read your config. You quoted it rather than formatting it. See: https://community.home-assistant.io/t/how-to-help-us-help-you-or-how-to-ask-a-good-question/114371#oneone-format-it-properly-16

1 Like

Formatting fixed. Thanks @tom_l