From 454eae48a0e4165f46cb0e6728549363ee69006d Mon Sep 17 00:00:00 2001 From: Andrea Mistrali Date: Thu, 4 Jul 2024 13:50:32 +0200 Subject: [PATCH] Version 1.0 --- hsapi/hsapi/preauthkeys.py | 2 +- hsapi/hsapi/schemas.py | 5 +- hsman/app/__init__.py | 6 +- hsman/app/static/main.css | 4 ++ hsman/app/static/main.js | 42 +++++++++++++- hsman/app/templates/node.html | 51 ++++++++--------- hsman/app/templates/user.html | 81 ++++++++++++++++++++++---- hsman/app/views.py | 105 ++++++++++++++++++++++------------ 8 files changed, 214 insertions(+), 82 deletions(-) diff --git a/hsapi/hsapi/preauthkeys.py b/hsapi/hsapi/preauthkeys.py index 117092f..01a4747 100644 --- a/hsapi/hsapi/preauthkeys.py +++ b/hsapi/hsapi/preauthkeys.py @@ -16,7 +16,7 @@ class v1PreAuthKeyResponse(BaseModel): class v1ExpirePreAuthKeyRequest(BaseModel): user: str = Field(alias="user", default=None) - key: int = Field(alias="key", default=None) + key: str = Field(alias="key", default=None) class v1CreatePreAuthKeyRequest(BaseModel): diff --git a/hsapi/hsapi/schemas.py b/hsapi/hsapi/schemas.py index 025980b..3b309a7 100644 --- a/hsapi/hsapi/schemas.py +++ b/hsapi/hsapi/schemas.py @@ -45,7 +45,10 @@ class v1PreAuthKey(BaseModel): def expired(self) -> bool: tzinfo = timezone(timedelta(hours=0)) # UTC now = datetime.now(tzinfo) - return self.expiration < now # type: ignore + exptime = self.expiration < now + expused = not self.reusable and self.used + expephemereal = self.ephemeral and self.used + return exptime or expused or expephemereal class v1User(BaseModel): diff --git a/hsman/app/__init__.py b/hsman/app/__init__.py index d6523ac..2bc08a9 100644 --- a/hsman/app/__init__.py +++ b/hsman/app/__init__.py @@ -13,11 +13,11 @@ import os mobility = Mobility() client_metadata = ClientMetadata( - client_id='client-id', - client_secret='client-secret') + client_id=os.getenv('HSMAN_OIDC_CLIENT_ID'), + client_secret=os.getenv('HSMAN_OIDC_CLIENT_SECRET')) -provider_config = ProviderConfiguration(issuer='oidc-issuer-url', +provider_config = ProviderConfiguration(issuer=os.getenv('HSMAN_OIDC_URL'), client_metadata=client_metadata, auth_request_params={ 'scope': ['openid', diff --git a/hsman/app/static/main.css b/hsman/app/static/main.css index 4404482..aacc4f6 100644 --- a/hsman/app/static/main.css +++ b/hsman/app/static/main.css @@ -99,3 +99,7 @@ a.route.False { div.dt-container div.dt-scroll-body { border-bottom: none !important; } + +tr.pka-hide { + visibility: collapse; +} diff --git a/hsman/app/static/main.js b/hsman/app/static/main.js index 97f7753..0079a52 100644 --- a/hsman/app/static/main.js +++ b/hsman/app/static/main.js @@ -1,6 +1,6 @@ function renameNode(nodeId) { var newName = $("#newName").val(); - var url = "/node/" + nodeId + "/rename/" + newName; + var url = `/node/${nodeId}/rename/${newName}`; $.ajax({ url: url, xhrFields: { @@ -13,9 +13,38 @@ function renameNode(nodeId) { }); } +function createPKA(username) { + console.log(username); + var url = `/user/${username}/pakcreate`; + console.log(url); + var ephemereal = $("#ephemereal").is(":checked"); + var reusable = $("#reusable").is(":checked"); + var expiration = $("#expiration").val(); + console.log(expiration); + $.ajax({ + url: url, + method: "POST", + dataType: "json", + contentType: "application/json; charset=utf-8", + xhrFields: { + withCredentials: true, + }, + data: JSON.stringify({ + ephemeral: ephemereal, + reusable: reusable, + expiration: expiration, + }), + + success: function (data) { + $("#createPKA").modal("hide"); + location.reload(); + }, + }); +} + function copyToClipboard(obj) { var span = $(obj); - var value = span.attr("data-original-title"); + var value = span.attr("value"); var original = span.html(); try { navigator.clipboard.writeText(value); @@ -28,3 +57,12 @@ function copyToClipboard(obj) { console.error(error); } } + +function toggleExpired(obj) { + var toggle = $(obj); + if (toggle.is(":checked")) { + $(".pka-expired").removeClass("pka-hide"); + } else { + $(".pka-expired").addClass("pka-hide"); + } +} diff --git a/hsman/app/templates/node.html b/hsman/app/templates/node.html index bcc3c65..0d9a02f 100644 --- a/hsman/app/templates/node.html +++ b/hsman/app/templates/node.html @@ -1,5 +1,4 @@ {% extends "base.html" %} - {% block content %}

