add more pki features, bug fixes, try not to use jquery
This commit is contained in:
parent
38932aef44
commit
6c388c8129
|
@ -16,7 +16,7 @@ LDAP_BIND_PW = '123456'
|
|||
|
||||
PKI_PATH = f'{DATA_FOLDER}/pki'
|
||||
DOMAIN = 'example.com'
|
||||
SERVER_NAME = f'account.{ DOMAIN }:9090'
|
||||
#SERVER_NAME = f'account.{ DOMAIN }:9090'
|
||||
|
||||
HYDRA_REQUEST_TIMEOUT_SECONDS = 3
|
||||
HYDRA_ADMIN_URL = 'http://127.0.0.1:4445'
|
||||
|
|
45
browser_app/confirm-modal.js
Normal file
45
browser_app/confirm-modal.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
"use strict";
|
||||
|
||||
const $ = document.querySelector.bind(document)
|
||||
const _ = document.getElementById
|
||||
|
||||
export class ConfirmDialog {
|
||||
|
||||
constructor(message) {
|
||||
this._div = document.getElementById('confirm-dialog-template').content.querySelector('div').cloneNode(true);
|
||||
this._div.querySelector('.modal-body').innerHTML = message;
|
||||
}
|
||||
|
||||
show() {
|
||||
var self = this;
|
||||
this._promise = new Promise((resolve, reject) => {
|
||||
self._resolve = resolve;
|
||||
self._reject = reject;
|
||||
});
|
||||
|
||||
this._div.querySelectorAll('.close').forEach(function (o){
|
||||
o.onclick=self.cancel.bind(self);
|
||||
});
|
||||
|
||||
this._div.querySelector('.process').onclick = () => {
|
||||
self._close();
|
||||
self._resolve();
|
||||
};
|
||||
|
||||
$('.messages-box').appendChild(this._div);
|
||||
return this._promise
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this._close()
|
||||
this._reject('canceled by user');
|
||||
}
|
||||
|
||||
_close() {
|
||||
$('.messages-box').removeChild(this._div);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,14 +1,20 @@
|
|||
import 'jquery';
|
||||
import 'bootstrap';
|
||||
import 'jquery-form'
|
||||
import {ConfirmDialog} from './confirm-modal.js';
|
||||
|
||||
window.$ =window.jQuery = require('jquery');
|
||||
jQuery = window.$ = window.jQuery = require('jquery');
|
||||
var forge = require('node-forge');
|
||||
var QRCode = require("qrcode-svg");
|
||||
var pki = require('node-forge/lib/pki');
|
||||
var asn1 = require('node-forge/lib/asn1');
|
||||
var pkcs12 = require('node-forge/lib/pkcs12');
|
||||
var util = require('node-forge/lib/util');
|
||||
import SimpleFormSubmit from "simple-form-submit";
|
||||
|
||||
const $ = document.querySelector.bind(document);
|
||||
const $$ = document.querySelectorAll.bind(document);
|
||||
|
||||
|
||||
/*
|
||||
Convert an ArrayBuffer into a string
|
||||
|
@ -32,12 +38,13 @@ function randBase32() {
|
|||
return result;
|
||||
}
|
||||
|
||||
window.$(document).ready(function () {
|
||||
$('#sidebarCollapse').on('click', function () {
|
||||
$('nav.sidebar').toggleClass('d-none');
|
||||
});
|
||||
});
|
||||
window.ConfirmDialog = ConfirmDialog;
|
||||
|
||||
window.$(document).ready(function () {
|
||||
$('#sidebarCollapse').onclick = function () {
|
||||
$('nav.sidebar').classList.toggle('d-none');
|
||||
};
|
||||
});
|
||||
|
||||
window.totp = {
|
||||
init_list: function(){
|
||||
|
@ -46,18 +53,18 @@ window.totp = {
|
|||
//create new TOTP secret, create qrcode and ask for token.
|
||||
var form = $('form');
|
||||
var secret = randBase32();
|
||||
var input_secret = form.find('#secret')
|
||||
if(input_secret.val() == '') {
|
||||
input_secret.val(secret);
|
||||
var input_secret = form.querySelector('#secret')
|
||||
if(input_secret.value == '') {
|
||||
input_secret.value = secret;
|
||||
}
|
||||
|
||||
form.find('#name').on('change',window.totp.generate_qrcode);
|
||||
form.querySelector('#name').on('change',window.totp.generate_qrcode);
|
||||
window.totp.generate_qrcode();
|
||||
},
|
||||
generate_qrcode: function(){
|
||||
var form = $('form');
|
||||
var secret = form.find('#secret').val();
|
||||
var name = form.find('#name').val();
|
||||
var secret = form.querySelector('#secret').value;
|
||||
var name = form.querySelector('#name').value;
|
||||
var issuer = 'Lenticular%20Cloud';
|
||||
var svg_container = $('#svg-container')
|
||||
var svg = new QRCode(`otpauth://totp/${issuer}:${name}?secret=${secret}&issuer=${issuer}`).svg();
|
||||
|
@ -84,29 +91,27 @@ window.client_cert = {
|
|||
},
|
||||
generate_private_key: function() {
|
||||
var form = $('form#gen-key-form');
|
||||
var key_size = form.find('#key-size').val();
|
||||
var valid_time = form.find('input[name=valid_time]').val();
|
||||
$('button#generate-key')[0].style['display'] = 'none';
|
||||
var key_size = form.querySelector('#key-size').value;
|
||||
var valid_time = form.querySelector('input[name=valid_time]').value;
|
||||
$('button#generate-key').style['display'] = 'none';
|
||||
pki.rsa.generateKeyPair({bits: key_size, workers: 2}, function(err, keypair) {
|
||||
console.log(keypair);
|
||||
form.data('keypair', keypair);
|
||||
|
||||
//returns the exported key to a hidden form
|
||||
var form_sign_key = $('#gen-key-sign form');
|
||||
form_sign_key.find('textarea[name=publickey]').val(pki.publicKeyToPem(keypair.publicKey));
|
||||
form_sign_key.find('input[name=valid_time]').val(valid_time);
|
||||
form_sign_key.querySelector('textarea[name=publickey]').value = pki.publicKeyToPem(keypair.publicKey);
|
||||
form_sign_key.querySelector('input[name=valid_time]').value = valid_time;
|
||||
|
||||
form_sign_key.ajaxForm({
|
||||
success: function(response) {
|
||||
SimpleFormSubmit.submitForm(form_sign_key.action, form_sign_key)
|
||||
.then(response => {
|
||||
response.json().then( response => {
|
||||
// get certificate
|
||||
var data = response['data'];
|
||||
|
||||
var data = response.data;
|
||||
var certs = [
|
||||
pki.certificateFromPem(data.cert),
|
||||
pki.certificateFromPem(data.ca_cert)
|
||||
];
|
||||
var password = form.find('#cert-password').val();
|
||||
var keypair = form.data('keypair');
|
||||
var password = form.querySelector('#cert-password').value;
|
||||
var p12Asn1;
|
||||
if (password == '') {
|
||||
p12Asn1 = pkcs12.toPkcs12Asn1(keypair.privateKey, certs, null, {algorithm: '3des'}); // without password
|
||||
|
@ -117,15 +122,21 @@ window.client_cert = {
|
|||
var p12b64 = util.encode64(p12Der);
|
||||
|
||||
|
||||
var button = $('#save-button')[0];
|
||||
var button = $('#save-button');
|
||||
button.href= "data:application/x-pkcs12;base64," + p12b64
|
||||
button.style['display'] ='block';
|
||||
}
|
||||
|
||||
});
|
||||
// submit hidden form
|
||||
form_sign_key.submit();
|
||||
});
|
||||
});
|
||||
},
|
||||
revoke_certificate: function(href, id){
|
||||
var dialog = new ConfirmDialog(`Are you sure to revoke the certificate with the fingerprint ${id}?`);
|
||||
dialog.show().then(()=>{
|
||||
fetch(href, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -3,3 +3,13 @@
|
|||
@import "~@fortawesome/fontawesome-free/css/all.css";
|
||||
//@import "~datatables.net-bs4/css/dataTables.bootstrap4.css";
|
||||
|
||||
|
||||
|
||||
.messages-box {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
z-index: 500;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -54,11 +54,12 @@ def init_app(name=None):
|
|||
hydra_client = hydra.ApiClient(hydra_config)
|
||||
app.hydra_api = hydra.AdminApi(hydra_client)
|
||||
|
||||
from .views import auth_views, frontend_views, init_login_manager, api_views
|
||||
from .views import auth_views, frontend_views, init_login_manager, api_views, pki_views
|
||||
init_login_manager(app)
|
||||
app.register_blueprint(auth_views)
|
||||
app.register_blueprint(frontend_views)
|
||||
app.register_blueprint(api_views)
|
||||
app.register_blueprint(pki_views)
|
||||
|
||||
@app.before_request
|
||||
def befor_request():
|
||||
|
|
|
@ -9,6 +9,7 @@ from cryptography.hazmat.primitives import hashes
|
|||
from cryptography.hazmat.primitives import serialization
|
||||
from collections.abc import MutableSequence
|
||||
from datetime import datetime
|
||||
from dateutil import tz
|
||||
import pyotp
|
||||
import json
|
||||
|
||||
|
@ -137,10 +138,13 @@ class Service(object):
|
|||
|
||||
class Certificate(object):
|
||||
|
||||
def __init__(self, cn, ca_name, cert_data):
|
||||
def __init__(self, cn, ca_name: str, cert_data, revoked=False):
|
||||
self._cn = cn
|
||||
self._ca_name = ca_name
|
||||
self._cert_data = cert_data
|
||||
self._revoked = revoked
|
||||
self._cert_data.not_valid_after.replace(tzinfo=tz.tzutc())
|
||||
self._cert_data.not_valid_before.replace(tzinfo=tz.tzutc())
|
||||
|
||||
@property
|
||||
def cn(self):
|
||||
|
@ -151,19 +155,35 @@ class Certificate(object):
|
|||
return self._ca_name
|
||||
|
||||
@property
|
||||
def not_valid_before(self):
|
||||
return self._cert_data.not_valid_before
|
||||
def not_valid_before(self) -> datetime:
|
||||
return self._cert_data.not_valid_before.replace(tzinfo=tz.tzutc()).astimezone(tz.tzlocal()).replace(tzinfo=None)
|
||||
|
||||
@property
|
||||
def not_valid_after(self):
|
||||
return self._cert_data.not_valid_after
|
||||
def not_valid_after(self) -> datetime:
|
||||
return self._cert_data.not_valid_after.replace(tzinfo=tz.tzutc()).astimezone(tz.tzlocal()).replace(tzinfo=None)
|
||||
|
||||
def fingerprint(self, algorithm=hashes.SHA256()):
|
||||
@property
|
||||
def serial_number(self) -> int:
|
||||
return self._cert_data.serial_number
|
||||
|
||||
@property
|
||||
def serial_number_hex(self) -> str:
|
||||
return f'{self._cert_data.serial_number:X}'
|
||||
|
||||
def fingerprint(self, algorithm=hashes.SHA256()) -> bytes:
|
||||
return self._cert_data.fingerprint(algorithm)
|
||||
|
||||
def pem(self):
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
return self.not_valid_after > datetime.now() and not self._revoked
|
||||
|
||||
def pem(self) -> str:
|
||||
return self._cert_data.public_bytes(encoding=serialization.Encoding.PEM).decode()
|
||||
|
||||
@property
|
||||
def raw(self):
|
||||
return self._cert_data
|
||||
|
||||
def __str__(self):
|
||||
return f'Certificate(cn={self._cn}, ca_name={self._ca_name}, not_valid_before={self.not_valid_before}, not_valid_after={self.not_valid_after})'
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ import os
|
|||
import string
|
||||
import re
|
||||
import datetime
|
||||
from dateutil import tz
|
||||
from operator import attrgetter
|
||||
import logging
|
||||
|
||||
from .model import Service, User, Certificate
|
||||
|
@ -52,6 +54,7 @@ class Pki(object):
|
|||
|
||||
ca_private_key = self._ensure_private_key(ca_name)
|
||||
ca_cert = self._ensure_ca_cert(ca_name, ca_private_key)
|
||||
self.update_crl(ca_name)
|
||||
|
||||
pki_path = self._pki_path / ca_name
|
||||
if not pki_path.exists():
|
||||
|
@ -62,15 +65,39 @@ class Pki(object):
|
|||
def get_client_certs(self, user: User, service: Service):
|
||||
pki_path = self._pki_path / service.name
|
||||
certs = []
|
||||
crl = self._load_ca_crl(service.name)
|
||||
for cert_path in pki_path.glob(f'{user.username}*.crt.pem'):
|
||||
print(cert_path)
|
||||
with cert_path.open('rb') as cert_fd:
|
||||
cert_data = x509.load_pem_x509_certificate(
|
||||
cert_fd.read(),
|
||||
backend=default_backend())
|
||||
cert = Certificate(user.username, service.name, cert_data)
|
||||
revoked = crl.get_revoked_certificate_by_serial_number(
|
||||
cert_data.serial_number) is not None
|
||||
cert = Certificate(
|
||||
user.username, service.name, cert_data, revoked)
|
||||
certs.append(cert)
|
||||
return certs
|
||||
|
||||
return sorted(certs, key=attrgetter('not_valid_before'), reverse=True)
|
||||
|
||||
def get_crl(self, service: Service):
|
||||
return self._load_ca_crl(service.name)
|
||||
|
||||
def get_client_cert(self, user: User, service: Service, serial_number: str) -> Certificate:
|
||||
pki_path = self._pki_path / service.name
|
||||
cert_path = pki_path / f'{user.username}-{serial_number}.crt.pem'
|
||||
crl = self._load_ca_crl(service.name)
|
||||
with cert_path.open('rb') as cert_fd:
|
||||
cert_data = x509.load_pem_x509_certificate(
|
||||
cert_fd.read(),
|
||||
backend=default_backend())
|
||||
revoked = crl.get_revoked_certificate_by_serial_number(
|
||||
cert_data.serial_number) is None
|
||||
cert = Certificate(user.username, service.name, cert_data, revoked)
|
||||
return cert
|
||||
|
||||
def revoke_certificate(self, cert: Certificate):
|
||||
ca_private_key = self._ensure_private_key(cert.ca_name)
|
||||
self._revoke_cert(ca_private_key, cert)
|
||||
|
||||
def signing_publickey(self, user: User, service: Service, publickey: str, valid_time=DAY*365):
|
||||
_public_key = serialization.load_pem_public_key(
|
||||
|
@ -81,7 +108,7 @@ class Pki(object):
|
|||
username = str(user.username)
|
||||
config = service.pki_config #TODO use this config
|
||||
domain = self._domain
|
||||
not_valid_before = datetime.datetime.now()
|
||||
not_valid_before = datetime.datetime.utcnow()
|
||||
|
||||
ca_public_key = ca_private_key.public_key()
|
||||
end_entity_cert_builder = x509.CertificateBuilder().\
|
||||
|
@ -124,6 +151,15 @@ class Pki(object):
|
|||
add_extension(
|
||||
x509.SubjectKeyIdentifier.from_public_key(_public_key),
|
||||
critical=False).\
|
||||
add_extension(
|
||||
x509.CRLDistributionPoints([
|
||||
x509.DistributionPoint(
|
||||
full_name=[x509.UniformResourceIdentifier(f'http://crl.{self._domain}/{ca_name}.crl')],
|
||||
relative_name=None,
|
||||
crl_issuer=None,
|
||||
reasons=None)
|
||||
]),
|
||||
critical=False).\
|
||||
add_extension(
|
||||
x509.AuthorityInformationAccess([
|
||||
x509.AccessDescription(
|
||||
|
@ -142,9 +178,9 @@ class Pki(object):
|
|||
backend=default_backend()
|
||||
)
|
||||
|
||||
fingerprint =end_entity_cert.fingerprint(hashes.SHA256()).hex()
|
||||
serial_number = f'{end_entity_cert.serial_number:X}'
|
||||
end_entity_cert_filename = self._pki_path / ca_name / \
|
||||
f'{safe_filename(username)}-{fingerprint}.crt.pem'
|
||||
f'{safe_filename(username)}-{serial_number}.crt.pem'
|
||||
# save cert
|
||||
with end_entity_cert_filename.open("wb") as end_entity_cert_file:
|
||||
end_entity_cert_file.write(
|
||||
|
@ -224,3 +260,65 @@ class Pki(object):
|
|||
ca_cert.public_bytes(encoding=serialization.Encoding.PEM))
|
||||
assert isinstance(ca_cert, x509.Certificate)
|
||||
return ca_cert
|
||||
|
||||
def _revoke_cert(self, ca_private_key, cert):
|
||||
crl = self._load_ca_crl(cert.ca_name)
|
||||
builder = self._builder_crl(cert.ca_name, crl)
|
||||
revoked_certificate = x509.RevokedCertificateBuilder().serial_number(
|
||||
cert.raw.serial_number
|
||||
).revocation_date(
|
||||
datetime.datetime.now(tz.tzlocal())
|
||||
).build(default_backend())
|
||||
builder = builder.add_revoked_certificate(revoked_certificate)
|
||||
crl = self._crl_save(cert.ca_name, ca_private_key, builder)
|
||||
foobar = crl.get_revoked_certificate_by_serial_number(cert.raw.serial_number)
|
||||
|
||||
def _get_path_crl(self, ca_name):
|
||||
return self._pki_path / f'{ca_name}.crl.pem'
|
||||
|
||||
def _crl_save(self, ca_name, private_key, builder):
|
||||
crl = builder.sign(
|
||||
private_key=private_key,
|
||||
algorithm=hashes.SHA256(),
|
||||
backend=default_backend()
|
||||
)
|
||||
ca_crl_filename = self._get_path_crl(ca_name)
|
||||
with open(ca_crl_filename, "wb") as ca_crl_file:
|
||||
ca_crl_file.write(
|
||||
crl.public_bytes(encoding=serialization.Encoding.PEM))
|
||||
return crl
|
||||
|
||||
def _builder_crl(self, ca_name, old_crl=None):
|
||||
builder = x509.CertificateRevocationListBuilder()
|
||||
iname = x509.Name([
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, f'Lenticular Cloud CA - {ca_name}'),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME,
|
||||
'Lenticluar Cloud'),
|
||||
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME,
|
||||
ca_name),
|
||||
])
|
||||
|
||||
builder = builder.issuer_name(iname)
|
||||
builder = builder.last_update(datetime.datetime.now(tz.tzlocal()))
|
||||
builder = builder.next_update(datetime.datetime.now(tz.tzlocal()) + DAY)
|
||||
if old_crl is not None:
|
||||
for revoked_certificate in old_crl:
|
||||
builder = builder.add_revoked_certificate(revoked_certificate)
|
||||
|
||||
return builder
|
||||
|
||||
def update_crl(self, ca_name):
|
||||
ca_private_key = self._ensure_private_key(ca_name)
|
||||
crl = self._load_ca_crl(ca_name)
|
||||
builder = self._builder_crl(ca_name, crl)
|
||||
return self._crl_save(ca_name, ca_private_key, builder)
|
||||
|
||||
def _load_ca_crl(self, ca_name):
|
||||
ca_crl_filename = self._get_path_crl(ca_name)
|
||||
if ca_crl_filename.exists():
|
||||
with ca_crl_filename.open("rb") as ca_crl_file:
|
||||
crl = x509.load_pem_x509_crl(
|
||||
ca_crl_file.read(),
|
||||
backend=default_backend())
|
||||
return crl
|
||||
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
from .auth import auth_views
|
||||
from .frontend import frontend_views, init_login_manager
|
||||
from .api import api_views
|
||||
from .pki import pki_views
|
||||
|
|
|
@ -126,12 +126,26 @@ def client_cert():
|
|||
return render_template('frontend/client_cert.html.j2', services=current_app.lenticular_services, client_certs=client_certs)
|
||||
|
||||
|
||||
@frontend_views.route('/client_cert/<service_name>/<fingerprint>')
|
||||
@frontend_views.route('/client_cert/<service_name>/<serial_number>')
|
||||
@login_required
|
||||
def get_client_cert(service_name, fingerprint):
|
||||
def get_client_cert(service_name, serial_number):
|
||||
service = current_app.lenticular_services[service_name]
|
||||
current_app.pki.get_client_cert(current_user, service, fingerprint)
|
||||
pass
|
||||
cert = current_app.pki.get_client_cert(
|
||||
current_user, service, serial_number)
|
||||
return jsonify({
|
||||
'data': {
|
||||
'pem': cert.pem()}
|
||||
})
|
||||
|
||||
|
||||
@frontend_views.route('/client_cert/<service_name>/<serial_number>', methods=['DELETE'])
|
||||
@login_required
|
||||
def revoke_client_cert(service_name, serial_number):
|
||||
service = current_app.lenticular_services[service_name]
|
||||
cert = current_app.pki.get_client_cert(
|
||||
current_user, service, serial_number)
|
||||
current_app.pki.revoke_certificate(cert)
|
||||
return jsonify({})
|
||||
|
||||
|
||||
@frontend_views.route(
|
||||
|
@ -160,7 +174,8 @@ def client_cert_new(service_name):
|
|||
'errors': form.errors
|
||||
})
|
||||
|
||||
return render_template('frontend/client_cert_new.html.j2',
|
||||
return render_template(
|
||||
'frontend/client_cert_new.html.j2',
|
||||
service=service,
|
||||
form=form)
|
||||
|
||||
|
@ -172,7 +187,7 @@ def totp():
|
|||
return render_template('frontend/totp.html.j2', delete_form=delete_form)
|
||||
|
||||
|
||||
@frontend_views.route('/totp/new', methods=['GET','POST'])
|
||||
@frontend_views.route('/totp/new', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def totp_new():
|
||||
form = TOTPForm()
|
||||
|
|
38
lenticular_cloud/views/pki.py
Normal file
38
lenticular_cloud/views/pki.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
import flask
|
||||
from flask import Blueprint, redirect, request
|
||||
from flask import current_app, session
|
||||
from flask import jsonify
|
||||
from flask.helpers import make_response
|
||||
from flask.templating import render_template
|
||||
from oic.oic.message import TokenErrorResponse, UserInfoErrorResponse, EndSessionRequest
|
||||
|
||||
from pyop.access_token import AccessToken, BearerTokenError
|
||||
from pyop.exceptions import InvalidAuthenticationRequest, InvalidAccessToken, InvalidClientAuthentication, OAuthError, \
|
||||
InvalidSubjectIdentifier, InvalidClientRegistrationRequest
|
||||
from pyop.util import should_fragment_encode
|
||||
|
||||
from flask import Blueprint, render_template, request, url_for
|
||||
from flask_login import login_required, login_user, logout_user
|
||||
from werkzeug.utils import redirect
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
from base64 import b64decode, b64encode
|
||||
import ory_hydra_client as hydra
|
||||
from requests_oauthlib.oauth2_session import OAuth2Session
|
||||
import requests
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
from ..model import User, SecurityUser
|
||||
from ..model_db import User as DbUser
|
||||
from ..form.login import LoginForm
|
||||
from ..auth_providers import LdapAuthProvider
|
||||
|
||||
|
||||
pki_views = Blueprint('pki', __name__, url_prefix='/')
|
||||
|
||||
@pki_views.route('/<service_name>.crl')
|
||||
def crl(service_name: str):
|
||||
service = current_app.lenticular_services[service_name]
|
||||
crl = current_app.pki.get_crl(service)
|
||||
return crl.public_bytes(encoding=serialization.Encoding.DER)
|
||||
|
1304
package-lock.json
generated
1304
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -7,8 +7,9 @@
|
|||
},
|
||||
"author": "TuxCoder",
|
||||
"license": "GPLv3",
|
||||
"devDependencies": {
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
"bootstrap": "^4.4.1",
|
||||
"css-loader": "^3.5.3",
|
||||
"file-loader": "^6.0.0",
|
||||
"jquery": "^3.5.0",
|
||||
|
@ -28,7 +29,7 @@
|
|||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^4.4.1"
|
||||
"devDependencies": {
|
||||
"simple-form-submit": "^1.0.22"
|
||||
}
|
||||
}
|
||||
|
|
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
|
@ -15,6 +15,28 @@
|
|||
|
||||
{% block body %}
|
||||
|
||||
<div class='messages-box'>
|
||||
</div>
|
||||
<template id='confirm-dialog-template'>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger close" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary btn-ok process">Process</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
@ -57,9 +79,9 @@
|
|||
<div class="sidebar-sticky active">
|
||||
{#<a href="/"><img alt="logo" class="container-fluid" src="/static/images/dog_main_small.png"></a>#}
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.index') }}">{{ gettext('Account') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.logout') }}">{{ gettext('Logout') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.client_cert') }}">{{ gettext('Client Cert') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.totp') }}">{{ gettext('2FA - TOTP') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.logout') }}">{{ gettext('Logout') }}</a></li>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
@ -76,10 +98,12 @@
|
|||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 row justify-content-center">
|
||||
<div class='container'>
|
||||
<div class="mt-5 row justify-content-center">
|
||||
<footer>
|
||||
<span class="text-muted">Render Time: {{ g.request_time() }}</span> | <span class="text-muted">{{ gettext('All right reserved. ©2019') }}</span>
|
||||
<span class="text-muted">Render Time: {{ g.request_time() }}</span> | <span class="text-muted">{{ gettext('All right reserved. ©') + '2020' }}</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -21,18 +21,27 @@
|
|||
<tr>
|
||||
<th>not valid before</th>
|
||||
<th>not valid after</th>
|
||||
<th>fingerprint<th>
|
||||
<th>serial_number<th>
|
||||
<th> <th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cert in client_certs[service.name] %}
|
||||
<tr>
|
||||
<tr {{ 'class="table-warning"' if not cert.is_valid else ''}}>
|
||||
<td>{{ cert.not_valid_before }}</td>
|
||||
<td>{{ cert.not_valid_after }}</td>
|
||||
<td>{{ cert.fingerprint().hex() }}</td>
|
||||
<td>{{ cert.serial_number_hex }}</td>
|
||||
<td>
|
||||
<a title="{{ gettext('Download') }}" href="{{ url_for('.get_client_cert', service_name=service.name, serial_number=cert.serial_number_hex) }}"><i class="fas fa-file-download"></i></a>
|
||||
|
||||
{% if cert.is_valid %}
|
||||
<a title="{{ gettext('Revoke')}}" href="{{ url_for('.revoke_client_cert', service_name=service.name, serial_number=cert.serial_number_hex) }}" onclick="client_cert.revoke_certificate(this.href, '{{ cert.serial_number_hex }}'); return false;"><i class="fas fa-ban"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<a class="btn btn-default" href="{{ url_for('frontend.client_cert_new', service_name=service.name) }}">
|
||||
<a class="btn btn-primary" href="{{ url_for('frontend.client_cert_new', service_name=service.name) }}">
|
||||
New Certificate
|
||||
</a>
|
||||
</div>
|
||||
|
@ -45,7 +54,7 @@
|
|||
|
||||
{% block script_js %}
|
||||
|
||||
client_certs.init_list();
|
||||
client_cert.init_list();
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue