add functionality to add public key

also add authentication
This commit is contained in:
Jakobus Schürz 2019-09-23 12:06:54 +02:00
parent ba88459551
commit e55e8609bd
14 changed files with 716 additions and 0 deletions

49
servecerts/__init__.py Normal file
View file

@ -0,0 +1,49 @@
import os
from flask import Flask
from flask_debugtoolbar import DebugToolbarExtension
from logging.handlers import RotatingFileHandler
import logging
def create_app(test_config=None):
# create and configure the app
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY='dev',
DATABASE=os.path.join(app.instance_path, 'servecerts.sqlite'),
)
app.debug = True
if test_config is None:
# load the instance config, if it exists, when not testing
app.config.from_pyfile('config.py', silent=True)
else:
# load the test config if passed in
app.config.from_mapping(test_config)
# ensure the instance folder exists
try:
os.makedirs(app.instance_path)
except OSError:
pass
# a simple page that says hello
@app.route('/hello')
def hello():
return 'Hello World!'
from . import db
db.init_app(app)
from . import auth
app.register_blueprint(auth.bp)
from . import pubkeys
app.register_blueprint(pubkeys.bp)
app.add_url_rule('/', endpoint='pubkeys.index')
app.add_url_rule('/pubkeys/', endpoint='index')
return app

146
servecerts/auth.py Normal file
View file

