Add API role system (#703)

* Add API role system

* Finish

* Simplify

* Fix lint

* Fix rights

* Fix lint

* Fix spell

* Fix log
This commit is contained in:
Pascal Vizeli 2018-09-18 20:39:58 +02:00 committed by GitHub
parent c2299ef8da
commit 9f8ad05471
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 89 additions and 20 deletions

1
API.md
View File

@ -483,6 +483,7 @@ Get all available addons.
"logo": "bool", "logo": "bool",
"changelog": "bool", "changelog": "bool",
"hassio_api": "bool", "hassio_api": "bool",
"hassio_role": "default|homeassistant|manager|admin",
"homeassistant_api": "bool", "homeassistant_api": "bool",
"full_access": "bool", "full_access": "bool",
"protected": "bool", "protected": "bool",

View File

@ -26,7 +26,7 @@ from ..const import (
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC,
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES,
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS, 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) SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..docker.addon import DockerAddon 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 True if the add-on access to Home-Assistant api proxy."""
return self._mesh[ATTR_HOMEASSISTANT_API] return self._mesh[ATTR_HOMEASSISTANT_API]
@property
def hassio_role(self):
"""Return Hass.io role for API."""
return self._mesh[ATTR_HASSIO_ROLE]
@property @property
def with_stdin(self): def with_stdin(self):
"""Return True if the add-on access use stdin input.""" """Return True if the add-on access use stdin input."""

View File

@ -6,7 +6,8 @@ import re
from ..const import ( from ..const import (
SECURITY_DISABLE, SECURITY_PROFILE, PRIVILEGED_NET_ADMIN, 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}") RE_SHA1 = re.compile(r"[a-f0-9]{8}")
@ -36,6 +37,12 @@ def rating_security(addon):
PRIVILEGED_SYS_RAWIO, PRIVILEGED_SYS_PTRACE): PRIVILEGED_SYS_RAWIO, PRIVILEGED_SYS_PTRACE):
rating += -1 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 # Not secure Networking
if addon.host_network: if addon.host_network:
rating += -1 rating += -1

View File

@ -19,10 +19,11 @@ from ..const import (
ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY,
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY,
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_PROTECTED, 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_NET_ADMIN, PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO,
PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE, 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 from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -72,6 +73,13 @@ PRIVILEGED_ALL = [
PRIVILEGED_SYS_PTRACE, PRIVILEGED_SYS_PTRACE,
] ]
ROLE_ALL = [
ROLE_DEFAULT,
ROLE_HOMEASSISTANT,
ROLE_MANAGER,
ROLE_ADMIN,
]
BASE_IMAGE = { BASE_IMAGE = {
ARCH_ARMHF: "homeassistant/armhf-base:latest", ARCH_ARMHF: "homeassistant/armhf-base:latest",
ARCH_AARCH64: "homeassistant/aarch64-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_GPIO, default=False): vol.Boolean(),
vol.Optional(ATTR_DEVICETREE, 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_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_HOMEASSISTANT_API, default=False): vol.Boolean(),
vol.Optional(ATTR_STDIN, default=False): vol.Boolean(), vol.Optional(ATTR_STDIN, default=False): vol.Boolean(),
vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(), vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(),

View File

@ -20,8 +20,8 @@ from ..const import (
ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES, ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES,
ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API,
ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_RATING, ATTR_HOST_PID, ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_RATING, ATTR_HOST_PID,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT, ATTR_HASSIO_ROLE,
REQUEST_FROM) CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT, REQUEST_FROM)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..validate import DOCKER_PORTS, ALSA_DEVICE from ..validate import DOCKER_PORTS, ALSA_DEVICE
from ..exceptions import APINotSupportedError from ..exceptions import APINotSupportedError
@ -153,6 +153,7 @@ class APIAddons(CoreSysAttributes):
ATTR_WEBUI: addon.webui, ATTR_WEBUI: addon.webui,
ATTR_STDIN: addon.with_stdin, ATTR_STDIN: addon.with_stdin,
ATTR_HASSIO_API: addon.access_hassio_api, ATTR_HASSIO_API: addon.access_hassio_api,
ATTR_HASSIO_ROLE: addon.hassio_role,
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api, ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
ATTR_GPIO: addon.with_gpio, ATTR_GPIO: addon.with_gpio,
ATTR_DEVICETREE: addon.with_devicetree, ATTR_DEVICETREE: addon.with_devicetree,
@ -197,6 +198,7 @@ class APIAddons(CoreSysAttributes):
addon = self._extract_addon(request) addon = self._extract_addon(request)
# Have Access # Have Access
# REMOVE: don't needed anymore
if addon.slug == request[REQUEST_FROM]: if addon.slug == request[REQUEST_FROM]:
_LOGGER.error("Can't self modify his security!") _LOGGER.error("Can't self modify his security!")
raise APINotSupportedError() raise APINotSupportedError()

View File

@ -25,7 +25,7 @@ class APIProxy(CoreSysAttributes):
hassio_token = request.headers.get(HEADER_HA_ACCESS) hassio_token = request.headers.get(HEADER_HA_ACCESS)
addon = self.sys_addons.from_token(hassio_token) addon = self.sys_addons.from_token(hassio_token)
# Need removed with 131 # REMOVE 132
if not addon: if not addon:
addon = self.sys_addons.from_uuid(hassio_token) addon = self.sys_addons.from_uuid(hassio_token)
@ -184,7 +184,7 @@ class APIProxy(CoreSysAttributes):
response.get('access_token')) response.get('access_token'))
addon = self.sys_addons.from_token(hassio_token) addon = self.sys_addons.from_token(hassio_token)
# Need removed with 131 # REMOVE 132
if not addon: if not addon:
addon = self.sys_addons.from_uuid(hassio_token) addon = self.sys_addons.from_uuid(hassio_token)

View File

@ -5,27 +5,61 @@ import re
from aiohttp.web import middleware from aiohttp.web import middleware
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPForbidden 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 from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Free to call or have own security concepts
NO_SECURITY_CHECK = re.compile( NO_SECURITY_CHECK = re.compile(
r"^(?:" r"^(?:"
r"|/homeassistant/api/.*$" r"|/homeassistant/api/.*"
r"|/homeassistant/websocket$" r"|/homeassistant/websocket"
r"|/supervisor/ping$" r"|/supervisor/ping"
r"|/services.*"
r")$" r")$"
) )
# Can called by every add-on
ADDONS_API_BYPASS = re.compile( ADDONS_API_BYPASS = re.compile(
r"^(?:" r"^(?:"
r"|/homeassistant/info$" r"|/homeassistant/info"
r"|/supervisor/info$" r"|/supervisor/info"
r"|/addons(?:/self/[^/]+)?$" r"|/addons(?:/self/(?!security)[^/]+)?"
r")$" 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): class SecurityMiddleware(CoreSysAttributes):
"""Security middleware functions.""" """Security middleware functions."""
@ -66,17 +100,22 @@ class SecurityMiddleware(CoreSysAttributes):
addon = None addon = None
if hassio_token and not request_from: if hassio_token and not request_from:
addon = self.sys_addons.from_token(hassio_token) addon = self.sys_addons.from_token(hassio_token)
# Need removed with 131 # REMOVE 132
if not addon: if not addon:
addon = self.sys_addons.from_uuid(hassio_token) addon = self.sys_addons.from_uuid(hassio_token)
# Check Add-on API access # Check Add-on API access
if addon and addon.access_hassio_api: if addon and ADDONS_API_BYPASS.match(request.path):
_LOGGER.info("%s access from %s", request.path, addon.slug)
request_from = addon.slug
elif addon and ADDONS_API_BYPASS.match(request.path):
_LOGGER.debug("Passthrough %s from %s", request.path, addon.slug) _LOGGER.debug("Passthrough %s from %s", request.path, addon.slug)
request_from = 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: if request_from:
request[REQUEST_FROM] = request_from request[REQUEST_FROM] = request_from

View File

@ -184,6 +184,7 @@ ATTR_DOCKER_API = 'docker_api'
ATTR_FULL_ACCESS = 'full_access' ATTR_FULL_ACCESS = 'full_access'
ATTR_PROTECTED = 'protected' ATTR_PROTECTED = 'protected'
ATTR_RATING = 'rating' ATTR_RATING = 'rating'
ATTR_HASSIO_ROLE = 'hassio_role'
SERVICE_MQTT = 'mqtt' SERVICE_MQTT = 'mqtt'
@ -246,3 +247,8 @@ FEATURES_REBOOT = 'reboot'
FEATURES_HASSOS = 'hassos' FEATURES_HASSOS = 'hassos'
FEATURES_HOSTNAME = 'hostname' FEATURES_HOSTNAME = 'hostname'
FEATURES_SERVICES = 'services' FEATURES_SERVICES = 'services'
ROLE_DEFAULT = 'default'
ROLE_HOMEASSISTANT = 'homeassistant'
ROLE_MANAGER = 'manager'
ROLE_ADMIN = 'admin'