Version 1.0

This commit is contained in:
Andrea Mistrali 2024-07-04 13:50:32 +02:00
parent ca72edd087
commit 454eae48a0
8 changed files with 214 additions and 82 deletions

View File

@ -16,7 +16,7 @@ class v1PreAuthKeyResponse(BaseModel):
class v1ExpirePreAuthKeyRequest(BaseModel): class v1ExpirePreAuthKeyRequest(BaseModel):
user: str = Field(alias="user", default=None) user: str = Field(alias="user", default=None)
key: int = Field(alias="key", default=None) key: str = Field(alias="key", default=None)
class v1CreatePreAuthKeyRequest(BaseModel): class v1CreatePreAuthKeyRequest(BaseModel):

View File

@ -45,7 +45,10 @@ class v1PreAuthKey(BaseModel):
def expired(self) -> bool: def expired(self) -> bool:
tzinfo = timezone(timedelta(hours=0)) # UTC tzinfo = timezone(timedelta(hours=0)) # UTC
now = datetime.now(tzinfo) 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): class v1User(BaseModel):

View File

@ -13,11 +13,11 @@ import os
mobility = Mobility() mobility = Mobility()
client_metadata = ClientMetadata( client_metadata = ClientMetadata(
client_id='client-id', client_id=os.getenv('HSMAN_OIDC_CLIENT_ID'),
client_secret='client-secret') 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, client_metadata=client_metadata,
auth_request_params={ auth_request_params={
'scope': ['openid', 'scope': ['openid',

View File

@ -99,3 +99,7 @@ a.route.False {
div.dt-container div.dt-scroll-body { div.dt-container div.dt-scroll-body {
border-bottom: none !important; border-bottom: none !important;
} }
tr.pka-hide {
visibility: collapse;
}

View File

@ -1,6 +1,6 @@
function renameNode(nodeId) { function renameNode(nodeId) {
var newName = $("#newName").val(); var newName = $("#newName").val();
var url = "/node/" + nodeId + "/rename/" + newName; var url = `/node/${nodeId}/rename/${newName}`;
$.ajax({ $.ajax({
url: url, url: url,
xhrFields: { 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) { function copyToClipboard(obj) {
var span = $(obj); var span = $(obj);
var value = span.attr("data-original-title"); var value = span.attr("value");
var original = span.html(); var original = span.html();
try { try {
navigator.clipboard.writeText(value); navigator.clipboard.writeText(value);
@ -28,3 +57,12 @@ function copyToClipboard(obj) {
console.error(error); console.error(error);
} }
} }
function toggleExpired(obj) {
var toggle = $(obj);
if (toggle.is(":checked")) {
$(".pka-expired").removeClass("pka-hide");
} else {
$(".pka-expired").addClass("pka-hide");
}
}

View File

@ -1,5 +1,4 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<h3> <h3>
<span id="givenName"> <span id="givenName">
@ -51,21 +50,21 @@
</div> </div>
</div> </div>
<p></p> <p></p>
<div class="row">
<div class="col col-3 float-left"> <!-- ADDRESSES -->
<h5>addresses</h5> <h5>addresses</h5>
</div>
</div>
{% for ip in node.ipAddresses %} {% for ip in node.ipAddresses %}
<div class="row data"> <div class="row data">
<div class="col col-3 float-left"> <div class="col col-3">
{{ ip }} {{ ip }}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
<p></p> <p></p>
<!-- TAGS -->
<h5>tags</h5> <h5>tags</h5>
<div class="row"> <div class="row data">
<div class="col col-3 float-left"> <div class="col col-3 float-left">
<strong> <strong>
announced announced
@ -74,7 +73,7 @@
<div class="col col-6 float-left"> <div class="col col-6 float-left">
{% if node.validTags %} {% if node.validTags %}
{% for tag in node.validTags %} {% for tag in node.validTags %}
<span class="badge badge-pill badge-success"> <span class="badge badge-pill badge-info">
{{ tag }} {{ tag }}
</span> </span>
{% endfor %} {% endfor %}
@ -83,7 +82,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="row"> <div class="row data">
<div class="col col-3 float-left"> <div class="col col-3 float-left">
<strong>forced</strong> <strong>forced</strong>
</div> </div>
@ -100,12 +99,12 @@
</div> </div>
</div> </div>
<!-- KEYS -->
<p></p> <p></p>
<h5>keys</h5> <h5>keys</h5>
<div class="row"> <div class="row data">
<div class="col col-4 float-left"> <div class="col col-3 float-left">
<strong>machineKey</strong> <strong>machineKey</strong>
</div> </div>
<div class="col col-8 float-left"> <div class="col col-8 float-left">
@ -113,8 +112,8 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row data">
<div class="col col-4 float-left"> <div class="col col-3 float-left">
<strong>nodeKey</strong> <strong>nodeKey</strong>
</div> </div>
<div class="col col-8 float-left"> <div class="col col-8 float-left">
@ -122,43 +121,43 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row data">
<div class="col col-4 float-left"> <div class="col col-3 float-left">
<strong>discoKey</strong> <strong>discoKey</strong>
</div> </div>
<div class="col col-8 float-left"> <div class="col col-8 float-left">
<code>{{ node.discoKey }}</code> <code>{{ node.discoKey }}</code>
</div> </div>
</div> </div>
<p></p> <p></p>
<!-- ROUTES -->
<h5>routes <h5>routes
{% if isExitNode %} {% if isExitNode %}
<span class="small badge-pill badge-success">Exit Node</span> <span class="small badge-pill badge-primary">Exit Node</span>
{% endif %} {% endif %}
</h5> </h5>
{% if routes %} {% if routes %}
<div class="row"> <div class="row">
<div class="col col-4 float-left"> <div class="col col-3 float-left">
<strong>prefix</strong> <strong>prefix</strong>
</div> </div>
<div class="col col-4 float-left"> <div class="col col-3 float-left">
<strong>enabled</strong> <strong>enabled</strong>
</div> </div>
<div class="col col-4 float-left"> <div class="col col-3 float-left">
<strong>primary</strong> <strong>primary</strong>
</div> </div>
</div> </div>
{% for route in routes | sort(attribute='prefix') %} {% for route in routes | sort(attribute='prefix') %}
<div class="row data"> <div class="row data">
<div class="col col-4 float-left"> <div class="col col-3 float-left">
{{ route.prefix }} {{ route.prefix }}
</div> </div>
<div class="col col-4 float-left"> <div class="col col-3 float-left">
{{ route.enabled | fancyBool | safe }} {{ route.enabled | fancyBool | safe }}
</div> </div>
<div class="col col-4 float-left"> <div class="col col-3 float-left">
{{ route.isPrimary | fancyBool | safe }} {{ route.isPrimary | fancyBool | safe }}
</div> </div>
</div> </div>
@ -169,6 +168,7 @@
<h3>No routes announced</h3> <h3>No routes announced</h3>
</div> </div>
</div> </div>
{% endif %}
<!-- rename modal --> <!-- rename modal -->
<!-- Modal --> <!-- Modal -->
@ -191,5 +191,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -14,12 +14,15 @@
<strong>registered</strong> <strong>registered</strong>
</div> </div>
<div class="col col-8"> <div class="col col-8">
<span data-toggle="tooltip" data-placement="right" title="{{ user.createdAt | fmt_datetime }}"> <span data-toggle="tooltip"
data-placement="right"
title="{{ user.createdAt | fmt_datetime }}">
{{ user.createdAt | htime_dt }} {{ user.createdAt | htime_dt }}
</span> </span>
</div> </div>
</div> </div>
<p></p> <p></p>
<!-- NODES -->
<h5>nodes</h5> <h5>nodes</h5>
<table id="nodes" class="display" style="width:80%"> <table id="nodes" class="display" style="width:80%">
<thead> <thead>
@ -38,7 +41,9 @@
</a> </a>
</td> </td>
<td> <td>
<span data-toggle="tooltip" data-placement="right" title="{{ node.lastSeen | fmt_datetime }}"> <span data-toggle="tooltip"
data-placement="right"
title="{{ node.lastSeen | fmt_datetime }}">
{{node.lastSeen | htime_dt }} {{node.lastSeen | htime_dt }}
</span> </span>
</td> </td>
@ -50,26 +55,40 @@
</tbody> </tbody>
</table> </table>
<p></p> <p></p>
<h5>pre auth keys</h5> <!-- PRE AUTH KEYS -->
<h5>
pre auth keys
&nbsp;
<button class="btn btn-outline-primary btn-sm" data-toggle="modal" data-target="#createPKA">create</button>
</h5>
{% if preauthKeys %} {% if preauthKeys %}
<table id="paks" class="display" style="width:80%"> <table id="paks" class="display" style="width:80%">
<thead> <thead>
<tr> <tr>
<th>&nbsp;</th> <th>
<div class="form-check form-check-inline">
<label class="form-check-label small" for="showExpired">
show expired&nbsp;
</label>
<input type="checkbox" class="form-check-input form-control-sm" id="showExpired">
</div>
</th>
<th>created</th> <th>created</th>
<th>expiration</th> <th>expiration</th>
<th>attributes</th> <th>attributes</th>
<!-- <th>valid</th> --> <!-- <th>&nbsp;</th> -->
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for key in preauthKeys %} {% for key in preauthKeys %}
<tr> <tr class="pka{% if key.expired %} pka-expired pka-hide{% endif %}">
<td> <td>
<span data-toggle="tooltip" <span data-toggle="tooltip"
data-placement="right" data-placement="right"
title="click to copy" value="{{ key.key}}"
class="pak_copy">{{ key.key }}</span> title="click to copy full value"
class="pak_copy">{{ key.key[:5] }}&hellip;{{ key.key[-5:] }}</span>
</td> </td>
<td> <td>
<span data-toggle="tooltip" <span data-toggle="tooltip"
@ -97,7 +116,11 @@
{% endif %} {% endif %}
</td> </td>
<!-- <td> <!-- <td>
{{(not key.expired) | fancyBool | safe}} <span data-toggle="tooltip" data-placement="right" title="expire">
<a class="nodeco" href="/user/{{user.name}}/expire/{{key.key}}">
<i class="fas fa-trash"></i>
</a>
</span>
</td> --> </td> -->
</tr> </tr>
{% endfor %} {% endfor %}
@ -111,7 +134,38 @@
</div> </div>
{% endif %} {% endif %}
<!-- new key modal -->
<div class="modal fade" id="createPKA" tabindex="-1" role="dialog" aria-labelledby="cretePKA" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="renameModalLabel">create new pre auth key</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-check form-check-inline">
<input class="form-check-input" type="datetime-local" name="expiration" id="expiration" value="{{ defaultExpiry}}">
<label class="form-check-label" for="ephemereal">expiration</label>
</div>
<p></p>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="reusable" id="reusable">
<label class="form-check-label" for="reusable">reusable</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="ephemereal" id="ephemereal">
<label class="form-check-label" for="ephemereal">ephemereal</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onClick="createPKA(user='{{user.name}}')">Save changes</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
@ -121,6 +175,9 @@
$('.pak_copy').on('click', function() { $('.pak_copy').on('click', function() {
copyToClipboard(this) copyToClipboard(this)
}) })
$('#showExpired').on('change', function() {
toggleExpired(this)
})
new DataTable('#nodes', { new DataTable('#nodes', {
scrollY: 130, scrollY: 130,
@ -138,7 +195,7 @@
keys: false, keys: false,
}); });
new DataTable('#paks', { new DataTable('#paks', {
scrollY: 130, scrollY: 230,
scrollCollapse: true, scrollCollapse: true,
paging: false, paging: false,
// lengthMenu: [5, 10, 30, 50, { label: 'All', value: -1 }], // lengthMenu: [5, 10, 30, 50, { label: 'All', value: -1 }],

View File

@ -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 flask import redirect, session, url_for
from app import auth from app import auth
from flask import jsonify from flask import jsonify
from flask_pyoidc.user_session import UserSession 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() log = logging.getLogger()
main_blueprint = Blueprint('main', __name__) main_blueprint = Blueprint('main', __name__)
@main_blueprint.route('/', methods=['GET', 'POST']) @ main_blueprint.route('/', methods=['GET', 'POST'])
@auth.access_control('default') @ auth.access_control('default')
def index(): def index():
user_session = UserSession(session) user_session = UserSession(session)
hs_user = user_session.userinfo['email'].split('@')[0] hs_user = user_session.userinfo['email'].split('@')[0]
@ -26,35 +30,35 @@ def index():
session=user_session) session=user_session)
@main_blueprint.route('/token', methods=['GET', 'POST']) @ main_blueprint.route('/token', methods=['GET', 'POST'])
@auth.access_control('default') @ auth.access_control('default')
def token(): def token():
user_session = UserSession(session) user_session = UserSession(session)
return jsonify(user_session.userinfo) return jsonify(user_session.userinfo)
@main_blueprint.route('/call', methods=['GET', 'POST']) @ main_blueprint.route('/call', methods=['GET', 'POST'])
@auth.access_control('default') @ auth.access_control('default')
def call(): def call():
return "CALL OK" return "CALL OK"
@main_blueprint.route('/logout') @ main_blueprint.route('/logout')
@auth.oidc_logout @ auth.oidc_logout
def logout(): def logout():
return redirect('/') return redirect('/')
@main_blueprint.route('/nodes', methods=['GET']) @ main_blueprint.route('/nodes', methods=['GET'])
@auth.authorize_admins('default') @ auth.authorize_admins('default')
def nodes(): def nodes():
nodelist = Node().list() nodelist = Node().list()
return render_template('nodes.html', return render_template('nodes.html',
nodes=nodelist.nodes) nodes=nodelist.nodes)
@main_blueprint.route('/node/<int:nodeId>', methods=['GET']) @ main_blueprint.route('/node/<int:nodeId>', methods=['GET'])
@auth.authorize_admins('default') @ auth.authorize_admins('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
@ -69,8 +73,8 @@ def node(nodeId):
node=node) node=node)
@main_blueprint.route('/users', methods=['GET']) @ main_blueprint.route('/users', methods=['GET'])
@auth.authorize_admins('default') @ auth.authorize_admins('default')
def users(): def users():
userList = User().list() userList = User().list()
# Get online status of devices of the user # Get online status of devices of the user
@ -85,24 +89,27 @@ def users():
online=online) online=online)
@main_blueprint.route('/user/<userName>', methods=['GET']) @ main_blueprint.route('/user/<userName>', methods=['GET'])
@auth.authorize_admins('default') @ auth.authorize_admins('default')
def user(userName): def user(userName):
user = User().get(userName) user = User().get(userName)
userNodeList = [n for n in Node().list().nodes if n.user.name == 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) 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", return render_template("user.html",
user=user.user, user=user.user,
preauthKeys=validpak, defaultExpiry=expStr,
preauthKeys=preauthKeys.preAuthKeys,
userNodeList=userNodeList) userNodeList=userNodeList)
@main_blueprint.route('/routes', methods=['GET']) @ main_blueprint.route('/routes', methods=['GET'])
@auth.authorize_admins('default') @ auth.authorize_admins('default')
def routes(): def routes():
routes = Route().list() routes = Route().list()
@ -120,8 +127,8 @@ def routes():
routes=final) routes=final)
@main_blueprint.route('/routeToggle/<int:routeId>', methods=['GET']) @ main_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() routes = Route().list()
route = [r for r in routes.routes if r.id == routeId] 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")) return redirect(url_for("main.routes"))
@main_blueprint.route('/node/<int:nodeId>/expire', methods=['GET']) @ main_blueprint.route('/node/<int:nodeId>/expire', methods=['GET'])
@auth.authorize_admins('default') @ auth.authorize_admins('default')
def expireNode(nodeId: int): def expireNode(nodeId: int):
Node().expire(nodeId) Node().expire(nodeId)
return redirect(url_for("main.node", nodeId=nodeId)) return redirect(url_for("main.node", nodeId=nodeId))
@main_blueprint.route('/node/<int:nodeId>/list-expire', methods=['GET']) @ main_blueprint.route('/node/<int:nodeId>/list-expire', methods=['GET'])
@auth.authorize_admins('default') @ auth.authorize_admins('default')
def expireNodeList(nodeId: int): def expireNodeList(nodeId: int):
Node().expire(nodeId) Node().expire(nodeId)
return redirect(url_for("main.nodes")) return redirect(url_for("main.nodes"))
@main_blueprint.route('/node/<int:nodeId>/delete', methods=['GET']) @ main_blueprint.route('/node/<int:nodeId>/delete', methods=['GET'])
@auth.authorize_admins('default') @ auth.authorize_admins('default')
def deleteNode(nodeId: int): def deleteNode(nodeId: int):
Node().delete(nodeId) Node().delete(nodeId)
return redirect(url_for("main.nodes")) return redirect(url_for("main.nodes"))
@main_blueprint.route('/node/<int:nodeId>/rename/<newName>', methods=['GET']) @ main_blueprint.route('/node/<int:nodeId>/rename/<newName>', methods=['GET'])
@auth.authorize_admins('default') @ auth.authorize_admins('default')
def renameNode(nodeId: int, newName: str): def renameNode(nodeId: int, newName: str):
Node().rename(nodeId, newName) Node().rename(nodeId, newName)
return jsonify(dict(newName=newName)) return jsonify(dict(newName=newName))
@main_blueprint.route('/user/<userName>/delete', methods=['GET']) @ main_blueprint.route('/user/<userName>/delete', methods=['GET'])
@auth.authorize_admins('default') @ auth.authorize_admins('default')
def deleteUser(userName: str): def deleteUser(userName: str):
nodes = Node().byUser(userName) nodes = Node().byUser(userName)
for node in nodes.nodes: for node in nodes.nodes:
@ -171,3 +178,27 @@ def deleteUser(userName: str):
Node().delete(node.id) Node().delete(node.id)
User().delete(userName) User().delete(userName)
return redirect(url_for("main.users")) return redirect(url_for("main.users"))
@ main_blueprint.route('/user/<userName>/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/<userName>/expire/<key>', 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))