Compare commits

..

No commits in common. "master" and "0.9.20" have entirely different histories.

19 changed files with 547 additions and 852 deletions

1
.gitignore vendored
View File

@ -266,4 +266,3 @@ tags
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
.flaskenv
docker.env
flask_session/

View File

@ -2,7 +2,6 @@ from flask import Flask, render_template, g
from werkzeug.exceptions import HTTPException
from flask_mobility import Mobility
from flask_session import Session
from . import filters
from .lib import OIDCAuthentication
@ -11,8 +10,6 @@ import os
mobility = Mobility()
auth = OIDCAuthentication()
# SESSION_TYPE = 'filesystem'
sess = Session()
def create_app(environment='development'):
@ -53,8 +50,6 @@ def create_app(environment='development'):
app.logger.info("jinja2 custom filters loaded")
filters.init_app(app)
sess.init_app(app)
# Error handlers.
@app.errorhandler(HTTPException)
def handle_http_error(exc):

View File

@ -5,7 +5,7 @@ from flask import request, abort, current_app
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, ProviderMetadata
from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata
from typing import Callable, List
@ -51,36 +51,35 @@ class OIDCAuthentication(_OIDCAuth):
super().init_app(app)
app.auth = self
@property
def userinfo(self) -> dict:
log.debug(flask_session.get('userinfo', {}))
return flask_session.get('userinfo', {})
@property
def username(self) -> str:
# This need to be changed after upgrading headscale version
# when hs will use the preferred_username field as username
return self.email.split('@')[0]
userinfo = flask_session['userinfo']
return userinfo['email'].split('@')[0]
@property
def email(self) -> str:
return self.userinfo.get('email', 'unknown')
userinfo = flask_session['userinfo']
return userinfo['email']
@property
def login_name(self) -> str:
return self.userinfo.get('preferred_username', self.username)
userinfo = flask_session['userinfo']
return userinfo.get('preferred_username', self.username)
@property
def full_name(self) -> str:
return self.userinfo.get('name', self.username)
userinfo = flask_session['userinfo']
return userinfo.get('name')
@property
def groups(self) -> list:
return self.userinfo.get('groups', [])
userinfo = flask_session['userinfo']
return userinfo.get('groups')
@property
def isAdmin(self) -> bool:
user_groups = self.userinfo.get('groups', [])
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', [])

File diff suppressed because one or more lines are too long

View File