@@ -51,21 +50,21 @@

-
-
-
addresses
-
-
+ + +
addresses
{% for ip in node.ipAddresses %}
-
+
{{ ip }}
{% endfor %}

+ +
tags
-
+
announced @@ -74,7 +73,7 @@
{% if node.validTags %} {% for tag in node.validTags %} - + {{ tag }} {% endfor %} @@ -83,7 +82,7 @@ {% endif %}
-
+
forced
@@ -100,12 +99,12 @@
- +

keys
-
-
+
+
machineKey
@@ -113,8 +112,8 @@
-
-
+
+
nodeKey
@@ -122,43 +121,43 @@
-
-
+
+
discoKey
{{ node.discoKey }}
-

+
routes {% if isExitNode %} -Exit Node +Exit Node {% endif %}
{% if routes %}
-
+
prefix
-
+
enabled
-
+
primary
{% for route in routes | sort(attribute='prefix') %}
-
+
{{ route.prefix }}
-
+
{{ route.enabled | fancyBool | safe }}
-
+
{{ route.isPrimary | fancyBool | safe }}
@@ -169,6 +168,7 @@

No routes announced

+{% endif %} @@ -191,5 +191,4 @@
-{% endif %} {% endblock %} diff --git a/hsman/app/templates/user.html b/hsman/app/templates/user.html index 4719be4..ea770a1 100644 --- a/hsman/app/templates/user.html +++ b/hsman/app/templates/user.html @@ -14,12 +14,15 @@ registered
- + {{ user.createdAt | htime_dt }}

+
nodes
@@ -38,7 +41,9 @@ @@ -50,26 +55,40 @@
- + {{node.lastSeen | htime_dt }}