@ -0,0 +1,146 @@
import functools
from flask import (
Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash
from servecerts.db import get_db
from flask_debugtoolbar import DebugToolbarExtension
import logging
bp = Blueprint('auth', __name__, url_prefix='/auth')
@bp.route('/register', methods=('GET', 'POST'))
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
fullname = request.form['fullname']
email = request.form['email']
db = get_db()
error = None
if not username:
error = 'Username is required.'
elif not password:
error = 'Password is required.'
elif not fullname:
error = 'Fullname is required.'
elif not email:
error = 'Email is required.'
elif db.execute(
'SELECT id FROM user WHERE username = ?', (username,)
).fetchone() is not None:
error = 'User {} is already registered.'.format(username)
if error is None:
db.execute(
'INSERT INTO user (username, password, fullname, email) VALUES (?, ?, ?, ?)',
(username, generate_password_hash(password), fullname, email)
)
db.commit()
return redirect(url_for('auth.login'))
flash(error)
return render_template('auth/register.html')
@bp.route('/<int:id>/update', methods=('GET', 'POST'))
def update(id):
user = get_user(id)
if request.method == 'POST':
username = request.form['username']
fullname = request.form['fullname']
password = request.form['password']
email = request.form['email']
error = None
if not username:
error = 'Username is required.'
elif not password:
error = 'Password is required.'
elif not fullname:
error = 'Fullname is required.'
elif not email:
error = 'Email is required.'
if error is not None:
flash(error)
else:
db = get_db()
db.execute(
'UPDATE user SET username = ?, password = ?, fullname = ?, email = ?'
' WHERE id = ?',
(username, generate_password_hash(password), fullname, email, id)
)
db.commit()
return redirect(url_for('pubkeys.index'))
return render_template('auth/update.html', user=user)
@bp.route('/login', methods=('GET', 'POST'))
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
db = get_db()
error = None
user = db.execute(
'SELECT * FROM user WHERE username = ?', (username,)
).fetchone()
if user is None:
error = 'Incorrect username.'
elif not check_password_hash(user['password'], password):
error = 'Incorrect password.'
if error is None:
session.clear()
session['user_id'] = user['id']
return redirect(url_for('pubkeys.index'))
flash(error)
return render_template('auth/login.html')
@bp.before_app_request
def load_logged_in_user():
user_id = session.get('user_id')
if user_id is None:
g.user = None
else:
g.user = get_db().execute(
'SELECT * FROM user WHERE id = ?', (user_id,)
).fetchone()
@bp.route('/logout')
def logout():
session.clear()
return redirect(url_for('pubkeys.index'))
def login_required(view):
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None:
return redirect(url_for('auth.login'))
return view(**kwargs)
return wrapped_view
def get_user(id):
user = get_db().execute(
'SELECT * FROM user WHERE id = ?', (id,)
).fetchone()
if user is None:
abort(404, "User id {0} does not exist.".format(id))
return user

41
servecerts/db.py Normal file
View file

@ -0,0 +1,41 @@
import sqlite3
import click
from flask import current_app, g
from flask.cli import with_appcontext
def get_db():
if 'db' not in g:
g.db = sqlite3.connect(
current_app.config['DATABASE'],
detect_types=sqlite3.PARSE_DECLTYPES
)
g.db.row_factory = sqlite3.Row
return g.db
def close_db(e=None):
db = g.pop('db', None)
if db is not None:
db.close()
def init_db():
db = get_db()
with current_app.open_resource('schema.sql') as f:
db.executescript(f.read().decode('utf8'))
@click.command('init-db')
@with_appcontext
def init_db_command():
"""Clear the existing data and create new tables."""
init_db()
click.echo('Initialized the database.')
def init_app(app):
app.teardown_appcontext(close_db)
app.cli.add_command(init_db_command)

156
servecerts/pubkeys.py Normal file
View file

@ -0,0 +1,156 @@
from flask import (
Blueprint, flash, g, redirect, render_template, request, url_for
)
from werkzeug.exceptions import abort
from servecerts.auth import login_required
from servecerts.db import get_db
import hashlib
import base64
#bp = Blueprint('pubkeys', __name__, url_prefix='/pubkeys')
bp = Blueprint('pubkeys', __name__, url_prefix='/pubkeys')
@bp.route('/')
@login_required
def index():
db = get_db()
pubkeys = db.execute(
'SELECT'
' p.id, key_name, fullname, ssh_pubkey, p.created, user_id, revoked, deleted'
' FROM pubkeys p'
' JOIN user u ON p.user_id = u.id'
' ORDER BY deleted ASC, revoked ASC, p.created DESC'
).fetchall()
users = db.execute(
'SELECT * FROM user WHERE id = ?', (g.user['id'],)
).fetchone()
return render_template('pubkeys/index.html', pubkeys=pubkeys, users=users)
def get_pubkey(id, check_user=True):
pubkey = get_db().execute(
'SELECT p.id, ssh_pubkey, key_name, user_id, fingerprint, deleted'
' FROM pubkeys p JOIN user u ON p.user_id = u.id'
' WHERE p.id = ?', (id,)
).fetchone()
if pubkey is None:
abort(404, "Pubkey id {0} does not exist.".format(id))
if check_user and pubkey['user_id'] != g.user['id']:
abort(403)
return pubkey
@bp.route('/create', methods=('GET', 'POST'))
@login_required
def create():
if request.method == 'POST':
key_name = request.form['key_name']
ssh_pubkey = request.form['ssh_pubkey']
splitkey = ssh_pubkey.split(' ')
enc = splitkey.pop(0)
key = splitkey.pop(0)
comment = ' '.join(splitkey)
fingerprint = base64.b64encode(hashlib.sha256(base64.b64decode(key)).digest()).rstrip("=")
print fingerprint
db = get_db()
ckfp = db.execute(
'SELECT id, fingerprint, deleted FROM pubkeys WHERE fingerprint = ?', (fingerprint, )
).fetchone()
if ckfp != None:
if ckfp['fingerprint'] == fingerprint:
if ckfp['deleted'] == 0:
error = "Key exists already"
else:
error = "Key was deleted before -> reactivate it again!"
flash(error)
return redirect(url_for('pubkeys.update', id=ckfp['id']))
if not key_name:
key_name = comment
error = None
if not ssh_pubkey:
error = 'SSH-Pubkey is required.'
if error is not None:
flash(error)
else:
#db = get_db()
db.execute(
'INSERT INTO pubkeys (ssh_pubkey, key_name, user_id, fingerprint)'
' VALUES (?, ?, ?, ?)',
(ssh_pubkey, key_name, g.user['id'], fingerprint)
)
db.commit()
return redirect(url_for('pubkeys.index'))
return render_template('pubkeys/create.html')
@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id):
pubkey = get_pubkey(id)
if request.method == 'POST':
key_name = request.form['key_name']
deleted = request.form.get('deleted')
error = None
if not key_name:
error = 'Key-ID is required.'
deleted = 0 if not deleted else 1
# if not ssh_pubkey:
# error = "SSH-Pubkey is requred."
if error is not None:
flash(error)
else:
db = get_db()
db.execute(
'UPDATE pubkeys SET key_name = ?, deleted = ?'
' WHERE id = ?',
(key_name, deleted, id)
)
db.commit()
return redirect(url_for('pubkeys.index'))
return render_template('pubkeys/update.html', pubkey=pubkey)
@bp.route('/<int:id>/delete', methods=('GET',))
@login_required
def delete(id):
get_pubkey(id)
db = get_db()
#db.execute('DELETE FROM pubkeys WHERE id = ?', (id,))
db.execute('UPDATE pubkeys SET deleted = 1 WHERE id = ?', (id,))
db.execute('UPDATE certificates SET deleted = 1 WHERE pubkey_id = ?', (id,))
db.commit()
return redirect(url_for('pubkeys.index'))
@bp.route('/<int:id>/deletefinal', methods=('GET',))
@login_required
def deletefinal(id):
get_pubkey(id)
db = get_db()
db.execute('DELETE FROM pubkeys WHERE id = ?', (id,))
#db.execute('UPDATE certificates SET deleted = 1 WHERE pubkey_id = ?', (id,))
db.commit()
return redirect(url_for('pubkeys.index'))
@bp.route('/<int:id>/revoke', methods=('POST', 'GET'))
@login_required
def revoke(id):
get_pubkey(id)
db = get_db()
db.execute(
'UPDATE pubkeys SET revoked = 1'
' WHERE id = ?', (id,)
)
db.commit()
return redirect(url_for('pubkeys.index'))

