mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 00:18:11 +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 . .
|
||||
RUN pip2 install --no-cache-dir -e .
|
||||
|
||||
ENV USERNAME=""
|
||||
ENV PASSWORD=""
|
||||
|
||||
WORKDIR /config
|
||||
ENTRYPOINT ["esphome"]
|
||||
CMD ["/config", "dashboard"]
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in a new issue