11 Commits

15 changed files with 203 additions and 189 deletions

View File

@@ -17,10 +17,10 @@ You can run the Flask application as any other Flask app, using `flask run` insi
There are some settings that must/can be provided to the application:
| Variable | Usage | Default |
| -------------------------- | ---------------------------------------- | :-----: |
| -------------------------- | -------------------------------------------------------------- | :-----: |
| `APPLICATION_ROOT` | Base URI path for the app | `/` |
| `HSMAN_SECRET_KEY` | Flask app secret key | |
| `HSMAN_ADMIN_GROUPS` | User groups that are considered admins | |
| `HSMAN_ADMIN_GROUPS` | Comma separated list of user groups that are considered admins | |
| `HSMAN_OIDC_CLIENT_ID` | OIDC client ID | |
| `HSMAN_OIDC_CLIENT_SECRET` | OIDC clietn secret | |
| `HSMAN_OIDC_URL` | OIDC server URL | |

View File

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

View File

@@ -2,7 +2,7 @@ import os
import functools
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.user_session import UserSession
from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata
@@ -21,19 +21,6 @@ def remote_ip() -> str:
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:
is_gunicorn = "gunicorn" in os.environ.get('SERVER_SOFTWARE', '')
is_werkzeug = os.environ.get('WERKZEUG_RUN_MAIN', False) == "true"
@@ -64,6 +51,53 @@ class OIDCAuthentication(_OIDCAuth):
super().init_app(app)
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):
if provider_name not in self._provider_configurations:
raise ValueError(
@@ -165,23 +199,7 @@ class OIDCAuthentication(_OIDCAuth):
"""
def _authz_fn(session) -> bool:
user_groups = session.userinfo.get('groups', [])
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.isAdmin
return self.authorize(provider_name,
authz_fn=_authz_fn)

View File

View File

@@ -1,7 +1,9 @@
{% extends "base.html" %}
{% block content %}
<h3>Welcome, {{ session.userinfo.name }}</h3>
<h3>
Welcome, {{ session.userinfo.name }}
</h3>
<hr>
<h4>authentication info</h4>
<div class="row data">
@@ -27,7 +29,13 @@
</div>
<div class="col col-6">
<i class="fas fa-angle-right"></i>
{% if session.userinfo.groups[0] in config['ADMIN_GROUPS'] %}
<span class="badge badge-pill badge-warning">
{% else %}
<span class="badge badge-pill badge-dark">
{% endif %}
{{ session.userinfo.groups[0]}}
</span>
</div>
</div>
{% for group in session.userinfo.groups[1:] |sort %}
@@ -36,10 +44,29 @@
&nbsp;
</div>
<div class="col col-6">
<i class="fas fa-angle-right"></i> {{ group }}
<i class="fas fa-angle-right"></i>
{% if group in config['ADMIN_GROUPS'] %}
<span class="badge badge-pill badge-warning">
{% else %}
<span class="badge badge-pill badge-dark">
{% endif %}
{{ group }}
</span>
</div>
</div>
{% 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>
<h4>your devices</h4>
<div class="row strong">
@@ -52,7 +79,7 @@
{% for node in userNodeList %}
<div class="row data">
<div class="col col-2">
{{ node.givenName}}
<a href="{{url_for('main.node', nodeId=node.id) }}">{{ node.givenName}}</a>
</div>
<div class="col col-2">
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
@@ -69,7 +96,7 @@
</div>
<div class="col col-2">
<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>
</a>
</span>

View File

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

View File

@@ -52,7 +52,7 @@
<td class="no-sort">
{% if node.expireDate and not node.expired %}
<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>
</a>
</span>

View File

@@ -49,7 +49,7 @@
<td class="no-sort">
{% if node.expireDate and not node.expired %}
<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>
</a>
</span>
@@ -57,7 +57,7 @@
<i class="fas fa-plug disabled"></i>
{% endif %}
<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>
</a>
</span>

View File

@@ -26,6 +26,15 @@ def health():
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'])
@auth.access_control('default')
def index():
@@ -34,17 +43,8 @@ def index():
userNodeList = [n for n in Node().list().nodes if n.user.name == hs_user]
return render_template('index.html',
userNodeList=userNodeList,
session=user_session)
@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)
session=user_session,
auth=auth)
@main_blueprint.route('/logout')
@@ -62,16 +62,18 @@ def nodes():
@main_blueprint.route('/node/<int:nodeId>', methods=['GET'])
@auth.authorize_admins('default')
@auth.access_control('default')
def node(nodeId):
# There is a bug in HS api with retrieving a single node
# and we added a workaround to hsapi, so node.get() returns a
# v1Node object instead of v1NodeResponse, so we access directly
# `node`, instead of `node.node`
if not auth.userOrAdmin(auth.username):
return auth.unathorized
node = Node().get(nodeId)
routes = Node().routes(nodeId)
isExitNode = any(
(r for r in routes.routes if r.prefix.endswith('/0') and r.enabled))
(r for r in routes.routes if r.prefix.endswith('0/0') and r.enabled))
return render_template("node.html",
routes=routes.routes,
isExitNode=isExitNode,
@@ -118,11 +120,11 @@ def user(userName):
def routes():
routes = Route().list()
prefixes = set(
(r.prefix for r in routes.routes if not r.prefix.endswith('/0')))
prefixes = sorted(set(
(r.prefix for r in routes.routes if not r.prefix.endswith('/0'))))
exitNodes = [r.node for r in routes.routes if r.prefix.endswith(
'/0') and r.enabled]
'0/0') and r.enabled]
final = {}
for prefix in prefixes:

View File

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

View File

@@ -17,7 +17,7 @@ class BaseConfig(object):
# All the followinf vars can be overriden
# in the environment, using `HSMAN_` prefix
SECRET_KEY = "secreto"
ADMIN_GROUPS = ["adminGroup"]
ADMIN_GROUPS = "adminGroup"
OIDC_CLIENT_ID = 'client-id'
OIDC_CLIENT_SECRET = 'client-secreto'
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"
# 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.
accesslog = "-"
errorlog = "-"

87
poetry.lock generated
View File

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

View File

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

View File

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