@ -1,8 +0,0 @@
/*!
* Bootstrap Reboot v4.6.2 (https://getbootstrap.com/)
* Copyright 2011-2022 The Bootstrap Authors
* Copyright 2011-2022 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -64,7 +64,7 @@ a:active {
text-decoration: none;
}
a.nodeco, a.nav-link, a.navbar-brand, a.routeToggle, a.plain {
a.nodeco, a.nav-link, a.navbar-brand, a.routeToggle {
text-decoration-line: none;
text-decoration-style: unset;
}

View File

@ -89,39 +89,3 @@ function backfillips(obj) {
},
});
}
function uploadACL(obj) {
var fd = new FormData();
var files = $("#upload")[0].files[0];
fd.append("file", files);
// When we close the modal, we reload the page
$("#uploadACL").on("hidden.bs.modal", function (event) {
location.reload();
});
$.ajax({
url: "policy/upload",
type: "POST",
xhrFields: {
withCredentials: true,
},
data: fd,
contentType: false,
processData: false,
success: function (response) {
if (response != 0) {
$("#output").html("acl updated");
} else {
$("#output").html("acl not updated");
}
setTimeout(() => {
$("#uploadACL").modal("hide");
}, 5000);
},
error: function (response) {
console.log(response.responseJSON.message);
$("#output").html(response.responseJSON.message);
},
});
}

View File

@ -41,16 +41,16 @@
</div>
<div class="col col-6">
<i class="fas fa-angle-right"></i>
{% if not auth.groups[0] or auth.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 %}
{{ auth.groups[0] | default('no group')}}
{{ auth.groups[0]}}
</span>
</div>
</div>
{% for group in auth.groups[1:] | default([]) |sort %}
{% for group in auth.groups[1:] |sort %}
<div class="row data">
<div class="col col-2">
&nbsp;

View File

@ -5,13 +5,11 @@
<div class="col col-4">
<h3>nodes</h3>
</div>
<div class="col col-4">
<div class="float-right">
<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>
</div>
<hr>
<table id="nodes" class="display" style="width:100%">

View File

@ -1,30 +1,13 @@
{% extends "base.html" %}
{% block content %}
<div class="row data justify-content-between">
<div class="col col-4">
<h3>policy</h3>
<footer class="blockquote-footer">
<h3>policy</h3>
<footer class="blockquote-footer">
for more info see <a href="https://tailscale.com/kb/1337/acl-syntax" target="_blank">tailscale docs</a>
</footer>
</div>
<div class="col col-4">
<div class="float-right">
last update:
<span data-toggle="tooltip"
data-placement="right"
title="{{ policy.updatedAt | fmt_datetime }}">
{{policy.updatedAt | htime_dt }}
</span>
<div>
<button type="button" class="btn btn-outline-primary btn-sm plain"><a class="plain" href="{{ url_for('main.policy', action='view') }}" target="_blank">View</a></button>
<button type="button" class="btn btn-outline-primary btn-sm plain"><a class="plain" href="{{ url_for('main.policy', action='download') }}">Download</a></button>
<button type="button" class="btn btn-outline-primary btn-sm plain" data-toggle="modal" data-target="#uploadACL">Upload</button>
</div>
</div>
</div>
</div>
</footer>
<hr>
<div class="row">
<div class="col col-6">
<div class="row">
@ -96,29 +79,8 @@
</div>
</div>
<!-- upload acl modal -->
<div class="modal fade" id="uploadACL" tabindex="-1" role="dialog" aria-labelledby="uploadACL" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="renameModalLabel">upload ACL</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="file" name="upload" id="upload">
</div>
<div>
<span id="output"></span>
</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="uploadACL(this);">Upload</button>
</div>
</div>
</div>
</div>
<!-- <textarea readonly rows="30" style="width: 80%; height: 80%;">
{{ policy.json }}
</textarea> -->
{% endblock %}

View File

@ -6,7 +6,9 @@ from flask import render_template, Blueprint
from flask import redirect, session, url_for
from app import auth
from flask import jsonify, make_response
# from ..lib import username
from flask import jsonify
from flask_pyoidc.user_session import UserSession
from hsapi_client import Node, User, Route, PreAuthKey, Policy
@ -45,8 +47,6 @@ def index():
@main_blueprint.route('/logout')
@auth.oidc_logout
def logout():
# UserSession(session).clear()
session.clear()
return redirect(url_for('main.index'))
@ -132,23 +132,9 @@ def routes():
routes=final)
@main_blueprint.route('/policy', defaults={'action': None}, methods=['GET'])
@main_blueprint.route('/policy/<action>', methods=['GET'])
@main_blueprint.route('/policy', methods=['GET'])
@auth.authorize_admins('default')
def policy(action):
def policy():
policy = Policy().get()
if action == "view":
return policy.json
elif action == "download":
updateStr = policy.updatedAt.strftime(format='%Y%m%d-%H%M')
log.debug(updateStr)
filename = f"acl-{updateStr}.json"
response = make_response(policy.json)
response.headers['Content-Disposition'] = f'attachment; filename={
filename}'
return response
else:
return render_template("policy.html",
policy=policy)

View File

@ -4,14 +4,11 @@ from flask import Blueprint, request
from flask import redirect, url_for
from app import auth
from flask import jsonify, make_response
from flask import jsonify
from hsapi_client import Node, User, Route, PreAuthKey, Policy
from hsapi_client import Node, User, Route, PreAuthKey
from hsapi_client.preauthkeys import (v1CreatePreAuthKeyRequest,
v1ExpirePreAuthKeyRequest)
from hsapi_client.policies import v1Policy
from hsapi_client.config import HTTPException
from app.lib import remote_ip
@ -114,21 +111,3 @@ def expirePKA(userName: str, key: str):
def backfillips():
response = Node().backfillips()
return jsonify(response.changes)
@rest_blueprint.route('/policy/upload', methods=['POST'])
@auth.authorize_admins('default')
def policyUpload():
file = request.files['file']
try:
acl = ''.join(map(bytes.decode, file.readlines()))
except UnicodeDecodeError as e:
log.debug(e)
return jsonify(message=str(e)), 422
policy = Policy()
try:
policy.put(acl)
except HTTPException as e:
return jsonify(message=str(e.message)), 422
return jsonify(message="acl updated"), 200

View File

@ -13,13 +13,8 @@ class BaseConfig(object):
APP_PREFIX = os.getenv('APP_PREFIX', '')
DEBUG_TB_ENABLED = False
WTF_CSRF_ENABLED = False
# Session
# We store sessions in filesystem, max 100 files, expire in 2 hours
SESSION_TYPE = 'filesystem'
SESSION_FILE_THRESHOLD = 100
PERMANENT_SESSION_LIFETIME = 7200
# All the following vars can be overriden
# All the followinf vars can be overriden
# in the environment, using `HSMAN_` prefix
SECRET_KEY = "secreto"
ADMIN_GROUPS = "adminGroup"
@ -27,7 +22,6 @@ class BaseConfig(object):
OIDC_CLIENT_SECRET = 'client-secreto'
OIDC_URL = "https://myidp.example.com/auth"
OIDC_REDIRECT_URI = 'http://localhost:5000/auth'
OIDC_CLOCK_SKEW = 30
# These are required by hsapi, should not be defined here
# HSAPI_SERVER = "https://headscale.example.com"

View File

@ -17,7 +17,7 @@ RUN apk --update --no-cache add \
libffi-dev \
curl && \
chmod g+w /run && \
pip install poetry gunicorn poetry-plugin-export
pip install poetry gunicorn
COPY . /hsman

1143
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,10 @@
[tool.poetry]
name = "hsman"
version = "0.9.24"
version = "0.9.20"
description = "Flask Admin webui for Headscale"
authors = ["Andrea Mistrali <andrea@mistrali.pw>"]
license = "BSD"
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = ">=3.11,<4.0"
@ -16,9 +15,7 @@ flask-mobility = "^2.0.1"
humanize = "^4.9.0"
flask-pydantic = "^0.12.0"
uvicorn = "^0.30.1"
hsapi-client = "^0.9.9"
# hsapi_client = { path = "../hsapi-client", develop = true }
flask-session = "^0.8.0"
hsapi-client = "^0.9.8"
[tool.poetry.group.dev.dependencies]