7 Commits

14 changed files with 171 additions and 172 deletions

View File

@@ -30,7 +30,7 @@ def create_app(environment='development'):
config[env].configure(app) config[env].configure(app)
app.config['APP_TZ'] = os.environ.get('TZ', 'UTC') app.config['APP_TZ'] = os.environ.get('TZ', 'UTC')
app.config['ADMIN_GROUPS'] = list( app.config['ADMIN_GROUPS'] = list(
map(str.strip, app.config['ADMIN_GROUPS'].split(','))) map(str.strip, app.config.get('ADMIN_GROUPS', "").split(',')))
app.logger.debug(f"admin groups: {app.config['ADMIN_GROUPS']}") app.logger.debug(f"admin groups: {app.config['ADMIN_GROUPS']}")
app.logger.info("middleware init: mobility") app.logger.info("middleware init: mobility")
@@ -41,12 +41,12 @@ def create_app(environment='development'):
# Register blueprints. # Register blueprints.
from .views import main_blueprint, rest_blueprint from .views import main_blueprint, rest_blueprint
app.logger.info(f"registering main blueprint with prefix '{ app.logger.info(f"register blueprint: 'main' [prefix '{
main_blueprint.url_prefix}'") main_blueprint.url_prefix}']")
app.register_blueprint(main_blueprint) app.register_blueprint(main_blueprint)
app.logger.info(f"registering rest blueprint with prefix '{ app.logger.info(f"register blueprint: 'rest' [prefix '{
rest_blueprint.url_prefix}'") rest_blueprint.url_prefix}']")
app.register_blueprint(rest_blueprint) app.register_blueprint(rest_blueprint)
app.logger.info("jinja2 custom filters loaded") app.logger.info("jinja2 custom filters loaded")

View File

