Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
379fef4b00
|
|||
|
bdba6db42d
|
|||
|
4fb45c41bd
|
|||
|
a1c66152ae
|
|||
|
2a38fb14dd
|
|||
|
d86b2b58c2
|
|||
|
b91e73f3a5
|
|||
|
71a3413cbe
|
|||
|
a1dadcd709
|
@@ -1,9 +1,7 @@
|
|||||||
from flask import Flask, render_template
|
from flask import Flask, render_template, g
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
from flask_mobility import Mobility
|
from flask_mobility import Mobility
|
||||||
from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata
|
|
||||||
|
|
||||||
|
|
||||||
from . import filters
|
from . import filters
|
||||||
from .lib import OIDCAuthentication
|
from .lib import OIDCAuthentication
|
||||||
@@ -53,9 +51,12 @@ def create_app(environment='development'):
|
|||||||
filters.init_app(app)
|
filters.init_app(app)
|
||||||
|
|
||||||
# Error handlers.
|
# Error handlers.
|
||||||
|
|
||||||
@app.errorhandler(HTTPException)
|
@app.errorhandler(HTTPException)
|
||||||
def handle_http_error(exc):
|
def handle_http_error(exc):
|
||||||
return render_template('error.html', error=exc), exc.code
|
return render_template('error.html', error=exc), exc.code
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject_auth():
|
||||||
|
return dict(auth=auth)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
17
app/lib.py
17
app/lib.py
@@ -56,11 +56,26 @@ class OIDCAuthentication(_OIDCAuth):
|
|||||||
userinfo = flask_session['userinfo']
|
userinfo = flask_session['userinfo']
|
||||||
return userinfo['email'].split('@')[0]
|
return userinfo['email'].split('@')[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def email(self) -> str:
|
||||||
|
userinfo = flask_session['userinfo']
|
||||||
|
return userinfo['email']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def login_name(self) -> str:
|
def login_name(self) -> str:
|
||||||
userinfo = flask_session['userinfo']
|
userinfo = flask_session['userinfo']
|
||||||
return userinfo.get('preferred_username', self.username)
|
return userinfo.get('preferred_username', self.username)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self) -> str:
|
||||||
|
userinfo = flask_session['userinfo']
|
||||||
|
return userinfo.get('name')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def groups(self) -> list:
|
||||||
|
userinfo = flask_session['userinfo']
|
||||||
|
return userinfo.get('groups')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isAdmin(self) -> bool:
|
def isAdmin(self) -> bool:
|
||||||
userinfo = flask_session['userinfo']
|
userinfo = flask_session['userinfo']
|
||||||
@@ -73,7 +88,7 @@ class OIDCAuthentication(_OIDCAuth):
|
|||||||
|
|
||||||
if len(authorized_groups):
|
if len(authorized_groups):
|
||||||
log.debug(f"'{self.username}' is a member of {
|
log.debug(f"'{self.username}' is a member of {
|
||||||
authorized_groups}")
|
authorized_groups}. isAdmin == True")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if self.username in admin_users:
|
if self.username in admin_users:
|
||||||
|
|||||||
@@ -118,3 +118,15 @@ i.disabled {
|
|||||||
span.expired {
|
span.expired {
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copy:hover {
|
||||||
|
transform: scale(1.5, 1.5);
|
||||||
|
-ms-transform: scale(1.5, 1.5)); /* IE 9 */
|
||||||
|
-webkit-transform: scale(1.5, 1.5);
|
||||||
|
}
|
||||||
|
.copy:hover::after {
|
||||||
|
content: "📄 click to copy";
|
||||||
|
font-size: 80%;
|
||||||
|
/* font-style:oblique; */
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,12 +14,10 @@ function renameNode(nodeId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createPKA(username) {
|
function createPKA(username) {
|
||||||
console.log(username);
|
|
||||||
var url = `${username}/pakcreate`;
|
var url = `${username}/pakcreate`;
|
||||||
var ephemereal = $("#ephemereal").is(":checked");
|
var ephemereal = $("#ephemereal").is(":checked");
|
||||||
var reusable = $("#reusable").is(":checked");
|
var reusable = $("#reusable").is(":checked");
|
||||||
var expiration = $("#expiration").val();
|
var expiration = $("#expiration").val();
|
||||||
console.log(expiration);
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -65,3 +63,29 @@ function toggleExpired(obj) {
|
|||||||
$(".pka-expired").addClass("pka-hide");
|
$(".pka-expired").addClass("pka-hide");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function backfillips(obj) {
|
||||||
|
var url = "backfillips";
|
||||||
|
var button = $(obj);
|
||||||
|
var original = button.html();
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
method: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
xhrFields: {
|
||||||
|
withCredentials: true,
|
||||||
|
},
|
||||||
|
data: {},
|
||||||
|
success: function (data) {
|
||||||
|
if (data.length) {
|
||||||
|
button.html("Updated");
|
||||||
|
} else {
|
||||||
|
button.html("Done");
|
||||||
|
}
|
||||||
|
setTimeout(function () {
|
||||||
|
button.html(original);
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
|
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav mr-auto">
|
<ul class="navbar-nav mr-auto">
|
||||||
|
{% if auth.isAdmin %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('main.nodes') }}">nodes</a>
|
<a class="nav-link" href="{{ url_for('main.nodes') }}">nodes</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('main.routes') }}">routes</a>
|
<a class="nav-link" href="{{ url_for('main.routes') }}">routes</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item me-right">
|
<li class="nav-item me-right">
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="jumbotron my-4">
|
<div class="jumbotron jumbotron-fluid my-4">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1>{{ '%s - %s' % (error.code, error.name) }}</h1>
|
<h1>Oops, something went wrong</h1>
|
||||||
|
<h1>{{ '%s - %s' % (error.code, error.name) }}</h2>
|
||||||
<p>{{ error.description }}.</p>
|
<p>{{ error.description }}.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,25 +2,26 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>
|
<h3>
|
||||||
Welcome, {{ session.userinfo.name }}
|
Welcome, {{ auth.full_name }}
|
||||||
</h3>
|
</h3>
|
||||||
<hr>
|
<hr>
|
||||||
<h4>authentication info</h4>
|
<h4>authentication info</h4>
|
||||||
<div class="row data">
|
<div class="row data">
|
||||||
<div class="col col-2">
|
<div class="col col-2">
|
||||||
<strong>email</strong>
|
<strong>username</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-6">
|
<div class="col col-6">
|
||||||
{{ session.userinfo.email }}
|
<span data-toggle="tooltip" data-placement="right" title="OIDC username: {{ auth.login_name }}">
|
||||||
<!-- {{ session.userinfo.email_verified | fancyBool | safe }} -->
|
{{ auth.username }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row data">
|
<div class="row data">
|
||||||
<div class="col col-2">
|
<div class="col col-2">
|
||||||
<strong>username</strong>
|
<strong>email</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-6">
|
<div class="col col-6">
|
||||||
{{ session.userinfo.preferred_username }}
|
{{ auth.email }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row data">
|
<div class="row data">
|
||||||
@@ -29,16 +30,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-6">
|
<div class="col col-6">
|
||||||
<i class="fas fa-angle-right"></i>
|
<i class="fas fa-angle-right"></i>
|
||||||
{% if session.userinfo.groups[0] in config['ADMIN_GROUPS'] %}
|
{% if auth.groups[0] in config['ADMIN_GROUPS'] %}
|
||||||
<span class="badge badge-pill badge-warning">
|
<span class="badge badge-pill badge-warning">
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge badge-pill badge-dark">
|
<span class="badge badge-pill badge-dark">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ session.userinfo.groups[0]}}
|
{{ auth.groups[0]}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% for group in session.userinfo.groups[1:] |sort %}
|
{% for group in auth.groups[1:] |sort %}
|
||||||
<div class="row data">
|
<div class="row data">
|
||||||
<div class="col col-2">
|
<div class="col col-2">
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
<div class="col col-8 float-left">
|
<div class="col col-8 float-left">
|
||||||
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
|
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
|
||||||
{{ node.createdAt | htime_dt }}
|
{{ node.createdAt | htime_dt }}
|
||||||
|
<span class="badge badge-pill badge-warning">
|
||||||
|
{{ node.registerMethod.name }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,11 +76,11 @@
|
|||||||
<h5>addresses</h5>
|
<h5>addresses</h5>
|
||||||
{% 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-6">
|
||||||
<span class="address copy"
|
<span class="address copy"
|
||||||
value="{{ ip }}">
|
value="{{ ip }}">
|
||||||
{{ ip }}
|
{{ ip }}
|
||||||
</spanundefined>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -129,7 +132,9 @@
|
|||||||
<strong>machineKey</strong>
|
<strong>machineKey</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-8 float-left">
|
<div class="col col-8 float-left">
|
||||||
|
<span class="copy" value="{{ node.machineKey }}">
|
||||||
<code>{{ node.machineKey }}</code>
|
<code>{{ node.machineKey }}</code>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -138,7 +143,9 @@
|
|||||||
<strong>nodeKey</strong>
|
<strong>nodeKey</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-8 float-left">
|
<div class="col col-8 float-left">
|
||||||
|
<span class="copy" value="{{ node.nodeKey }}">
|
||||||
<code>{{ node.nodeKey }}</code>
|
<code>{{ node.nodeKey }}</code>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -147,7 +154,9 @@
|
|||||||
<strong>discoKey</strong>
|
<strong>discoKey</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-8 float-left">
|
<div class="col col-8 float-left">
|
||||||
|
<span class="copy" value="{{ node.discoKey }}">
|
||||||
<code>{{ node.discoKey }}</code>
|
<code>{{ node.discoKey }}</code>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p></p>
|
<p></p>
|
||||||
@@ -217,7 +226,7 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
$(function () {
|
$(function () {
|
||||||
$('.address.copy').on('click', function() {
|
$('.copy').on('click', function() {
|
||||||
copyToClipboard(this)
|
copyToClipboard(this)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="row data justify-content-between">
|
||||||
|
<div class="col col-4">
|
||||||
<h3>nodes</h3>
|
<h3>nodes</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col col-2">
|
||||||
|
<span data-toggle="tooltip" data-placement="right" title="Recheck all IP addresses of all nodes">
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm" onClick="backfillips(this);">Backfill IP addresses</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<p></p>
|
<p></p>
|
||||||
<table id="nodes" class="display" style="width:100%">
|
<table id="nodes" class="display" style="width:100%">
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="no-sort">
|
<td class="no-sort">
|
||||||
<span data-toggle="tooltip" data-placement="right" title="delete">
|
<span data-toggle="tooltip" data-placement="right" title="delete">
|
||||||
<a class="nodeco" href="/user/{{user.name}}/delete">
|
<a class="nodeco" href="{{ url_for('rest.deleteUser', userName=user.name) }}">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask import render_template, Blueprint, request
|
from flask import render_template, Blueprint
|
||||||
from flask import redirect, session, url_for
|
from flask import redirect, session, url_for
|
||||||
from app import auth
|
from app import auth
|
||||||
|
|
||||||
@@ -38,13 +38,10 @@ def token():
|
|||||||
@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)
|
hs_user = auth.username
|
||||||
hs_user = user_session.userinfo['email'].split('@')[0]
|
|
||||||
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,
|
|
||||||
auth=auth)
|
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/logout')
|
@main_blueprint.route('/logout')
|
||||||
|
|||||||
@@ -4,18 +4,17 @@ 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 flask import jsonify
|
from flask import jsonify
|
||||||
|
|
||||||
from hsapi_client import Node, User, Route, PreAuthKey
|
from hsapi_client import Node, User, Route, PreAuthKey
|
||||||
from hsapi_client.preauthkeys import (v1CreatePreAuthKeyRequest,
|
from hsapi_client.preauthkeys import (v1CreatePreAuthKeyRequest,
|
||||||
v1ExpirePreAuthKeyRequest)
|
v1ExpirePreAuthKeyRequest)
|
||||||
|
from hsapi_client.nodes import v1BackfillNodeIPsResponse
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
# REST calls
|
|
||||||
|
|
||||||
|
# REST calls
|
||||||
rest_blueprint = Blueprint(
|
rest_blueprint = Blueprint(
|
||||||
'rest', __name__, url_prefix=os.getenv('APPLICATION_ROOT', '/'))
|
'rest', __name__, url_prefix=os.getenv('APPLICATION_ROOT', '/'))
|
||||||
|
|
||||||
@@ -64,8 +63,11 @@ def deleteNode(nodeId: int):
|
|||||||
|
|
||||||
|
|
||||||
@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))
|
||||||
|
|
||||||
@@ -103,3 +105,10 @@ def expirePKA(userName: str, key: str):
|
|||||||
|
|
||||||
PreAuthKey().expire(req)
|
PreAuthKey().expire(req)
|
||||||
return redirect(url_for('main.user', userName=userName))
|
return redirect(url_for('main.user', userName=userName))
|
||||||
|
|
||||||
|
|
||||||
|
@rest_blueprint.route('/backfillips', methods=['POST'])
|
||||||
|
@auth.authorize_admins('default')
|
||||||
|
def backfillips():
|
||||||
|
response = Node().backfillips()
|
||||||
|
return jsonify(response.changes)
|
||||||
|
|||||||
6
poetry.lock
generated
6
poetry.lock
generated
@@ -452,13 +452,13 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hsapi-client"
|
name = "hsapi-client"
|
||||||
version = "0.9.5"
|
version = "0.9.7"
|
||||||
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.5-py3-none-any.whl", hash = "sha256:a2ef7a62fba6f31ad08d6c04db95306c1da10c255383c699d04aa2dbd293f743"},
|
{file = "hsapi_client-0.9.7-py3-none-any.whl", hash = "sha256:6cd8ac2a787112a02d7d5d3e029ceba0749844806b20b3c27247393cccd53def"},
|
||||||
{file = "hsapi_client-0.9.5.tar.gz", hash = "sha256:aa8bf51a960c8e472b8a423bd7de5f7d9514ca5706e2b98ac473d57d158767f6"},
|
{file = "hsapi_client-0.9.7.tar.gz", hash = "sha256:7a6bf7cb533a4f0431c322bc292f09559eb27b37177ea2101a6ea559dc0c9e47"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "hsman"
|
name = "hsman"
|
||||||
version = "0.9.10"
|
version = "0.9.17"
|
||||||
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"
|
||||||
|
|||||||
2
wsgi.py
2
wsgi.py
@@ -19,7 +19,7 @@ log.debug(f"Running in web mode: {lib.webMode()}")
|
|||||||
def get_context():
|
def get_context():
|
||||||
# flask cli context setup
|
# flask cli context setup
|
||||||
"""Objects exposed here will be automatically available from the shell."""
|
"""Objects exposed here will be automatically available from the shell."""
|
||||||
return dict(app=app, models=models)
|
return dict(app=app)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Reference in New Issue
Block a user