60
servecerts/schema.sql Normal file
View file

@ -0,0 +1,60 @@
DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS pubkeys;
DROP TABLE IF EXISTS certificates;
DROP TABLE IF EXISTS settings;
CREATE TABLE settings (
id INTEGER PRIMARY KEY,
current_serialnumber INTEGER NOT NULL,
default_principals TEXT,
default_commands TEXT,
default_capabilities TEXT,
default_client_from TEXT,
current_timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
fullname TEXT NOT NULL,
email TEXT NOT NULL,
principals TEXT,
commands TEXT DEFAULT username NOT NULL,
capabilities TEXT,
client_from TEXT,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE pubkeys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
key_name TEXT NOT NULL,
ssh_pubkey TEXT NOT NULL,
fingerprint TEXT,
revoked INTEGER DEFAULT 0 NOT NULL,
deleted INTEGER DEFAULT 0 NOT NULL,
userca INTEGER DEFAULT 0 NOT NULL,
hostca INTEGER DEFAULT 0 NOT NULL,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user (id)
);
CREATE TABLE certificates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pubkey_id INTEGER NOT NULL,
key_id TEXT NOT NULL,
serial INTEGER NOT NULL,
principals TEXT,
commands TEXT,
capabilities TEXT,
client_from TEXT,
revoked INTEGER DEFAULT 0 NOT NULL,
deleted INTEGER DEFAULT 0 NOT NULL,
valid_from TIMESTAMP,
valid_unitl TIMESTAMP,
expired INTEGER DEFAULT 0 NOT NULL,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (pubkey_id) REFERENCES pubkeys (id)
);

View file

