diff --git a/API.md b/API.md index d8deb5cfc..4a9955a03 100644 --- a/API.md +++ b/API.md @@ -483,6 +483,7 @@ Get all available addons. "logo": "bool", "changelog": "bool", "hassio_api": "bool", + "hassio_role": "default|homeassistant|manager|admin", "homeassistant_api": "bool", "full_access": "bool", "protected": "bool", diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index 68a3dfcfb..b58ea9404 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -26,7 +26,7 @@ from ..const import ( ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS, - ATTR_PROTECTED, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, + ATTR_PROTECTED, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE, SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT) from ..coresys import CoreSysAttributes from ..docker.addon import DockerAddon @@ -376,6 +376,11 @@ class Addon(CoreSysAttributes): """Return True if the add-on access to Home-Assistant api proxy.""" return self._mesh[ATTR_HOMEASSISTANT_API] + @property + def hassio_role(self): + """Return Hass.io role for API.""" + return self._mesh[ATTR_HASSIO_ROLE] + @property def with_stdin(self): """Return True if the add-on access use stdin input.""" diff --git a/hassio/addons/utils.py b/hassio/addons/utils.py index 7c2e4ba31..e937e5bc1 100644 --- a/hassio/addons/utils.py +++ b/hassio/addons/utils.py @@ -6,7 +6,8 @@ import re from ..const import ( SECURITY_DISABLE, SECURITY_PROFILE, PRIVILEGED_NET_ADMIN, - PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO, PRIVILEGED_SYS_PTRACE) + PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO, PRIVILEGED_SYS_PTRACE, + ROLE_ADMIN, ROLE_MANAGER) RE_SHA1 = re.compile(r"[a-f0-9]{8}") @@ -36,6 +37,12 @@ def rating_security(addon): PRIVILEGED_SYS_RAWIO, PRIVILEGED_SYS_PTRACE): rating += -1 + # API Hass.io role + if addon.hassio_role == ROLE_MANAGER: + rating += -1 + elif addon.hassio_role == ROLE_ADMIN: + rating += -2 + # Not secure Networking if addon.host_network: rating += -1 diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 43ee5c3cf..794c00d9c 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -19,10 +19,11 @@ from ..const import ( ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_PROTECTED, - ATTR_FULL_ACCESS, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, + ATTR_FULL_ACCESS, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE, PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO, PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE, - PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE) + PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE, + ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_MANAGER, ROLE_ADMIN) from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE _LOGGER = logging.getLogger(__name__) @@ -72,6 +73,13 @@ PRIVILEGED_ALL = [ PRIVILEGED_SYS_PTRACE, ] +ROLE_ALL = [ + ROLE_DEFAULT, + ROLE_HOMEASSISTANT, + ROLE_MANAGER, + ROLE_ADMIN, +] + BASE_IMAGE = { ARCH_ARMHF: "homeassistant/armhf-base:latest", ARCH_AARCH64: "homeassistant/aarch64-base:latest", @@ -121,6 +129,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Optional(ATTR_GPIO, default=False): vol.Boolean(), vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(), vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(), + vol.Optional(ATTR_HASSIO_ROLE, default=ROLE_DEFAULT): vol.In(ROLE_ALL), vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(), vol.Optional(ATTR_STDIN, default=False): vol.Boolean(), vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(), diff --git a/hassio/api/addons.py b/hassio/api/addons.py index af3a3b515..e95ec063a 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -20,8 +20,8 @@ from ..const import ( ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES, ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_RATING, ATTR_HOST_PID, - CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT, - REQUEST_FROM) + ATTR_HASSIO_ROLE, + CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT, REQUEST_FROM) from ..coresys import CoreSysAttributes from ..validate import DOCKER_PORTS, ALSA_DEVICE from ..exceptions import APINotSupportedError @@ -153,6 +153,7 @@ class APIAddons(CoreSysAttributes): ATTR_WEBUI: addon.webui, ATTR_STDIN: addon.with_stdin, ATTR_HASSIO_API: addon.access_hassio_api, + ATTR_HASSIO_ROLE: addon.hassio_role, ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api, ATTR_GPIO: addon.with_gpio, ATTR_DEVICETREE: addon.with_devicetree, @@ -197,6 +198,7 @@ class APIAddons(CoreSysAttributes): addon = self._extract_addon(request) # Have Access + # REMOVE: don't needed anymore if addon.slug == request[REQUEST_FROM]: _LOGGER.error("Can't self modify his security!") raise APINotSupportedError() diff --git a/hassio/api/proxy.py b/hassio/api/proxy.py index 59ead36b3..526528566 100644 --- a/hassio/api/proxy.py +++ b/hassio/api/proxy.py @@ -25,7 +25,7 @@ class APIProxy(CoreSysAttributes): hassio_token = request.headers.get(HEADER_HA_ACCESS) addon = self.sys_addons.from_token(hassio_token) - # Need removed with 131 + # REMOVE 132 if not addon: addon = self.sys_addons.from_uuid(hassio_token) @@ -184,7 +184,7 @@ class APIProxy(CoreSysAttributes): response.get('access_token')) addon = self.sys_addons.from_token(hassio_token) - # Need removed with 131 + # REMOVE 132 if not addon: addon = self.sys_addons.from_uuid(hassio_token) diff --git a/hassio/api/security.py b/hassio/api/security.py index 2a17b141f..c2b800f71 100644 --- a/hassio/api/security.py +++ b/hassio/api/security.py @@ -5,27 +5,61 @@ import re from aiohttp.web import middleware from aiohttp.web_exceptions import HTTPUnauthorized, HTTPForbidden -from ..const import HEADER_TOKEN, REQUEST_FROM +from ..const import ( + HEADER_TOKEN, REQUEST_FROM, ROLE_ADMIN, ROLE_DEFAULT, ROLE_HOMEASSISTANT, + ROLE_MANAGER) from ..coresys import CoreSysAttributes _LOGGER = logging.getLogger(__name__) +# Free to call or have own security concepts NO_SECURITY_CHECK = re.compile( r"^(?:" - r"|/homeassistant/api/.*$" - r"|/homeassistant/websocket$" - r"|/supervisor/ping$" + r"|/homeassistant/api/.*" + r"|/homeassistant/websocket" + r"|/supervisor/ping" + r"|/services.*" r")$" ) +# Can called by every add-on ADDONS_API_BYPASS = re.compile( r"^(?:" - r"|/homeassistant/info$" - r"|/supervisor/info$" - r"|/addons(?:/self/[^/]+)?$" + r"|/homeassistant/info" + r"|/supervisor/info" + r"|/addons(?:/self/(?!security)[^/]+)?" r")$" ) +# Policy role add-on API access +ADDONS_ROLE_ACCESS = { + ROLE_DEFAULT: re.compile( + r"^(?:" + r"|/[^/]+/info" + r")$" + ), + ROLE_HOMEASSISTANT: re.compile( + r"^(?:" + r"|/homeassistant/.+" + r")$" + ), + ROLE_MANAGER: re.compile( + r"^(?:" + r"|/homeassistant/.+" + r"|/host/.+" + r"|/hardware/.+" + r"|/hassos/.+" + r"|/supervisor/.+" + r"|/addons/.+/(?!security|options).+" + r"|/addons(?:/self/(?!security).+)" + r"|/snapshots.*" + r")$" + ), + ROLE_ADMIN: re.compile( + r".+" + ), +} + class SecurityMiddleware(CoreSysAttributes): """Security middleware functions.""" @@ -66,17 +100,22 @@ class SecurityMiddleware(CoreSysAttributes): addon = None if hassio_token and not request_from: addon = self.sys_addons.from_token(hassio_token) - # Need removed with 131 + # REMOVE 132 if not addon: addon = self.sys_addons.from_uuid(hassio_token) # Check Add-on API access - if addon and addon.access_hassio_api: - _LOGGER.info("%s access from %s", request.path, addon.slug) - request_from = addon.slug - elif addon and ADDONS_API_BYPASS.match(request.path): + if addon and ADDONS_API_BYPASS.match(request.path): _LOGGER.debug("Passthrough %s from %s", request.path, addon.slug) request_from = addon.slug + elif addon and addon.access_hassio_api: + # Check Role + if ADDONS_ROLE_ACCESS[addon.hassio_role].match(request.path): + _LOGGER.info("%s access from %s", request.path, addon.slug) + request_from = addon.slug + else: + _LOGGER.warning("%s no role for %s", request.path, addon.slug) + request_from = addon.slug # REMOVE: 132 if request_from: request[REQUEST_FROM] = request_from diff --git a/hassio/const.py b/hassio/const.py index 7965b4694..abc3a2aac 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -184,6 +184,7 @@ ATTR_DOCKER_API = 'docker_api' ATTR_FULL_ACCESS = 'full_access' ATTR_PROTECTED = 'protected' ATTR_RATING = 'rating' +ATTR_HASSIO_ROLE = 'hassio_role' SERVICE_MQTT = 'mqtt' @@ -246,3 +247,8 @@ FEATURES_REBOOT = 'reboot' FEATURES_HASSOS = 'hassos' FEATURES_HOSTNAME = 'hostname' FEATURES_SERVICES = 'services' + +ROLE_DEFAULT = 'default' +ROLE_HOMEASSISTANT = 'homeassistant' +ROLE_MANAGER = 'manager' +ROLE_ADMIN = 'admin'