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:
Nikolay Vasilchuk 2019-10-13 14:52:02 +03:00 committed by Otto Winter
parent 38dfab11b4
commit 1a763ae974
5 changed files with 58 additions and 30 deletions

View File

@ -4,6 +4,9 @@ FROM ${BUILD_FROM}
COPY . .
RUN pip2 install --no-cache-dir -e .
ENV USERNAME=""
ENV PASSWORD=""
WORKDIR /config
ENTRYPOINT ["esphome"]
CMD ["/config", "dashboard"]

View File

@ -477,7 +477,11 @@ def parse_args(argv):
help="Create a simple web server for a dashboard.")
dashboard.add_argument("--port", help="The HTTP port to open connections on. Defaults to 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='')
dashboard.add_argument("--open-ui", help="Open the dashboard UI in a browser.",
action='store_true')

View File

@ -46,19 +46,22 @@ class DashboardSettings(object):
def __init__(self):
self.config_dir = ''
self.password_digest = ''
self.username = ''
self.using_password = False
self.on_hassio = False
self.cookie_secret = None
def parse_args(self, args):
self.on_hassio = args.hassio
password = args.password or os.getenv('PASSWORD', '')
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 IS_PY2:
self.password_digest = hmac.new(args.password).digest()
self.password_digest = hmac.new(password).digest()
else:
self.password_digest = hmac.new(args.password.encode()).digest()
self.password_digest = hmac.new(password.encode()).digest()
self.config_dir = args.configuration[0]
@property
@ -79,7 +82,7 @@ class DashboardSettings(object):
def using_auth(self):
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:
return True
@ -87,7 +90,7 @@ class DashboardSettings(object):
password = hmac.new(password).digest()
else:
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):
return os.path.join(self.config_dir, *args)
@ -585,16 +588,14 @@ PING_REQUEST = threading.Event()
class LoginHandler(BaseHandler):
def get(self):
if settings.using_hassio_auth:
self.render_hassio_login()
return
self.write('<html><body><form action="./login" method="post">'
'Password: <input type="password" name="password">'
'<input type="submit" value="Sign in">'
'</form></body></html>')
if is_authenticated(self):
self.redirect('/')
else:
self.render_login_page()
def render_hassio_login(self, error=None):
self.render("templates/login.html", error=error, **template_args())
def render_login_page(self, error=None):
self.render("templates/login.html", error=error, hassio=settings.using_hassio_auth,
has_username=bool(settings.username), **template_args())
def post_hassio_login(self):
import requests
@ -615,20 +616,34 @@ class LoginHandler(BaseHandler):
except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Error during Hass.io auth request: %s", err)
self.set_status(500)
self.render_hassio_login(error="Internal server error")
self.render_login_page(error="Internal server error")
return
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):
if settings.using_hassio_auth:
self.post_hassio_login()
return
else:
self.post_native_login()
password = str(self.get_argument("password", ''))
if settings.check_password(password):
self.set_secure_cookie("authenticated", cookie_authenticated_yes)
self.redirect("/")
class LogoutHandler(BaseHandler):
@authenticated
def get(self):
self.clear_cookie("authenticated")
self.redirect('./login')
_STATIC_FILE_HASHES = {}
@ -681,6 +696,7 @@ def make_app(debug=False):
app = tornado.web.Application([
(rel + "", MainRequestHandler),
(rel + "login", LoginHandler),
(rel + "logout", LogoutHandler),
(rel + "logs", EsphomeLogsHandler),
(rel + "upload", EsphomeUploadHandler),
(rel + "compile", EsphomeCompileHandler),

View File

@ -38,8 +38,9 @@
</div>
<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="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>
</nav>

View File

@ -31,19 +31,23 @@
<form action="./login" method="post">
<div class="card-content">
<span class="card-title">Enter credentials</span>
<p>
Please login using your Home Assistant credentials.
</p>
{% if hassio %}
<p>
Please login using your Home Assistant credentials.
</p>
{% end %}
{% if error is not None %}
<p class="error">
{{ escape(error) }}
</p>
{% end %}
<div class="row">
<div class="input-field col s12">
<label for="username">Username</label>
<input type="text" class="validate" name="username" id="username" />
</div>
{% if has_username or hassio %}
<div class="input-field col s12">
<label for="username">Username</label>
<input type="text" class="validate" name="username" id="username" />
</div>
{% end %}
<div class="input-field col s12">
<label for="password">Password</label>
<input type="password" class="validate" name="password" id="password" />