@@ -2,7 +2,7 @@ import os
import functools import functools
from flask import request, abort, current_app from flask import request, abort, current_app
from flask import session as flask_session from flask import session as flask_session, jsonify
from flask_pyoidc import OIDCAuthentication as _OIDCAuth from flask_pyoidc import OIDCAuthentication as _OIDCAuth
from flask_pyoidc.user_session import UserSession from flask_pyoidc.user_session import UserSession
from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata
@@ -21,19 +21,6 @@ def remote_ip() -> str:
return str(request.environ.get('REMOTE_ADDR')) return str(request.environ.get('REMOTE_ADDR'))
def username() -> str:
userinfo = flask_session['userinfo']
return userinfo['email'].split('@')[0]
def login_name() -> str:
userinfo = flask_session['userinfo']
if 'preferred_username' in userinfo:
return userinfo['preferred_username']
else:
return username()
def webMode() -> bool: def webMode() -> bool:
is_gunicorn = "gunicorn" in os.environ.get('SERVER_SOFTWARE', '') is_gunicorn = "gunicorn" in os.environ.get('SERVER_SOFTWARE', '')
is_werkzeug = os.environ.get('WERKZEUG_RUN_MAIN', False) == "true" is_werkzeug = os.environ.get('WERKZEUG_RUN_MAIN', False) == "true"
@@ -64,6 +51,53 @@ class OIDCAuthentication(_OIDCAuth):
super().init_app(app) super().init_app(app)
app.auth = self app.auth = self
@property
def username(self) -> str:
userinfo = flask_session['userinfo']
return userinfo['email'].split('@')[0]
@property
def login_name(self) -> str:
userinfo = flask_session['userinfo']
return userinfo.get('preferred_username', self.username)
@property
def isAdmin(self) -> bool:
userinfo = flask_session['userinfo']
user_groups = userinfo.get('groups', [])
with current_app.app_context():
admin_groups = current_app.config.get('ADMIN_GROUPS', [])
admin_users = current_app.config.get('ADMIN_USERS', [])
authorized_groups = set(admin_groups).intersection(user_groups)
if len(authorized_groups):
log.debug(f"'{self.username}' is a member of {
authorized_groups}")
return True
if self.username in admin_users:
log.debug(f"'{self.username}' is an admin user")
return True
return False
@property
def unathorized(self):
response = jsonify(
{'message': f"not authorized",
'comment': 'nice try, info logged',
'logged': f"'{self.username}@{remote_ip()}",
'result': 'GO AWAY!'})
log.warning(
f"user '{self.username}' attempted denied operation from {remote_ip()}")
return response, 403
def userOrAdmin(self, username: str):
"""
Check is the current user is an admin OR the username passed as argument
"""
return self.isAdmin or self.username == username
def authorize(self, provider_name: str, authz_fn: Callable, **kwargs): def authorize(self, provider_name: str, authz_fn: Callable, **kwargs):
if provider_name not in self._provider_configurations: if provider_name not in self._provider_configurations:
raise ValueError( raise ValueError(
@@ -165,23 +199,7 @@ class OIDCAuthentication(_OIDCAuth):
""" """
def _authz_fn(session) -> bool: def _authz_fn(session) -> bool:
user_groups = session.userinfo.get('groups', []) return self.isAdmin
username = session.userinfo.get('preferred_username', "")
with current_app.app_context():
admin_groups = current_app.config.get('ADMIN_GROUPS', [])
admin_users = current_app.config.get('ADMIN_USERS', [])
authorized_groups = set(admin_groups).intersection(user_groups)
if len(authorized_groups):
log.debug(f"'{username}' is a member of {
authorized_groups}")
return True
if username in admin_users:
log.debug(f"'{username}' is an admin user")
return True
return False
return self.authorize(provider_name, return self.authorize(provider_name,
authz_fn=_authz_fn) authz_fn=_authz_fn)

View File

View File

@@ -55,6 +55,18 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
<div class="row data">
<div class="col col-2">
<strong>access level</strong>
</div>
<div class="col col-6">
{% if auth.isAdmin %}
<span class="badge badge-pill badge-danger">ADMIN</span>
{% else %}
<span class="badge badge-pill badge-info">USER</span>
{% endif %}
</div>
</div>
<hr> <hr>
<h4>your devices</h4> <h4>your devices</h4>
<div class="row strong"> <div class="row strong">
@@ -67,7 +79,7 @@
{% for node in userNodeList %} {% for node in userNodeList %}
<div class="row data"> <div class="row data">
<div class="col col-2"> <div class="col col-2">
{{ node.givenName}} <a href="{{url_for('main.node', nodeId=node.id) }}">{{ node.givenName}}</a>
</div> </div>
<div class="col col-2"> <div class="col col-2">
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}"> <span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
@@ -84,7 +96,7 @@
</div> </div>
<div class="col col-2"> <div class="col col-2">
<span data-toggle="tooltip" data-placement="right" title="delete"> <span data-toggle="tooltip" data-placement="right" title="delete">
<a class="nodeco" href="{{ url_for('rest.deleteOwnNode', nodeId=node.id) }}"> <a class="nodeco" href="{{ url_for('rest.deleteNode', nodeId=node.id) }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</span> </span>

View File

@@ -74,7 +74,10 @@
{% for ip in node.ipAddresses %} {% for ip in node.ipAddresses %}
<div class="row data"> <div class="row data">
<div class="col col-3"> <div class="col col-3">
<span class="address copy"
value="{{ ip }}">
{{ ip }} {{ ip }}
</spanundefined>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
@@ -210,3 +213,13 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %}
<script>
$(function () {
$('.address.copy').on('click', function() {
copyToClipboard(this)
})
})
</script>
{% endblock %}

View File

@@ -52,7 +52,7 @@
<td class="no-sort"> <td class="no-sort">
{% if node.expireDate and not node.expired %} {% if node.expireDate and not node.expired %}
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect"> <span data-toggle="tooltip" data-placement="right" title="expire/disconnect">
<a class="nodeco" href="{{ url_for('rest.expireNodeList', nodeId=node.id) }}"> <a class="nodeco" href="{{ url_for('rest.expireNode', nodeId=node.id) }}">
<i class="fas fa-plug"></i> <i class="fas fa-plug"></i>
</a> </a>
</span> </span>

View File

@@ -49,7 +49,7 @@
<td class="no-sort"> <td class="no-sort">
{% if node.expireDate and not node.expired %} {% if node.expireDate and not node.expired %}
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect"> <span data-toggle="tooltip" data-placement="right" title="expire/disconnect">
<a class="nodeco" href="{{ url_for('rest.expireNodeUser', nodeId=node.id) }}"> <a class="nodeco" href="{{ url_for('rest.expireNode', nodeId=node.id) }}">
<i class="fas fa-plug"></i> <i class="fas fa-plug"></i>
</a> </a>
</span> </span>
@@ -57,7 +57,7 @@
<i class="fas fa-plug disabled"></i> <i class="fas fa-plug disabled"></i>
{% endif %} {% endif %}
<span data-toggle="tooltip" data-placement="right" title="delete"> <span data-toggle="tooltip" data-placement="right" title="delete">
<a class="nodeco" href="{{ url_for('rest.deleteNodeUser', nodeId=node.id) }}"> <a class="nodeco" href="{{ url_for('rest.deleteNode', nodeId=node.id) }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</span> </span>