-
pre auth keys
+ +
+ pre auth keys +   + + +
{% if preauthKeys %} - + - + {% for key in preauthKeys %} - + --> {% endfor %} @@ -111,7 +134,38 @@ {% endif %} - + + {% endblock %} {% block scripts %} @@ -121,6 +175,9 @@ $('.pak_copy').on('click', function() { copyToClipboard(this) }) + $('#showExpired').on('change', function() { + toggleExpired(this) + }) new DataTable('#nodes', { scrollY: 130, @@ -138,7 +195,7 @@ keys: false, }); new DataTable('#paks', { - scrollY: 130, + scrollY: 230, scrollCollapse: true, paging: false, // lengthMenu: [5, 10, 30, 50, { label: 'All', value: -1 }], diff --git a/hsman/app/views.py b/hsman/app/views.py index d042f12..c503523 100644 --- a/hsman/app/views.py +++ b/hsman/app/views.py @@ -1,22 +1,26 @@ -from flask import render_template, Blueprint +import logging +from .lib import remote_ip +import datetime +from flask import render_template, Blueprint, request from flask import redirect, session, url_for from app import auth from flask import jsonify from flask_pyoidc.user_session import UserSession -from hsapi import Node, User, Route, PreAuthKey, v1ListPreAuthKeyRequest +from hsapi import Node, User, Route, PreAuthKey +from hsapi.preauthkeys import (v1ListPreAuthKeyRequest, + v1CreatePreAuthKeyRequest, + v1ExpirePreAuthKeyRequest) -from .lib import remote_ip -import logging log = logging.getLogger() main_blueprint = Blueprint('main', __name__) -@main_blueprint.route('/', methods=['GET', 'POST']) -@auth.access_control('default') +@ main_blueprint.route('/', methods=['GET', 'POST']) +@ auth.access_control('default') def index(): user_session = UserSession(session) hs_user = user_session.userinfo['email'].split('@')[0] @@ -26,35 +30,35 @@ def index(): session=user_session) -@main_blueprint.route('/token', methods=['GET', 'POST']) -@auth.access_control('default') +@ main_blueprint.route('/token', methods=['GET', 'POST']) +@ auth.access_control('default') def token(): user_session = UserSession(session) return jsonify(user_session.userinfo) -@main_blueprint.route('/call', methods=['GET', 'POST']) -@auth.access_control('default') +@ main_blueprint.route('/call', methods=['GET', 'POST']) +@ auth.access_control('default') def call(): return "CALL OK" -@main_blueprint.route('/logout') -@auth.oidc_logout +@ main_blueprint.route('/logout') +@ auth.oidc_logout def logout(): return redirect('/') -@main_blueprint.route('/nodes', methods=['GET']) -@auth.authorize_admins('default') +@ main_blueprint.route('/nodes', methods=['GET']) +@ auth.authorize_admins('default') def nodes(): nodelist = Node().list() return render_template('nodes.html', nodes=nodelist.nodes) -@main_blueprint.route('/node/', methods=['GET']) -@auth.authorize_admins('default') +@ main_blueprint.route('/node/', methods=['GET']) +@ auth.authorize_admins('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 @@ -69,8 +73,8 @@ def node(nodeId): node=node) -@main_blueprint.route('/users', methods=['GET']) -@auth.authorize_admins('default') +@ main_blueprint.route('/users', methods=['GET']) +@ auth.authorize_admins('default') def users(): userList = User().list() # Get online status of devices of the user @@ -85,24 +89,27 @@ def users(): online=online) -@main_blueprint.route('/user/', methods=['GET']) -@auth.authorize_admins('default') +@ main_blueprint.route('/user/', methods=['GET']) +@ auth.authorize_admins('default') def user(userName): user = User().get(userName) userNodeList = [n for n in Node().list().nodes if n.user.name == userName] - preauthkeyreq = v1ListPreAuthKeyRequest(user=userName) + preauthkeyreq = v1ListPreAuthKeyRequest(user=userName) preauthKeys = PreAuthKey().list(preauthkeyreq) - validpak = [k for k in preauthKeys.preAuthKeys if not k.expired] + + defaultExpiry = datetime.datetime.now() + datetime.timedelta(days=7) + expStr = defaultExpiry.strftime('%Y-%m-%dT%H:%M') return render_template("user.html", user=user.user, - preauthKeys=validpak, + defaultExpiry=expStr, + preauthKeys=preauthKeys.preAuthKeys, userNodeList=userNodeList) -@main_blueprint.route('/routes', methods=['GET']) -@auth.authorize_admins('default') +@ main_blueprint.route('/routes', methods=['GET']) +@ auth.authorize_admins('default') def routes(): routes = Route().list() @@ -120,8 +127,8 @@ def routes(): routes=final) -@main_blueprint.route('/routeToggle/', methods=['GET']) -@auth.authorize_admins('default') +@ main_blueprint.route('/routeToggle/', methods=['GET']) +@ auth.authorize_admins('default') def routeToggle(routeId: int): routes = Route().list() route = [r for r in routes.routes if r.id == routeId] @@ -134,36 +141,36 @@ def routeToggle(routeId: int): return redirect(url_for("main.routes")) -@main_blueprint.route('/node//expire', methods=['GET']) -@auth.authorize_admins('default') +@ main_blueprint.route('/node//expire', methods=['GET']) +@ auth.authorize_admins('default') def expireNode(nodeId: int): Node().expire(nodeId) return redirect(url_for("main.node", nodeId=nodeId)) -@main_blueprint.route('/node//list-expire', methods=['GET']) -@auth.authorize_admins('default') +@ main_blueprint.route('/node//list-expire', methods=['GET']) +@ auth.authorize_admins('default') def expireNodeList(nodeId: int): Node().expire(nodeId) return redirect(url_for("main.nodes")) -@main_blueprint.route('/node//delete', methods=['GET']) -@auth.authorize_admins('default') +@ main_blueprint.route('/node//delete', methods=['GET']) +@ auth.authorize_admins('default') def deleteNode(nodeId: int): Node().delete(nodeId) return redirect(url_for("main.nodes")) -@main_blueprint.route('/node//rename/', methods=['GET']) -@auth.authorize_admins('default') +@ main_blueprint.route('/node//rename/', methods=['GET']) +@ auth.authorize_admins('default') def renameNode(nodeId: int, newName: str): Node().rename(nodeId, newName) return jsonify(dict(newName=newName)) -@main_blueprint.route('/user//delete', methods=['GET']) -@auth.authorize_admins('default') +@ main_blueprint.route('/user//delete', methods=['GET']) +@ auth.authorize_admins('default') def deleteUser(userName: str): nodes = Node().byUser(userName) for node in nodes.nodes: @@ -171,3 +178,27 @@ def deleteUser(userName: str): Node().delete(node.id) User().delete(userName) return redirect(url_for("main.users")) + + +@ main_blueprint.route('/user//pakcreate', methods=['POST']) +@ auth.authorize_admins('default') +def createPKA(userName: str): + data = request.json + log.debug(data) + expiration = f"{data['expiration']}:00Z" + req = v1CreatePreAuthKeyRequest(user=userName, + reusable=data['reusable'], + ephemeral=data['ephemeral'], + expiration=expiration) + pak = PreAuthKey().create((req)) + return jsonify(dict(key=pak.preAuthKey.key)) + + +@ main_blueprint.route('/user//expire/', methods=['GET']) +@ auth.authorize_admins('default') +def expirePKA(userName: str, key: str): + log.debug(key) + req = v1ExpirePreAuthKeyRequest(user=userName, key=key) + + PreAuthKey().expire(req) + return redirect(url_for('main.user', userName=userName))
  +
+ + +
+
created expiration attributes
{{ key.key }} + value="{{ key.key}}" + title="click to copy full value" + class="pak_copy">{{ key.key[:5] }}…{{ key.key[-5:] }} + + + + + +