3 Commits

Author SHA1 Message Date
bdba6db42d Add support for backfillips 2024-09-10 11:51:53 +02:00
4fb45c41bd Bump version 2024-09-05 11:13:16 +02:00
a1c66152ae Fixed view for non admin users 2024-09-05 11:12:55 +02:00
13 changed files with 91 additions and 36 deletions

View File

@@ -1,9 +1,7 @@
from flask import Flask, render_template
from flask import Flask, render_template, g
from werkzeug.exceptions import HTTPException
from flask_mobility import Mobility
from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata
from . import filters
from .lib import OIDCAuthentication
@@ -53,9 +51,12 @@ def create_app(environment='development'):
filters.init_app(app)
# Error handlers.
@app.errorhandler(HTTPException)
def handle_http_error(exc):
return render_template('error.html', error=exc), exc.code
@app.context_processor
def inject_auth():
return dict(auth=auth)
return app

View File

@@ -56,11 +56,26 @@ class OIDCAuthentication(_OIDCAuth):
userinfo = flask_session['userinfo']
return userinfo['email'].split('@')[0]
@property
def email(self) -> str:
userinfo = flask_session['userinfo']
return userinfo['email']
@property
def login_name(self) -> str:
userinfo = flask_session['userinfo']
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
def isAdmin(self) -> bool:
userinfo = flask_session['userinfo']
@@ -73,7 +88,7 @@ class OIDCAuthentication(_OIDCAuth):
if len(authorized_groups):
log.debug(f"'{self.username}' is a member of {
authorized_groups}")
authorized_groups}. isAdmin == True")
return True
if self.username in admin_users:

View File

@@ -14,12 +14,10 @@ function renameNode(nodeId) {
}
function createPKA(username) {
console.log(username);
var url = `${username}/pakcreate`;
var ephemereal = $("#ephemereal").is(":checked");
var reusable = $("#reusable").is(":checked");
var expiration = $("#expiration").val();
console.log(expiration);
$.ajax({
url: url,
method: "POST",
@@ -65,3 +63,29 @@ function toggleExpired(obj) {
$(".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);
},
});
}

View File

@@ -50,6 +50,7 @@
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
{% if auth.isAdmin %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.nodes') }}">nodes</a>
</li>
@@ -59,6 +60,7 @@
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.routes') }}">routes</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
<li class="nav-item me-right">

View File

@@ -1,10 +1,11 @@
{% extends "base.html" %}
{% block content %}
<div class="jumbotron my-4">
<div class="jumbotron jumbotron-fluid my-4">
<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>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@@ -2,25 +2,26 @@
{% block content %}
<h3>
Welcome, {{ session.userinfo.name }}
Welcome, {{ auth.full_name }}
</h3>
<hr>
<h4>authentication info</h4>
<div class="row data">
<div class="col col-2">
<strong>email</strong>
<strong>username</strong>
</div>
<div class="col col-6">
{{ session.userinfo.email }}
<!-- {{ session.userinfo.email_verified | fancyBool | safe }} -->
<span data-toggle="tooltip" data-placement="right" title="OIDC username: {{ auth.login_name }}">
{{ auth.username }}
</span>
</div>
</div>
<div class="row data">
<div class="col col-2">
<strong>username</strong>
<strong>email</strong>
</div>
<div class="col col-6">
{{ session.userinfo.preferred_username }}
{{ auth.email }}
</div>
</div>
<div class="row data">
@@ -29,16 +30,16 @@
</div>
<div class="col col-6">
<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">
{% else %}
<span class="badge badge-pill badge-dark">
{% endif %}
{{ session.userinfo.groups[0]}}
{{ auth.groups[0]}}
</span>
</div>
</div>
{% for group in session.userinfo.groups[1:] |sort %}
{% for group in auth.groups[1:] |sort %}
<div class="row data">
<div class="col col-2">
&nbsp;

View File

@@ -39,7 +39,6 @@
<span class="badge badge-pill badge-warning">
{{ node.registerMethod.name }}
</span>
</span>
</div>
</div>

View File

@@ -1,7 +1,16 @@
{% extends "base.html" %}
{% block content %}
<h3>nodes</h3>
<div class="row data justify-content-between">
<div class="col col-4">
<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>
<p></p>
<table id="nodes" class="display" style="width:100%">

View File

@@ -2,7 +2,7 @@ import logging
import datetime
import os
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 app import auth
@@ -38,13 +38,10 @@ def token():
@main_blueprint.route('/', methods=['GET', 'POST'])
@auth.access_control('default')
def index():
user_session = UserSession(session)
hs_user = user_session.userinfo['email'].split('@')[0]
hs_user = auth.username
userNodeList = [n for n in Node().list().nodes if n.user.name == hs_user]
return render_template('index.html',
userNodeList=userNodeList,
session=user_session,
auth=auth)
userNodeList=userNodeList)
@main_blueprint.route('/logout')

View File

@@ -4,18 +4,17 @@ from flask import Blueprint, request
from flask import redirect, url_for
from app import auth
# from ..lib import login_name, username
from flask import jsonify
from hsapi_client import Node, User, Route, PreAuthKey
from hsapi_client.preauthkeys import (v1CreatePreAuthKeyRequest,
v1ExpirePreAuthKeyRequest)
from hsapi_client.nodes import v1BackfillNodeIPsResponse
log = logging.getLogger()
# REST calls
# REST calls
rest_blueprint = Blueprint(
'rest', __name__, url_prefix=os.getenv('APPLICATION_ROOT', '/'))
@@ -30,7 +29,7 @@ def routeToggle(routeId: int):
else:
action = 'enabled'
log.info(
f"route '{route.prefix}' via '{route.node.givenName}'"
f"route '{route.prefix}' via '{route.node.givenName}' "
f"{action} by '{auth.username}'")
Route().toggle(routeId)
return redirect(request.referrer)
@@ -106,3 +105,10 @@ def expirePKA(userName: str, key: str):
PreAuthKey().expire(req)
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
View File

@@ -452,13 +452,13 @@ files = [
[[package]]
name = "hsapi-client"
version = "0.9.6"
version = "0.9.7"
description = "Headscale API client"
optional = false
python-versions = "<4.0,>=3.11"
files = [
{file = "hsapi_client-0.9.6-py3-none-any.whl", hash = "sha256:441cd219a2384f66511b8cca21224171b4e6753d16d364d984eb9887aa686a6c"},
{file = "hsapi_client-0.9.6.tar.gz", hash = "sha256:b6a4183fb9cdf95b0e864eec5b79ea18843e25379f928c4770b68e4f1ce8334b"},
{file = "hsapi_client-0.9.7-py3-none-any.whl", hash = "sha256:6cd8ac2a787112a02d7d5d3e029ceba0749844806b20b3c27247393cccd53def"},
{file = "hsapi_client-0.9.7.tar.gz", hash = "sha256:7a6bf7cb533a4f0431c322bc292f09559eb27b37177ea2101a6ea559dc0c9e47"},
]
[package.dependencies]

View File

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

View File

@@ -19,7 +19,7 @@ log.debug(f"Running in web mode: {lib.webMode()}")
def get_context():
# flask cli context setup
"""Objects exposed here will be automatically available from the shell."""
return dict(app=app, models=models)
return dict(app=app)
if __name__ == '__main__':