mirror of
https://github.com/esphome/esphome.git
synced 2024-11-21 22:48:10 +01:00
Authorization by username and password (#668)
* Auth * Logout * Lint fix * Small hassio fix * Reverted uppercase * Secrets editor * Reverted secrets editor * Reverted log height * Fix default username
This commit is contained in:
parent
38dfab11b4
commit
1a763ae974
5 changed files with 58 additions and 30 deletions
|
@ -4,6 +4,9 @@ FROM ${BUILD_FROM}
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN pip2 install --no-cache-dir -e .
|
RUN pip2 install --no-cache-dir -e .
|
||||||
|
|
||||||
|
ENV USERNAME=""
|
||||||
|
ENV PASSWORD=""
|
||||||
|
|
||||||
WORKDIR /config
|
WORKDIR /config
|
||||||
ENTRYPOINT ["esphome"]
|
ENTRYPOINT ["esphome"]
|
||||||
CMD ["/config", "dashboard"]
|
CMD ["/config", "dashboard"]
|
||||||
|
|
|
@ -477,7 +477,11 @@ def parse_args(argv):
|
||||||
help="Create a simple web server for a dashboard.")
|
help="Create a simple web server for a dashboard.")
|
||||||
dashboard.add_argument("--port", help="The HTTP port to open connections on. Defaults to 6052.",
|
dashboard.add_argument("--port", help="The HTTP port to open connections on. Defaults to 6052.",
|
||||||
type=int, default=6052)
|
type=int, default=6052)
|
||||||
dashboard.add_argument("--password", help="The optional password to require for all requests.",
|
dashboard.add_argument("--username", help="The optional username to require "
|
||||||
|
"for authentication.",
|
||||||
|
type=str, default='')
|
||||||
|
dashboard.add_argument("--password", help="The optional password to require "
|
||||||
|
"for authentication.",
|
||||||
type=str, default='')
|
type=str, default='')
|
||||||
dashboard.add_argument("--open-ui", help="Open the dashboard UI in a browser.",
|
dashboard.add_argument("--open-ui", help="Open the dashboard UI in a browser.",
|
||||||
action='store_true')
|
action='store_true')
|
||||||
|
|
|
@ -46,19 +46,22 @@ class DashboardSettings(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.config_dir = ''
|
self.config_dir = ''
|
||||||
self.password_digest = ''
|
self.password_digest = ''
|
||||||
|
self.username = ''
|
||||||
self.using_password = False
|
self.using_password = False
|
||||||
self.on_hassio = False
|
self.on_hassio = False
|
||||||
self.cookie_secret = None
|
self.cookie_secret = None
|
||||||
|
|
||||||
def parse_args(self, args):
|
def parse_args(self, args):
|
||||||
self.on_hassio = args.hassio
|
self.on_hassio = args.hassio
|
||||||
|
password = args.password or os.getenv('PASSWORD', '')
|
||||||
if not self.on_hassio:
|
if not self.on_hassio:
|
||||||
self.using_password = bool(args.password)
|
self.username = args.username or os.getenv('USERNAME', '')
|
||||||
|
self.using_password = bool(password)
|
||||||
if self.using_password:
|
if self.using_password:
|
||||||
if IS_PY2:
|
if IS_PY2:
|
||||||
self.password_digest = hmac.new(args.password).digest()
|
self.password_digest = hmac.new(password).digest()
|
||||||
else:
|
else:
|
||||||
self.password_digest = hmac.new(args.password.encode()).digest()
|
self.password_digest = hmac.new(password.encode()).digest()
|
||||||
self.config_dir = args.configuration[0]
|
self.config_dir = args.configuration[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -79,7 +82,7 @@ class DashboardSettings(object):
|
||||||
def using_auth(self):
|
def using_auth(self):
|
||||||
return self.using_password or self.using_hassio_auth
|
return self.using_password or self.using_hassio_auth
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, username, password):
|
||||||
if not self.using_auth:
|
if not self.using_auth:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -87,7 +90,7 @@ class DashboardSettings(object):
|
||||||
password = hmac.new(password).digest()
|
password = hmac.new(password).digest()
|
||||||
else:
|
else:
|
||||||
password = hmac.new(password.encode()).digest()
|
password = hmac.new(password.encode()).digest()
|
||||||
return hmac.compare_digest(self.password_digest, password)
|
return username == self.username and hmac.compare_digest(self.password_digest, password)
|
||||||
|
|
||||||
def rel_path(self, *args):
|
def rel_path(self, *args):
|
||||||
return os.path.join(self.config_dir, *args)
|
return os.path.join(self.config_dir, *args)
|
||||||
|
@ -585,16 +588,14 @@ PING_REQUEST = threading.Event()
|
||||||
|
|
||||||
class LoginHandler(BaseHandler):
|
class LoginHandler(BaseHandler):
|
||||||
def get(self):
|
def get(self):
|
||||||
if settings.using_hassio_auth:
|
if is_authenticated(self):
|
||||||
self.render_hassio_login()
|
self.redirect('/')
|
||||||
return
|
else:
|
||||||
self.write('<html><body><form action="./login" method="post">'
|
self.render_login_page()
|
||||||
'Password: <input type="password" name="password">'
|
|
||||||
'<input type="submit" value="Sign in">'
|
|
||||||
'</form></body></html>')
|
|
||||||
|
|
||||||
def render_hassio_login(self, error=None):
|
def render_login_page(self, error=None):
|
||||||
self.render("templates/login.html", error=error, **template_args())
|
self.render("templates/login.html", error=error, hassio=settings.using_hassio_auth,
|
||||||
|
has_username=bool(settings.username), **template_args())
|
||||||
|
|
||||||
def post_hassio_login(self):
|
def post_hassio_login(self):
|
||||||
import requests
|
import requests
|
||||||
|
@ -615,20 +616,34 @@ class LoginHandler(BaseHandler):
|
||||||
except Exception as err: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
_LOGGER.warning("Error during Hass.io auth request: %s", err)
|
_LOGGER.warning("Error during Hass.io auth request: %s", err)
|
||||||
self.set_status(500)
|
self.set_status(500)
|
||||||
self.render_hassio_login(error="Internal server error")
|
self.render_login_page(error="Internal server error")
|
||||||
return
|
return
|
||||||
self.set_status(401)
|
self.set_status(401)
|
||||||
self.render_hassio_login(error="Invalid username or password")
|
self.render_login_page(error="Invalid username or password")
|
||||||
|
|
||||||
|
def post_native_login(self):
|
||||||
|
username = str(self.get_argument("username", '').encode('utf-8'))
|
||||||
|
password = str(self.get_argument("password", '').encode('utf-8'))
|
||||||
|
if settings.check_password(username, password):
|
||||||
|
self.set_secure_cookie("authenticated", cookie_authenticated_yes)
|
||||||
|
self.redirect("/")
|
||||||
|
return
|
||||||
|
error_str = "Invalid username or password" if settings.username else "Invalid password"
|
||||||
|
self.set_status(401)
|
||||||
|
self.render_login_page(error=error_str)
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
if settings.using_hassio_auth:
|
if settings.using_hassio_auth:
|
||||||
self.post_hassio_login()
|
self.post_hassio_login()
|
||||||
return
|
else:
|
||||||
|
self.post_native_login()
|
||||||
|
|
||||||
password = str(self.get_argument("password", ''))
|
|
||||||
if settings.check_password(password):
|
class LogoutHandler(BaseHandler):
|
||||||
self.set_secure_cookie("authenticated", cookie_authenticated_yes)
|
@authenticated
|
||||||
self.redirect("/")
|
def get(self):
|
||||||
|
self.clear_cookie("authenticated")
|
||||||
|
self.redirect('./login')
|
||||||
|
|
||||||
|
|
||||||
_STATIC_FILE_HASHES = {}
|
_STATIC_FILE_HASHES = {}
|
||||||
|
@ -681,6 +696,7 @@ def make_app(debug=False):
|
||||||
app = tornado.web.Application([
|
app = tornado.web.Application([
|
||||||
(rel + "", MainRequestHandler),
|
(rel + "", MainRequestHandler),
|
||||||
(rel + "login", LoginHandler),
|
(rel + "login", LoginHandler),
|
||||||
|
(rel + "logout", LogoutHandler),
|
||||||
(rel + "logs", EsphomeLogsHandler),
|
(rel + "logs", EsphomeLogsHandler),
|
||||||
(rel + "upload", EsphomeUploadHandler),
|
(rel + "upload", EsphomeUploadHandler),
|
||||||
(rel + "compile", EsphomeCompileHandler),
|
(rel + "compile", EsphomeCompileHandler),
|
||||||
|
|
|
@ -38,8 +38,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul id="dropdown-nav-actions" class="select-action dropdown-content card-dropdown-action">
|
<ul id="dropdown-nav-actions" class="select-action dropdown-content card-dropdown-action">
|
||||||
|
<li><a id="logout-button" href="{{ relative_url }}logout">Logout</a></li>
|
||||||
<li><a id="update-all-button" data-node="{{ escape(config_dir) }}">Update All</a></li>
|
<li><a id="update-all-button" data-node="{{ escape(config_dir) }}">Update All</a></li>
|
||||||
<li><a id="secrets-button" class="action-edit" data-node="secrets.yaml">Secrets</a></li>
|
<li><a id="secrets-button" class="action-edit" data-node="secrets.yaml">Secrets Editor</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
|
@ -31,19 +31,23 @@
|
||||||
<form action="./login" method="post">
|
<form action="./login" method="post">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<span class="card-title">Enter credentials</span>
|
<span class="card-title">Enter credentials</span>
|
||||||
<p>
|
{% if hassio %}
|
||||||
Please login using your Home Assistant credentials.
|
<p>
|
||||||
</p>
|
Please login using your Home Assistant credentials.
|
||||||
|
</p>
|
||||||
|
{% end %}
|
||||||
{% if error is not None %}
|
{% if error is not None %}
|
||||||
<p class="error">
|
<p class="error">
|
||||||
{{ escape(error) }}
|
{{ escape(error) }}
|
||||||
</p>
|
</p>
|
||||||
{% end %}
|
{% end %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="input-field col s12">
|
{% if has_username or hassio %}
|
||||||
<label for="username">Username</label>
|
<div class="input-field col s12">
|
||||||
<input type="text" class="validate" name="username" id="username" />
|
<label for="username">Username</label>
|
||||||
</div>
|
<input type="text" class="validate" name="username" id="username" />
|
||||||
|
</div>
|
||||||
|
{% end %}
|
||||||
<div class="input-field col s12">
|
<div class="input-field col s12">
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<input type="password" class="validate" name="password" id="password" />
|
<input type="password" class="validate" name="password" id="password" />
|
||||||
|
|
Loading…
Reference in a new issue