Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
9fcae05d20 | |||
0409ac3d08
|
|||
425a1cd094
|
|||
31910dc034
|
|||
7fa17adfb1
|
@ -74,7 +74,7 @@ class OIDCAuthentication(_OIDCAuth):
|
||||
@property
|
||||
def groups(self) -> list:
|
||||
userinfo = flask_session['userinfo']
|
||||
return userinfo.get('groups')
|
||||
return userinfo.get('groups') or []
|
||||
|
||||
@property
|
||||
def isAdmin(self) -> bool:
|
||||
|
7
app/static/bootstrap/bootstrap-grid.min.css
vendored
Normal file
7
app/static/bootstrap/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
8
app/static/bootstrap/bootstrap-reboot.min.css
vendored
Normal file
8
app/static/bootstrap/bootstrap-reboot.min.css
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* 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 */
|
7
app/static/bootstrap/bootstrap.bundle.min.js
vendored
Normal file
7
app/static/bootstrap/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
11
app/static/bootstrap/bootstrap.min.css
vendored
11
app/static/bootstrap/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
8
app/static/bootstrap/bootstrap.min.js
vendored
8
app/static/bootstrap/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
@ -64,7 +64,7 @@ a:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.nodeco, a.nav-link, a.navbar-brand, a.routeToggle {
|
||||
a.nodeco, a.nav-link, a.navbar-brand, a.routeToggle, a.plain {
|
||||
text-decoration-line: none;
|
||||
text-decoration-style: unset;
|
||||
}
|
||||
|
@ -89,3 +89,39 @@ 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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -41,16 +41,16 @@
|
||||
</div>
|
||||
<div class="col col-6">
|
||||
<i class="fas fa-angle-right"></i>
|
||||
{% if auth.groups[0] in config['ADMIN_GROUPS'] %}
|
||||
{% if not auth.groups[0] or 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]}}
|
||||
{{ auth.groups[0] | default('no group')}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% for group in auth.groups[1:] |sort %}
|
||||
{% for group in auth.groups[1:] | default([]) |sort %}
|
||||
<div class="row data">
|
||||
<div class="col col-2">
|
||||
|
||||
|
@ -5,10 +5,12 @@
|
||||
<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 class="col col-4">
|
||||
<div class="float-right">
|
||||
<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>
|
||||
|
@ -1,13 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<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 class="row data justify-content-between">
|
||||
<div class="col col-4">
|
||||
<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>
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-6">
|
||||
<div class="row">
|
||||
@ -79,8 +96,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <textarea readonly rows="30" style="width: 80%; height: 80%;">
|
||||
{{ policy.json }}
|
||||
</textarea> -->
|
||||
|
||||
<!-- 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">×</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>
|
||||
{% endblock %}
|
||||
|
@ -8,7 +8,7 @@ from app import auth
|
||||
|
||||
# from ..lib import username
|
||||
|
||||
from flask import jsonify
|
||||
from flask import jsonify, make_response
|
||||
from flask_pyoidc.user_session import UserSession
|
||||
|
||||
from hsapi_client import Node, User, Route, PreAuthKey, Policy
|
||||
@ -132,9 +132,23 @@ def routes():
|
||||
routes=final)
|
||||
|
||||
|
||||
@main_blueprint.route('/policy', methods=['GET'])
|
||||
@main_blueprint.route('/policy', defaults={'action': None}, methods=['GET'])
|
||||
@main_blueprint.route('/policy/<action>', methods=['GET'])
|
||||
@auth.authorize_admins('default')
|
||||
def policy():
|
||||
def policy(action):
|
||||
policy = Policy().get()
|
||||
return render_template("policy.html",
|
||||
policy=policy)
|
||||
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)
|
||||
|
@ -4,11 +4,14 @@ from flask import Blueprint, request
|
||||
from flask import redirect, url_for
|
||||
from app import auth
|
||||
|
||||
from flask import jsonify
|
||||
from flask import jsonify, make_response
|
||||
|
||||
from hsapi_client import Node, User, Route, PreAuthKey
|
||||
from hsapi_client import Node, User, Route, PreAuthKey, Policy
|
||||
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
|
||||
|
||||
@ -111,3 +114,21 @@ 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
|
||||
|
987
poetry.lock
generated
987
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "hsman"
|
||||
version = "0.9.20"
|
||||
version = "0.9.21"
|
||||
description = "Flask Admin webui for Headscale"
|
||||
authors = ["Andrea Mistrali <andrea@mistrali.pw>"]
|
||||
license = "BSD"
|
||||
@ -15,7 +15,8 @@ flask-mobility = "^2.0.1"
|
||||
humanize = "^4.9.0"
|
||||
flask-pydantic = "^0.12.0"
|
||||
uvicorn = "^0.30.1"
|
||||
hsapi-client = "^0.9.8"
|
||||
hsapi-client = "^0.9.9"
|
||||
# hsapi_client = { path = "../hsapi-client", develop = true }
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
Reference in New Issue
Block a user