Support for policy upload/download

Plus a small bugfix for groups
This commit is contained in:
Andrea Mistrali 2024-12-17 12:10:40 +01:00
parent 7fa17adfb1
commit 31910dc034
Signed by: andre
SSH Key Fingerprint: SHA256:/D780pZnuHMQ8xFII5lAtXWy8zdowtBhgWjwi88p+lI
10 changed files with 646 additions and 508 deletions

View File

@ -74,7 +74,7 @@ class OIDCAuthentication(_OIDCAuth):
@property @property
def groups(self) -> list: def groups(self) -> list:
userinfo = flask_session['userinfo'] userinfo = flask_session['userinfo']
return userinfo.get('groups') return userinfo.get('groups') or []
@property @property
def isAdmin(self) -> bool: def isAdmin(self) -> bool:

View File

@ -64,7 +64,7 @@ a:active {
text-decoration: none; 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-line: none;
text-decoration-style: unset; text-decoration-style: unset;
} }

View File

@ -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);
},
});
}

View File

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

View File

@ -5,10 +5,12 @@
<div class="col col-4"> <div class="col col-4">
<h3>nodes</h3> <h3>nodes</h3>
</div> </div>
<div class="col col-2"> <div class="col col-4">
<span data-toggle="tooltip" data-placement="right" title="Recheck all IP addresses of all nodes"> <div class="float-right">
<button type="button" class="btn btn-outline-primary btn-sm" onClick="backfillips(this);">Backfill IP addresses</button> <span data-toggle="tooltip" data-placement="right" title="Recheck all IP addresses of all nodes">
</span> <button type="button" class="btn btn-outline-primary btn-sm" onClick="backfillips(this);">Backfill IP addresses</button>
</span>
</div>
</div> </div>
</div> </div>
<hr> <hr>

View File

@ -1,13 +1,30 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="row data justify-content-between">
<h3>policy</h3> <div class="col col-4">
<footer class="blockquote-footer"> <h3>policy</h3>
for more info see <a href="https://tailscale.com/kb/1337/acl-syntax" target="_blank">tailscale docs</a> <footer class="blockquote-footer">
</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="/policy/view" target="_blank">View</a></button>
<button type="button" class="btn btn-outline-primary btn-sm plain"><a class="plain" href="/policy/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> <hr>
<div class="row"> <div class="row">
<div class="col col-6"> <div class="col col-6">
<div class="row"> <div class="row">
@ -79,8 +96,29 @@
</div> </div>
</div> </div>
<!-- <textarea readonly rows="30" style="width: 80%; height: 80%;"> <!-- upload acl modal -->
{{ policy.json }} <div class="modal fade" id="uploadACL" tabindex="-1" role="dialog" aria-labelledby="uploadACL" aria-hidden="true">
</textarea> --> <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>
{% endblock %} {% endblock %}

View File

@ -8,7 +8,7 @@ from app import auth
# from ..lib import username # from ..lib import username
from flask import jsonify from flask import jsonify, make_response
from flask_pyoidc.user_session import UserSession from flask_pyoidc.user_session import UserSession
from hsapi_client import Node, User, Route, PreAuthKey, Policy from hsapi_client import Node, User, Route, PreAuthKey, Policy
@ -132,9 +132,24 @@ def routes():
routes=final) 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') @auth.authorize_admins('default')
def policy(): def policy(action):
log.debug(f"action: {action}")
policy = Policy().get() policy = Policy().get()
return render_template("policy.html", if action == "view":
policy=policy) 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,11 +4,14 @@ 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 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, from hsapi_client.preauthkeys import (v1CreatePreAuthKeyRequest,
v1ExpirePreAuthKeyRequest) v1ExpirePreAuthKeyRequest)
from hsapi_client.policies import v1Policy
from hsapi_client.config import HTTPException
from app.lib import remote_ip from app.lib import remote_ip
@ -111,3 +114,21 @@ def expirePKA(userName: str, key: str):
def backfillips(): def backfillips():
response = Node().backfillips() response = Node().backfillips()
return jsonify(response.changes) 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

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,8 @@ flask-mobility = "^2.0.1"
humanize = "^4.9.0" humanize = "^4.9.0"
flask-pydantic = "^0.12.0" flask-pydantic = "^0.12.0"
uvicorn = "^0.30.1" 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] [tool.poetry.group.dev.dependencies]