@ -0,0 +1,28 @@
html { font-family: sans-serif; background: #eee; padding: 1rem; }
body { max-width: 960px; margin: 0 auto; background: white; }
h1 { font-family: serif; color: #377ba8; margin: 1rem 0; }
a { color: #377ba8; }
hr { border: none; border-top: 1px solid lightgray; }
nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; }
nav h1 { flex: auto; margin: 0; }
nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; }
nav ul { display: flex; list-style: none; margin: 0; padding: 0; }
nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; }
.content { padding: 0 1rem 1rem; }
.content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; }
.content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; }
.flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; }
.post > header { display: flex; align-items: flex-end; font-size: 0.85em; }
.post > header > div:first-of-type { flex: auto; }
.post > header h1 { font-size: 1.5em; margin-bottom: 0; }
.post .about { color: slategray; font-style: italic; }
.post .body { white-space: pre-line; }
.content:last-child { margin-bottom: 0; }
.content form { margin: 1em 0; display: flex; flex-direction: column; }
.content label { font-weight: bold; margin-bottom: 0.5em; }
.content input, .content textarea { margin-bottom: 1em; }
.content textarea { min-height: 12em; resize: vertical; }
.content div { overflow: hidden; word-wrap: break-word; }
.revoked, .deleted {text-decoration: line-through; color: slategray; font-style: italic;}
input.danger { color: #cc2f2e; }
input[type=submit] { align-self: start; min-width: 10em; }

View file

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Log In{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="username">Username</label>
<input name="username" id="username" required>
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
<input type="submit" value="Log In">
</form>
{% endblock %}

View file

@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Register new User{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="username">Username</label>
<input name="username" id="username" required>
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
<label for="fullname">Anzeigename</label>
<input name="fullname" id="fullname" required>
<label for="email">Emailaddress</label>
<input name="email" id="email" required>
<input type="submit" value="Register">
</form>
{% endblock %}

View file

@ -0,0 +1,37 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Edit User{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="fullname">Fullname ({{ user['fullname'] }})</label>
<input name="fullname" id="fullname"
value="{{ request.form['fullname'] or user['fullname'] }}" required>
<label for="username">Username ({{ user['username'] }})</label>
<input name="username" id="username"
value="{{ request.form['username'] or user['username'] }}" required>
<label for="password">Password</label>
<input type="password" name="password" id="password"
value="" required>
<label for="email">Email: ({{ user['email'] }})</label>
<input name="email" id="email"
value="{{ request.form['email'] or user['email'] }}" required>
<label for="principals">Principals: ({{ user['principals'] }})</label>
<input name="principals" id="principals"
value="{{ request.form['principals'] or user['principals'] }}">
<label for="allowed_commands">Allowed commands: ({{ user['allowed_commands'] }})</label>
<input name="allowed_commands" id="allowed_commands"
value="{{ request.form['allowed_commands'] or user['allowed_commands'] }}">
<label for="client_from">Client from: ({{ user['client_from'] }})</label>
<input name="client_from" id="client_from"
value="{{ request.form['client_from'] or user['client_from'] }}">
<label for="capabilities">Capabilities: ({{ user['capabilities'] }})</label>
<input name="capabilities" id="capabilities"
value="{{ request.form['capabilities'] or user['capabilities'] }}">
<input type="submit" value="Save">
</form>
<hr>
{% endblock %}

View file

@ -0,0 +1,25 @@
<!doctype html>
<title>{% block title %}{% endblock %} - Serve Certs</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<nav>
<h1><a href="{{ url_for('pubkeys.index') }}">SSH-Certificates</a></h1>
"{{ request.environ.get('HTTP_X_REAL_IP', request.remote_addr) }}"
<ul>
{% if g.user %}
<li><a class="action" href="{{ url_for('auth.update', id=g.user['id']) }}">{{ g.user['username'] }} (Settings)</a>
<li><a href="{{ url_for('auth.logout') }}">Log Out</a>
{% else %}
<li><a href="{{ url_for('auth.register') }}">Register</a>
<li><a href="{{ url_for('auth.login') }}">Log In</a>
{% endif %}
</ul>
</nav>
<section class="content">
<header>
{% block header %}{% endblock %}
</header>
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% block content %}{% endblock %}
</section>

View file

@ -0,0 +1,50 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}
{% if g.user %}
Certificates for Pubkey {{ g.pubkeys['fullname'] }}
{% else %}
Certificates
{% endif %}
{% endblock %}</h1>
{% if g.user %}
<a class="action" href="{{ url_for('certificates.create') }}">New</a>
{% endif %}
{% endblock %}
{% block content %}
{% if g.user %}
{% for certificate in certificates %}
<article class="post">
<header>
<div>
<h1>Certificate ({{ certificate['id'] }}): {{ certificate['key_id'] }} </h1>
</div>
{% if g.user['id'] == pubkey['user_id'] %}
<a class="action" href="{{ url_for('certificates.revoke', id=certificate['id']) }}">Revoke</a>
{% endif %}
</header>
<p class="body{% if pubkey['revoked'] != 0 %} revoked {% endif %}">
Serialnumber: {{ certificate['serial'] }}{% if pubkey['revoked'] != 0 %} - revoked {% endif %}"
Validity duration: {{ certificate['valid_from'].strftime('%Y-%m-%d %H:%M') }} - {{ certificate['valid_until'].strftime('%Y-%m-%d %H:%M') }}
Principals: {{ certificate['principals'] }}
Valid Client IP: {{ certificate['from_ip'] }}
Allowed Command: {{ certificate['commands'] }}
Capabilities: {{ certificate['capabilities'] }}
</p>
<p class="about">created on {{ certificate['created'].strftime('%Y-%m-%d') }}</p>
</form>
<form action="{{ url_for('certificates.revoke', id=certificate['id']) }}" method="POST">
<input class="danger" type="submit" value="Revoke" onclick="return confirm('Are you sure?');">
</form>
</article>
{% if not loop.last %}
<hr>
{% endif %}
{% endfor %}
{% else %}
<div class="danger">To view pubkeys and certificates, please log in</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}New Public Key{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="key_name">Key Identifier</label>
<input name="key_name" id="key_name" value="{{ request.form['key_name'] }}">
<label for="ssh_pubkey">SSH-Pubkey</label>
<textarea name="ssh_pubkey" id="ssh_pubkey" required>{{ request.form['ssh_pubkey'] }}</textarea>
<input type="submit" value="Save">
</form>
{% endblock %}

View file

@ -0,0 +1,50 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}
{% if g.user %}
Pubkeys for {{ g.user['fullname'] }}
{% else %}
Pubkeys
{% endif %}
{% endblock %}</h1>
{% if g.user %}
<a class="action" href="{{ url_for('pubkeys.create') }}">New</a>
{% endif %}
{% endblock %}
{% block content %}
{% if g.user %}
{% for pubkey in pubkeys %}
{% if pubkey['deleted'] == 0 %}
<article class="post{% if pubkey['revoked'] != 0 %} revoked{% endif %}{% if pubkey['deleted'] != 0 %} deleted{% endif %}">
<header>
<div>
<h1>{% if pubkey['revoked'] != 0 %}<div class="danger">revoked key<div> - {% endif %}({{ pubkey['id'] }}): {{ pubkey['key_name'] }} </h1>
</div>
{% if g.user['id'] == pubkey['user_id'] %}
<a class="action" href="{{ url_for('pubkeys.update', id=pubkey['id']) }}">Edit</a>
{% if pubkey['deleted'] == 0 %}
<a class="action" href="{{ url_for('pubkeys.delete', id=pubkey['id']) }}" onclick="return confirm('Are you sure?');">Delete</a>
{% endif %}
{% if pubkey['revoked'] == 0 %}
<a class="action" href="{{ url_for('pubkeys.revoke', id=pubkey['id']) }}" onclick="return confirm('Are you sure?');">Revoke</a>
{% endif %}
{% endif %}
</header>
<div name="ssh_pubkey" id="ssh_pubkey">{{ request.form['ssh_pubkey'] or pubkey['ssh_pubkey'] }}</div>
<p class="about">registered on {{ pubkey['created'].strftime('%Y-%m-%d') }}</p>
</form>
<form action="{{ url_for('pubkeys.revoke', id=pubkey['id']) }}" method="POST">
<input class="danger" type="submit" value="Revoke" onclick="return confirm('Are you sure?');">
</form>
</article>
{% if not loop.last %}
<hr>
{% endif %}
{% endif %}
{% endfor %}
{% else %}
<div class="danger">To view pubkeys and certificates, please log in</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,25 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Edit "{{ pubkey['fingerprint'] }}"{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="key_name">SSH-Pubkey ({{ pubkey['id'] }}) {{ pubkey['deleted'] }}</label>
<input name="key_name" id="key_name"
value="{{ request.form['key_name'] or pubkey['key_name'] }}" required>
<label for="ssh_pubkey">SSH-Pubkey</label>
<textarea name="ssh_pubkey" id="ssh_pubkey" readonly>{{ request.form['ssh_pubkey'] or pubkey['ssh_pubkey'] }}</textarea>
<!--div class="about">{{ request.form['ssh_pubkey'] or pubkey['ssh_pubkey'] }}<div-->
<input type="checkbox" name="deleted" id="deleted" {% if pubkey['deleted'] != 0 %}checked="checked" {% endif %}>
<label for="deleted">Key marked as deleted</label>
<input type="submit" value="Save">
</form>
<hr>
<ul>
<li><form action="{{ url_for('pubkeys.delete', id=pubkey['id']) }}" method="post">
<input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');">
</form>
</ul>
{% endblock %}