View File

@@ -26,6 +26,15 @@ def health():
return jsonify(dict(status="OK", version=current_app.config['APP_VERSION'])) return jsonify(dict(status="OK", version=current_app.config['APP_VERSION']))
@main_blueprint.route('/token', methods=['GET', 'POST'])
@auth.authorize_admins('default')
def token():
user_session = UserSession(session)
return jsonify(access_token=user_session.access_token,
id_token=user_session.id_token,
userinfo=user_session.userinfo)
@main_blueprint.route('/', methods=['GET', 'POST']) @main_blueprint.route('/', methods=['GET', 'POST'])
@auth.access_control('default') @auth.access_control('default')
def index(): def index():
@@ -34,17 +43,8 @@ def index():
userNodeList = [n for n in Node().list().nodes if n.user.name == hs_user] userNodeList = [n for n in Node().list().nodes if n.user.name == hs_user]
return render_template('index.html', return render_template('index.html',
userNodeList=userNodeList, userNodeList=userNodeList,
session=user_session) session=user_session,
auth=auth)
@main_blueprint.route('/token', methods=['GET', 'POST'])
@auth.authorize_admins('default')
def token():
user_session = UserSession(session)
# return jsonify(user_session.userinfo)
return jsonify(access_token=user_session.access_token,
id_token=user_session.id_token,
userinfo=user_session.userinfo)
@main_blueprint.route('/logout') @main_blueprint.route('/logout')
@@ -62,12 +62,14 @@ def nodes():
@main_blueprint.route('/node/<int:nodeId>', methods=['GET']) @main_blueprint.route('/node/<int:nodeId>', methods=['GET'])
@auth.authorize_admins('default') @auth.access_control('default')
def node(nodeId): def node(nodeId):
# There is a bug in HS api with retrieving a single node # There is a bug in HS api with retrieving a single node
# and we added a workaround to hsapi, so node.get() returns a # and we added a workaround to hsapi, so node.get() returns a
# v1Node object instead of v1NodeResponse, so we access directly # v1Node object instead of v1NodeResponse, so we access directly
# `node`, instead of `node.node` # `node`, instead of `node.node`
if not auth.userOrAdmin(auth.username):
return auth.unathorized
node = Node().get(nodeId) node = Node().get(nodeId)
routes = Node().routes(nodeId) routes = Node().routes(nodeId)
isExitNode = any( isExitNode = any(

View File

@@ -4,7 +4,7 @@ from flask import Blueprint, request
from flask import redirect, url_for from flask import redirect, url_for
from app import auth from app import auth
from ..lib import login_name, username # from ..lib import login_name, username
from flask import jsonify from flask import jsonify
@@ -23,92 +23,52 @@ rest_blueprint = Blueprint(
@rest_blueprint.route('/routeToggle/<int:routeId>', methods=['GET']) @rest_blueprint.route('/routeToggle/<int:routeId>', methods=['GET'])
@auth.authorize_admins('default') @auth.authorize_admins('default')
def routeToggle(routeId: int): def routeToggle(routeId: int):
routes = Route().list() route = Route().get(routeId)
route = [r for r in routes.routes if r.id == routeId]
if route: if route:
route = route[0]
if route.enabled: if route.enabled:
action = 'disabled' action = 'disabled'
Route().disable(routeId)
else: else:
Route().enable(routeId)
action = 'enabled' action = 'enabled'
log.info( log.info(
f"route '{route.prefix}' via '{route.node.givenName}'" f"route '{route.prefix}' via '{route.node.givenName}'"
f"{action} by '{username()}'") f"{action} by '{auth.username}'")
return redirect(url_for("main.routes")) Route().toggle(routeId)
return redirect(request.referrer)
@rest_blueprint.route('/node/<int:nodeId>/expire', methods=['GET']) @rest_blueprint.route('/node/<int:nodeId>/expire', methods=['GET'])
@auth.authorize_admins('default') @auth.access_control('default')
def expireNode(nodeId: int): def expireNode(nodeId: int):
"""
This expires a node from the node page.
The difference from above is that it returns to the /node/nodeId page
"""
Node().expire(nodeId)
log.info(f"node '{nodeId}' expired by '{username()}'")
return redirect(url_for("main.node", nodeId=nodeId))
@rest_blueprint.route('/node/<int:nodeId>/user-expire', methods=['GET'])
@auth.authorize_admins('default')
def expireNodeUser(nodeId: int):
""" """
This expires a node from the node page. This expires a node from the node page.
The difference from above is that it returns to the /node/nodeId page The difference from above is that it returns to the /node/nodeId page
""" """
node = Node().get(nodeId) node = Node().get(nodeId)
userName = node.user.name if not auth.userOrAdmin(node.user.name):
return auth.unathorized
Node().expire(nodeId) Node().expire(nodeId)
log.info(f"node '{nodeId}' expired by '{username()}'") log.info(f"node '{nodeId}' expired by '{auth.username}'")
return redirect(url_for("main.user", userName=userName)) return redirect(request.referrer)
@rest_blueprint.route('/node/<int:nodeId>/list-expire', methods=['GET'])
@auth.authorize_admins('default')
def expireNodeList(nodeId: int):
"""
This expires a node from the node list.
The difference from above is that it returns to the /nodes page
"""
Node().expire(nodeId)
log.info(f"node '{nodeId}' expired by '{username()}'")
return redirect(url_for("main.nodes"))
@rest_blueprint.route('/node/<int:nodeId>/delete', methods=['GET']) @rest_blueprint.route('/node/<int:nodeId>/delete', methods=['GET'])
@ auth.authorize_admins('default') @auth.access_control('default')
def deleteNode(nodeId: int): def deleteNode(nodeId: int):
Node().delete(nodeId)
log.info(f"node '{nodeId}' deleted by '{username()}'")
return redirect(url_for("main.nodes"))
@rest_blueprint.route('/node/<int:nodeId>/delete-own', methods=['GET'])
@auth.access_control('default')
def deleteOwnNode(nodeId: int):
node = Node().get(nodeId) node = Node().get(nodeId)
if node.user.name != username(): if not auth.userOrAdmin(node.user.name):
response = jsonify({'message': 'not authorized'}) return auth.unathorized
return response, 401 Node().expire(nodeId)
Node().delete(nodeId) Node().delete(nodeId)
log.info(f"'{username()}' delete their own node '{nodeId}'") log.info(f"node '{nodeId}' deleted by '{auth.username}'")
return redirect(url_for("main.index")) return redirect(request.referrer)
@rest_blueprint.route('/node/<int:nodeId>/delete-user', methods=['GET'])
@auth.access_control('default')
def deleteNodeUser(nodeId: int):
node = Node().get(nodeId)
Node().delete(nodeId)
log.info(f"'{username()}' delete their own node '{nodeId}'")
return redirect(url_for("main.user", userName=node.user.name))
@rest_blueprint.route('/node/<int:nodeId>/rename/<newName>', methods=['GET']) @rest_blueprint.route('/node/<int:nodeId>/rename/<newName>', methods=['GET'])
@ auth.authorize_admins('default') @auth.access_control('default')
def renameNode(nodeId: int, newName: str): def renameNode(nodeId: int, newName: str):
node = Node().get(nodeId)
if not auth.userOrAdmin(node.user.name):
return auth.unathorized
Node().rename(nodeId, newName) Node().rename(nodeId, newName)
return jsonify(dict(newName=newName)) return jsonify(dict(newName=newName))

View File

@@ -17,7 +17,7 @@ class BaseConfig(object):
# All the followinf vars can be overriden # All the followinf vars can be overriden
# in the environment, using `HSMAN_` prefix # in the environment, using `HSMAN_` prefix
SECRET_KEY = "secreto" SECRET_KEY = "secreto"
ADMIN_GROUPS = ["adminGroup"] ADMIN_GROUPS = "adminGroup"
OIDC_CLIENT_ID = 'client-id' OIDC_CLIENT_ID = 'client-id'
OIDC_CLIENT_SECRET = 'client-secreto' OIDC_CLIENT_SECRET = 'client-secreto'
OIDC_URL = "https://myidp.example.com/auth" OIDC_URL = "https://myidp.example.com/auth"

View File

@@ -11,7 +11,7 @@ preload_app = True
# logconfig = "app/logging/production.ini" # logconfig = "app/logging/production.ini"
logconfig = "app/logging/production.ini" logconfig = "app/logging/production.ini"
# access_log_format = "%(h)s %(l)s %(t)s %(r)s %(s)s %(b)s %(f)s %(a)s" access_log_format = "%(h)s %({x-forwarded-for}i)s %(t)s %(r)s %(s)s %(b)s %(L)s"
# Log to stdout. # Log to stdout.
accesslog = "-" accesslog = "-"
errorlog = "-" errorlog = "-"

87
poetry.lock generated
View File

@@ -241,43 +241,38 @@ files = [
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "42.0.8" version = "43.0.0"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"},
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"},
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"},
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"},
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"},
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"},
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"},
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"},
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"},
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"},
{file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"},
{file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"},
{file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"},
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"},
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"},
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"},
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"},
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"},
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"},
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"},
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"},
{file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"},
{file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"},
{file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"},
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"},
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"},
{file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"},
{file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"},
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"},
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"},
{file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"},
{file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"},
] ]
[package.dependencies] [package.dependencies]
@@ -290,7 +285,7 @@ nox = ["nox"]
pep8test = ["check-sdist", "click", "mypy", "ruff"] pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"] sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"] ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"] test-randomorder = ["pytest-randomly"]
[[package]] [[package]]
@@ -457,13 +452,13 @@ files = [
[[package]] [[package]]
name = "hsapi-client" name = "hsapi-client"
version = "0.9.3" version = "0.9.5"
description = "Headscale API client" description = "Headscale API client"
optional = false optional = false
python-versions = "<4.0,>=3.11" python-versions = "<4.0,>=3.11"
files = [ files = [
{file = "hsapi_client-0.9.3-py3-none-any.whl", hash = "sha256:75bf3e5f35a857c36f49560ba8d70243b3dc66138f50dbf2b862ae240a68b9ab"}, {file = "hsapi_client-0.9.5-py3-none-any.whl", hash = "sha256:a2ef7a62fba6f31ad08d6c04db95306c1da10c255383c699d04aa2dbd293f743"},
{file = "hsapi_client-0.9.3.tar.gz", hash = "sha256:58e1494608e17b224d27ca7fa004219cc1a1f7926c677e16fc21774e9502ed25"}, {file = "hsapi_client-0.9.5.tar.gz", hash = "sha256:aa8bf51a960c8e472b8a423bd7de5f7d9514ca5706e2b98ac473d57d158767f6"},
] ]
[package.dependencies] [package.dependencies]
@@ -473,13 +468,13 @@ requests = ">=2.32.3,<3.0.0"
[[package]] [[package]]
name = "humanize" name = "humanize"
version = "4.9.0" version = "4.10.0"
description = "Python humanize utilities" description = "Python humanize utilities"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "humanize-4.9.0-py3-none-any.whl", hash = "sha256:ce284a76d5b1377fd8836733b983bfb0b76f1aa1c090de2566fcf008d7f6ab16"}, {file = "humanize-4.10.0-py3-none-any.whl", hash = "sha256:39e7ccb96923e732b5c2e27aeaa3b10a8dfeeba3eb965ba7b74a3eb0e30040a6"},
{file = "humanize-4.9.0.tar.gz", hash = "sha256:582a265c931c683a7e9b8ed9559089dea7edcf6cc95be39a3cbc2c5d5ac2bcfa"}, {file = "humanize-4.10.0.tar.gz", hash = "sha256:06b6eb0293e4b85e8d385397c5868926820db32b9b654b932f57fa41c23c9978"},
] ]
[package.extras] [package.extras]
@@ -793,13 +788,13 @@ files = [
[[package]] [[package]]
name = "pure-eval" name = "pure-eval"
version = "0.2.2" version = "0.2.3"
description = "Safely evaluate AST nodes without side effects" description = "Safely evaluate AST nodes without side effects"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"},
{file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"},
] ]
[package.extras] [package.extras]
@@ -1139,13 +1134,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.30.1" version = "0.30.3"
description = "The lightning-fast ASGI server." description = "The lightning-fast ASGI server."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"}, {file = "uvicorn-0.30.3-py3-none-any.whl", hash = "sha256:94a3608da0e530cea8f69683aa4126364ac18e3826b6630d1a65f4638aade503"},
{file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"}, {file = "uvicorn-0.30.3.tar.gz", hash = "sha256:0d114d0831ff1adbf231d358cbf42f17333413042552a624ea6a9b4c33dcfd81"},
] ]
[package.dependencies] [package.dependencies]

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "hsman" name = "hsman"
version = "0.9.7" version = "0.9.11"
description = "Flask Admin webui for Headscale" description = "Flask Admin webui for Headscale"
authors = ["Andrea Mistrali <andrea@mistrali.pw>"] authors = ["Andrea Mistrali <andrea@mistrali.pw>"]
license = "BSD" license = "BSD"

View File

@@ -1,6 +1,5 @@
from app import create_app from app import create_app
from app import lib from app import lib
from app import models
import logging import logging
import logging.config import logging.config
import os import os