diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..7da1f96 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 100 diff --git a/.gitignore b/.gitignore index 7bbc71c..3f8a7fb 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,16 @@ ENV/ # mypy .mypy_cache/ + +# project specific +keyserver/config.py + +# vs code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +keyserver.log* +keyserv/config.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d3d05e1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,231 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Flask", + "type": "python", + "request": "launch", + "stopOnEntry": false, + "pythonPath": "${config:python.pythonPath}", + "program": "/Users/sam/git/mini-key-server/venv/bin/flask", + "cwd": "${workspaceRoot}", + "env": { + "FLASK_APP": "${workspaceRoot}/keyserver.py" + }, + "args": [ + "run", + "--no-debugger", + "--no-reload" + ], + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + }, + { + "name": "Python", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "program": "${file}", + "cwd": "${workspaceRoot}", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + }, + { + "name": "Python: Attach", + "type": "python", + "request": "attach", + "localRoot": "${workspaceRoot}", + "remoteRoot": "${workspaceRoot}", + "port": 3000, + "secret": "my_secret", + "host": "localhost" + }, + { + "name": "Python: Terminal (integrated)", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "program": "${file}", + "cwd": "", + "console": "integratedTerminal", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit" + ] + }, + { + "name": "Python: Terminal (external)", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "program": "${file}", + "cwd": "", + "console": "externalTerminal", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit" + ] + }, + { + "name": "Python: Django", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "program": "${workspaceRoot}/manage.py", + "cwd": "${workspaceRoot}", + "args": [ + "runserver", + "--noreload", + "--nothreading" + ], + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput", + "DjangoDebugging" + ] + }, + { + "name": "Python: Flask (0.11.x or later)", + "type": "python", + "request": "launch", + "stopOnEntry": false, + "pythonPath": "${config:python.pythonPath}", + "program": "fully qualified path fo 'flask' executable. Generally located along with python interpreter", + "cwd": "${workspaceRoot}", + "env": { + "FLASK_APP": "${workspaceRoot}/quickstart/app.py" + }, + "args": [ + "run", + "--no-debugger", + "--no-reload" + ], + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + }, + { + "name": "Python: Flask (0.10.x or earlier)", + "type": "python", + "request": "launch", + "stopOnEntry": false, + "pythonPath": "${config:python.pythonPath}", + "program": "${workspaceRoot}/run.py", + "cwd": "${workspaceRoot}", + "args": [], + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + }, + { + "name": "Python: PySpark", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "osx": { + "pythonPath": "${env:SPARK_HOME}/bin/spark-submit" + }, + "windows": { + "pythonPath": "${env:SPARK_HOME}/bin/spark-submit.cmd" + }, + "linux": { + "pythonPath": "${env:SPARK_HOME}/bin/spark-submit" + }, + "program": "${file}", + "cwd": "${workspaceRoot}", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + }, + { + "name": "Python: Module", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "module": "module.name", + "cwd": "${workspaceRoot}", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + }, + { + "name": "Python: Pyramid", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "cwd": "${workspaceRoot}", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "args": [ + "${workspaceRoot}/development.ini" + ], + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput", + "Pyramid" + ] + }, + { + "name": "Python: Watson", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "program": "${workspaceRoot}/console.py", + "cwd": "${workspaceRoot}", + "args": [ + "dev", + "runserver", + "--noreload=True" + ], + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit", + "RedirectOutput" + ] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d582968 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "python.pythonPath": "${workspaceFolder}/venv/bin/python3.6", + "python.linting.enabled": true, + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": true, + "python.linting.mypyEnabled": true, + "python.linting.pydocstyleEnabled": false, + "editor.formatOnSave": false +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..288ce57 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# mini-key-server + +## Requirements + +Aside from the python module requirements listed in [requirements.txt](requirements.txt), the following is required: +* Python 3.6 or later. +* PostgreSQL + + +## Installation + +This software should be used from a [viritualenv](https://virtualenv.pypa.io/en/stable/) environment. + +```sh +virtualenv venv +source venv/bin/activate +pip3 install -U -r requirements.txt +``` + +## Database Setup + +The following commands will create a suitable database for the keyserver to use. + +```sh +su - postgres +createuser keyserver +createdb -O keyserver keyserver +``` diff --git a/keyserv/__init__.py b/keyserv/__init__.py new file mode 100644 index 0000000..5eb9dec --- /dev/null +++ b/keyserv/__init__.py @@ -0,0 +1,62 @@ +# MIT License + +# Copyright(c) 2018 Samuel Hoffman + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import click +from flask import Flask +from flask_bootstrap import Bootstrap + +from .auth import login_manager, add_user +from .endpoints import api +from .models import db, Event +from .views import frontend + + +def format_event(value): + return Event(value) + + +def create_app(config): + app = Flask(__name__) + + app.config.from_object(__name__) + app.config.from_object("keyserv.config.{}".format(config)) + app.jinja_env.filters["event"] = format_event + + Bootstrap(app) + api.init_app(app) + db.init_app(app) + login_manager.init_app(app) + + app.register_blueprint(frontend) + + @app.cli.command("initdb") + def initdb_command(): + db.create_all() + print("database initialized") + + @app.cli.command("create-user") + @click.argument("username") + @click.argument("password") + def create_user_command(username: str, password: str): + add_user(username, password.encode()) + + return app diff --git a/keyserv/auth.py b/keyserv/auth.py new file mode 100644 index 0000000..9163458 --- /dev/null +++ b/keyserv/auth.py @@ -0,0 +1,61 @@ +# MIT License + +# Copyright(c) 2018 Samuel Hoffman + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import secrets + +import argon2 +from flask_login import LoginManager, UserMixin + +from keyserv.models import db + +login_manager = LoginManager() +login_manager.session_protection = "strong" + + +def add_user(username: str, password: bytes, level=500): + passwd = argon2.hash_password(password, secrets.token_bytes(None)) + user = Users(username, passwd, level) + db.session.add(user) + db.session.commit() + + +class Users(db.Model, UserMixin): + id = db.Column(db.Integer(), primary_key=True) + username = db.Column(db.String(), unique=True, nullable=False) + passwd = db.Column(db.LargeBinary(), nullable=False) + level = db.Column(db.Integer()) + + def __init__(self, username=None, passwd=None, level=0): + self.username = username + self.passwd = passwd + self.level = level + + def get_id(self): + return self.id + + def check_password(self, passwd): + return argon2.verify_password(self.passwd, bytes(passwd, "UTF-8")) + + +@login_manager.user_loader +def user_loader(user_id) -> Users: + return Users.query.get(user_id) diff --git a/keyserv/config.example.py b/keyserv/config.example.py new file mode 100644 index 0000000..475db31 --- /dev/null +++ b/keyserv/config.example.py @@ -0,0 +1,24 @@ +# adjust the Config class below, then rename this file to config.py + + +class DefaultConfig(object): + # a decent way to generate a secret key is by running: python -c "import os; print(repr(os.urandom(24)))" + # then pasting the output here. + SECRET_KEY = __NOT_SET__ + + DEBUG = False + TESTING = False + + SQLALCHEMY_TRACK_MODIFICATIONS = False + SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:" + + +class ProductionConfig(DefaultConfig): + + SQLALCHEMY_DATABASE_URI = "postgres://localhost/keyserver" + + +class DevelopmentConfig(ProductionConfig): + DEBUG = True + TESTING = True + SQLALCHEMY_TRACK_MODIFICATIONS = True diff --git a/keyserv/endpoints.py b/keyserv/endpoints.py new file mode 100644 index 0000000..27f9451 --- /dev/null +++ b/keyserv/endpoints.py @@ -0,0 +1,83 @@ +"""RESTful API views.""" +# MIT License + +# Copyright(c) 2017 Samuel Hoffman + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from flask import request +from flask_restful import Resource, Api +from flask_restful import reqparse +from keyserv.keymanager import Origin, key_exists_const, key_get_unsafe, activate_key_unsafe + +api = Api() + + +class ActivateKey(Resource): + """Endpoint used for key activation.""" + + def post(self): + """ + Activate a key + + Activates a live key; will either allow key activation or deny if there are no more key + activations left. Function will log attempts to activate regardless of success or failure. + """ + parser = reqparse.RequestParser() + parser.add_argument("token", required=True) + parser.add_argument("machine", required=True) + parser.add_argument("user", required=True) + + args = parser.parse_args() + + origin = Origin(request.remote_addr, args.machine, args.user) + + if not key_exists_const(args.token, origin): + return {"result": "failure", "error": "invalid activation token"}, 404 + + key = key_get_unsafe(args.token, origin) + + if key.remaining == 0: + return {"result": "failure", "error": "key is out of activations"}, 410 + + activate_key_unsafe(args.token, origin) + + return {"result": "ok", "remainingActivations": str(key.remaining)}, 201 + + +class CheckKey(Resource): + """Endpoint used for checking if a key is valid.""" + + def get(self): + parser = reqparse.RequestParser() + parser.add_argument("token", required=True) + parser.add_argument("machine", required=True) + parser.add_argument("user", required=True) + + args = parser.parse_args() + + origin = Origin(request.remote_addr, args.machine, args.user) + + if key_exists_const(args.token, origin): + return {"result": "ok"}, 201 + + return {"result": "failure", "error": "invalid key"}, 404 + + +api.add_resource(ActivateKey, "/api/activate") +api.add_resource(CheckKey, "/api/check") diff --git a/keyserv/forms.py b/keyserv/forms.py new file mode 100644 index 0000000..714dba3 --- /dev/null +++ b/keyserv/forms.py @@ -0,0 +1,46 @@ +# MIT License + +# Copyright(c) 2018 Samuel Hoffman + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + +from flask_wtf import FlaskForm +from wtforms import (BooleanField, IntegerField, PasswordField, SelectField, + StringField, SubmitField) +from wtforms.validators import required + + +class LoginForm(FlaskForm): + username = StringField("Username", [required()]) + password = PasswordField("Password", [required()]) + submit = SubmitField("Log In") + + +class KeyForm(FlaskForm): + activations = IntegerField("Number of Activations", [required()], + render_kw={"type": "number", "min": -1, "value": 0}) + application = SelectField("Application", coerce=int) + + active = BooleanField("Active", [required()], default=True) + memo = StringField("Memo") + submit = SubmitField("Submit") + + +class AppForm(FlaskForm): + name = StringField("Application Name") + submit = SubmitField("Submit") diff --git a/keyserv/keymanager.py b/keyserv/keymanager.py new file mode 100644 index 0000000..eb7a279 --- /dev/null +++ b/keyserv/keymanager.py @@ -0,0 +1,185 @@ +# MIT License + +# Copyright(c) 2018 Samuel Hoffman + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import secrets +import string +from datetime import datetime +from hmac import compare_digest + +from flask import current_app, request +from flask_login import current_user +from sqlalchemy import exists + +from keyserv.models import AuditLog, Event, Key, db + + +class ExhuastedActivations(Exception): + """Raised when an activation attempt is made but the remaining activations is already at 0.""" + pass + + +class KeyNotFound(Exception): + """Raised when an action is attempted on a non-existent key.""" + pass + + +class Origin: + """Origin that identifies a key action.""" + + def __init__(self, ip, machine, user): + self.ip = ip + self.machine = machine + self.user = user + + def __str__(self): + return f"IP: {self.ip}, Machine: {self.machine}, User: {self.user}" + + def __repr__(self): + return f"" + + +def rand_token(length: int = 25, chars: str = string.ascii_uppercase + string.digits) -> str: + """ + Generate a random token. Does not check for duplicates yet. + + A length of 25 should give us 8.082812775E38 keys. + + length: - length of token to generate + chars: - characters used in seeding of token + """ + return "".join(secrets.choice(chars) for i in range(length)) + + +def token_exists_unsafe(token: str) -> bool: + """Check if `token` exists in the token database. Does NOT perform constant time comparison.""" + return db.session.query(exists().where(Key.token == token)).scalar() + + +def generate_token_unsafe() -> str: + """ + Generate a new token. + + Does not perform constant time comparison when checking if the generated token is a duplicate. + """ + key = rand_token() + while token_exists_unsafe(key): + key = rand_token() + return key + + +def cut_key_unsafe(activations: int, app_id: int, active: bool = True, memo: str = "") -> str: + """ + Cuts a new key and returns the activation token. + + Cuts a new key with # `activations` allowed activations. -1 is considered unlimited activations. + """ + token = generate_token_unsafe() + key = Key(token, activations, app_id, active, memo) + key.cutdate = datetime.now() + + db.session.add(key) + db.session.commit() + + current_app.logger.info( + f"cut new key {key} with {activations} activation(s), memo: {memo}") + AuditLog.from_key( + key, f"new key cut by {current_user.username} ({request.remote_addr})", Event.KeyCreated) + + return token + + +def disable_key_unsafe(token: str): + """Disable a key by its token.""" + key = Key.query.filter(Key.token == token).first() + if not key: + current_app.logger.error( + f"failed to disable key by non-existent token {token}") + raise KeyNotFound(f"no key found for token {token}") + key.enabled = False + current_app.logger.info(f"disabled key {key}") + AuditLog.from_key(key, "key was disabled", Event.KeyModified) + db.session.commit() + + +def _compare(left: str, right: str) -> int: + if len(left) != len(right): + return 0 + res = 0 + for leftchr, rightchr in zip(left, right): + res |= ord(leftchr) ^ ord(rightchr) + return res % 1 + + +def key_exists_const(token: str, origin: Origin) -> bool: + """Constant time check to see if `token` exists in the database. Compares against all keys + even if a match is found.""" + current_app.logger.info(f"key lookup by token {token}") + found = False + for key in Key.query.all(): + if compare_digest(token, key.token): + found = True + return found + + +def key_get_unsafe(token: str, origin) -> Key: + """Get a key by its token using constant time comparison.""" + + current_app.logger.info(f"key retreival by token {token} from {origin}") + + key = Key.query.filter(Key.token == token).first() + if key: + AuditLog.from_key(key, f"key retreival from {origin}", Event.KeyAccess) + return key + return None + + +def activate_key_unsafe(token: str, origin: Origin): + """Mark a key as activated by its token. Does not perform constant time comparisons. + + `ip`, `machine`, and `user` are of the originating activation attempt. + """ + key = Key.query.filter(Key.token == token).first() + + if key.remaining == -1: + current_app.logger.info( + f"new unlimited activation: Key {key!r} from {origin}") + AuditLog.from_key( + key, f"new unlimited activation from from {origin}", Event.AppActivation) + return + + if key.remaining == 0: + current_app.logger.info( + f"failed activation attempt: Key {key!r} from {origin}") + AuditLog.from_key( + key, f"failed activation attempt from {origin}", Event.FailedActivation) + + raise ExhuastedActivations( + f"token {token} has exhausted all remaining activations") + + key.remaining -= 1 + + current_app.logger.info(f"new activation: Key {key!r} from {origin}." + f" remaining activations: {key.remaining}") + AuditLog.from_key( + key, f"new activation from {origin}", Event.AppActivation) + + db.session.commit() diff --git a/keyserv/models.py b/keyserv/models.py new file mode 100644 index 0000000..fb798ed --- /dev/null +++ b/keyserv/models.py @@ -0,0 +1,104 @@ +# MIT License + +# Copyright(c) 2018 Samuel Hoffman + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from datetime import datetime +from enum import IntEnum +from typing import Any # NOQA: F401 + +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() # type: Any + + +class Application(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String, nullable=False, unique=True) + + +class Key(db.Model): + """ + Database representation of a software key provided by MKS. + + Id: identifier for a kkey + token: the license token fed to the program + remaining: remaining activations for a key. -1 if unlimited + enabled: if the license is able to + """ + id = db.Column(db.Integer, primary_key=True) + token = db.Column(db.String, unique=True) + remaining = db.Column(db.Integer) + enabled = db.Column(db.Boolean, default=True) + memo = db.Column(db.String) + cutdate = db.Column(db.DateTime) + app_id = db.Column(db.Integer, db.ForeignKey("application.id"), nullable=False) + app = db.relationship("Application", uselist=False, backref="keys") + + def __init__(self, token: str, remaining: int, app_id: int, + enabled: bool=True, memo: str="") -> None: + self.token = token + self.remaining = remaining + self.enabled = enabled + self.memo = memo + self.app_id = app_id + + def __str__(self): + return f"" + + +class Event(IntEnum): + Info = 0 + Warn = 1 + Error = 2 + AppActivation = 3 + FailedActivation = 4 + KeyModified = 5 + KeyCreated = 6 + KeyAccess = 7 + AppCreated = 8 + AppModified = 9 + + +class AuditLog(db.Model): + """ + Database representation of an audit log. + """ + id = db.Column(db.Integer, primary_key=True) + key_id = db.Column(db.Integer, db.ForeignKey("key.id"), nullable=False) + key = db.relationship("Key", uselist=False, backref="logs") + app_id = db.Column(db.Integer, db.ForeignKey("application.id"), nullable=False) + app = db.relationship("Application", backref="logs") + message = db.Column(db.String) + event_type = db.Column(db.Integer) + timestamp = db.Column(db.DateTime) + + def __init__(self, key_id: int, app_id: int, message: str, event_type: Event) -> None: + self.key_id = key_id + self.app_id = app_id + self.message = message + self.event_type = int(event_type) + self.timestamp = datetime.now() + + @classmethod + def from_key(cls, key: Key, message: str, event_type: Event): + audit = cls(key.id, key.app.id, message, event_type) + db.session.add(audit) + db.session.commit() diff --git a/keyserv/templates/add_modify.html b/keyserv/templates/add_modify.html new file mode 100644 index 0000000..f89feb3 --- /dev/null +++ b/keyserv/templates/add_modify.html @@ -0,0 +1,34 @@ +{# + MIT License + + Copyright(c) 2018 Samuel Hoffman + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files(the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and / or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +#} + +{% import "bootstrap/wtf.html" as wtf %} +{% import "bootstrap/utils.html" as utils %} +{% extends "layout.html" %} + +{% block title %}Mini Key Server - {{ header }}{% endblock%} + +{% block container %} +

{{ header }}

+{{ wtf.quick_form(form) }} +{%- endblock %} diff --git a/keyserv/templates/applications.html b/keyserv/templates/applications.html new file mode 100644 index 0000000..1a16648 --- /dev/null +++ b/keyserv/templates/applications.html @@ -0,0 +1,71 @@ +{# + MIT License + + Copyright(c) 2018 Samuel Hoffman + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files(the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and / or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +#} + +{% import "bootstrap/wtf.html" as wtf %} +{% import "bootstrap/utils.html" as utils %} +{% extends "layout.html" %} + +{% block title %}Mini Key Server - Keys{% endblock%} + +{% block container %} +

Applications

+ + + Add Application + + +{% if apps %} + + + + + + + + + + + + {% for app in apps %} + + + + + + + {% endfor %} + +
IDNameKeysActions
{{ app.id }}{{ app.name }}{{ app.keys|length }} + Modify + + Detail +
+{% else %} +

No applications added. + Add + one to get started.

+{% endif %} +{%- endblock %} diff --git a/keyserv/templates/detail_app.html b/keyserv/templates/detail_app.html new file mode 100644 index 0000000..0903b9a --- /dev/null +++ b/keyserv/templates/detail_app.html @@ -0,0 +1,70 @@ +{# + MIT License + + Copyright(c) 2018 Samuel Hoffman + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files(the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and / or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +#} +{% extends "layout.html" %} + +{% block title %}Mini Key Server - Application Detail{% endblock%} + +{% block container %} +

Detail for Application {{ app.name }}

+ + + +
+ +
{{ app.name }}
+
+ + Modify +

+ +
+
+ +

Audit Log

+ + + + + + + + + + + + {% for log in app.logs %} + + + + + + + + {% endfor %} + +
IDKeyTime StampMessageEvent
{{ log.id }}{{ log.key_id }}{{ log.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}{{ log.message }}{{ log.event_type|event }}
+{%- endblock %} diff --git a/keyserv/templates/detail_key.html b/keyserv/templates/detail_key.html new file mode 100644 index 0000000..b42e047 --- /dev/null +++ b/keyserv/templates/detail_key.html @@ -0,0 +1,77 @@ +{# + MIT License + + Copyright(c) 2018 Samuel Hoffman + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files(the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and / or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +#} +{% extends "layout.html" %} + +{% block title %}Mini Key Server - Key Detail{% endblock%} + +{% block container %} +

Detail for Key {{ key.id }}

+ + + +
+ +
Key Info for {{ key.app.name }}
+
+ + Modify +

+
    +
  • Token: {{ key.token }}
  • +
  • Remaining Activations: + {% if key.remaining == -1 %} + Unlimited + {% else %} + {{ key.remaining }} + {% endif %}
  • +
  • Cut On: {{ key.cutdate.strftime("%Y-%m-%d %H:%M:%S ") }}
  • + {% if key.memo %} +
  • Memo: {{ key.memo }}
  • + {% endif %} +
+
+
+ +

Audit Log

+ + + + + + + + + + + {% for log in key.logs %} + + + + + + + {% endfor %} + +
IDTime StampMessageEvent
{{ log.id }}{{ log.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}{{ log.message }}{{ log.event_type|event }}
+{%- endblock %} diff --git a/keyserv/templates/index.html b/keyserv/templates/index.html new file mode 100644 index 0000000..c60521f --- /dev/null +++ b/keyserv/templates/index.html @@ -0,0 +1,40 @@ +{# + MIT License + + Copyright(c) 2018 Samuel Hoffman + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files(the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and / or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +#} + +{% import "bootstrap/wtf.html" as wtf %} +{% extends "layout.html" %} + +{% block title %}Mini Key Server{% endblock%} + +{% block container %} + +{% if current_user.is_authenticated %} +

Currently signed in as + {{ current_user.username }} +

+{% else %} +

Please login to continue.

+{{ wtf.quick_form(form) }} {% endif %} + +{%- endblock %} diff --git a/keyserv/templates/keys.html b/keyserv/templates/keys.html new file mode 100644 index 0000000..349acfa --- /dev/null +++ b/keyserv/templates/keys.html @@ -0,0 +1,78 @@ +{# + MIT License + + Copyright(c) 2018 Samuel Hoffman + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files(the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and / or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +#} + +{% import "bootstrap/wtf.html" as wtf %} +{% import "bootstrap/utils.html" as utils %} +{% extends "layout.html" %} + +{% block title %}Mini Key Server - Keys{% endblock%} + +{% block container %} +

Keys

+ + Add Key + +{% if keys %} + + + + + + + + + + + + + + + {% for key in keys %} + + + + + + + + + + + {% endfor %} + +
IDTokenApplicationActiveRemaining ActivationsCut DateMemoModify
{{ key.id }}{{ key.token }}{{ key.app.name }}{% if key.active %}Yes{% else %}No{% endif %}{% if key.remaining == -1 %} + Unlimited + {% else %} + {{ key.remaining }} + {% endif %}{{ key.cutdate.strftime('%Y-%m-%d %H:%M:%S') }}{{ key.memo }} + Modify + + + Detail
+{% else %} +

No keys have been cut.

+{% endif %} +{%- endblock %} diff --git a/keyserv/templates/layout.html b/keyserv/templates/layout.html new file mode 100644 index 0000000..4620357 --- /dev/null +++ b/keyserv/templates/layout.html @@ -0,0 +1,60 @@ +{# + MIT License + + Copyright(c) 2018 Samuel Hoffman + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files(the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and / or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +#} + +{% extends "bootstrap/base.html" %} +{% import "bootstrap/utils.html" as utils %} + +{% block title %}Mini Key Server{% endblock %} + +{% block styles %} {{ super() }} + +{% endblock styles %} + +{% block content %} + + +
+

Mini Key Server

+
+
+ {{ utils.flashed_messages(messages) }} +
+
+ + Keys + + Applications + + Audit Log + + Log Out +
+ {% block container %}{% endblock container %} +
+
+
+

Copyright © 2017, GliTch_ Is Mad Studios

+
+
+{% endblock %} diff --git a/keyserv/templates/logs.html b/keyserv/templates/logs.html new file mode 100644 index 0000000..c7119ca --- /dev/null +++ b/keyserv/templates/logs.html @@ -0,0 +1,52 @@ +{# + MIT License + + Copyright(c) 2018 Samuel Hoffman + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files(the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and / or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +#} +{% extends "layout.html" %} + +{% block title %}Mini Key Server - Audit Log{% endblock%} + +{% block container %} +

Audit Log

+ + + + + + + + + + + + {% for log in logs %} + + + + + + + + {% endfor %} + +
KeyApplicationTime StampMessageEvent
{{ log.key.id }}{{ log.app.name }}{{ log.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}{{ log.message }}{{ log.event_type|event }}
+{%- endblock %} diff --git a/keyserv/views.py b/keyserv/views.py new file mode 100644 index 0000000..cac15a5 --- /dev/null +++ b/keyserv/views.py @@ -0,0 +1,215 @@ +# MIT License + +# Copyright(c) 2018 Samuel Hoffman + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import psycopg2 +from flask import (Blueprint, abort, current_app, flash, redirect, + render_template, request, url_for) +from flask_login import current_user, login_required, login_user, logout_user + +from keyserv.auth import Users +from keyserv.forms import AppForm, KeyForm, LoginForm +from keyserv.keymanager import cut_key_unsafe +from keyserv.models import Application, AuditLog, Event, Key, db + +frontend = Blueprint("frontend", __name__) + + +@frontend.route("/", methods=["GET", "POST"]) +def index(): + + form = LoginForm(request.form) + + if request.method == "POST" and form.validate(): + current_app.logger.debug("login form was submitted") + user = Users.query.filter_by(username=form.username.data).first() + if user and user.check_password(form.password.data): + if login_user(user): + current_app.logger.debug(f"login for {user}") + else: + flash("Invalid username or password.", "error") + return redirect(url_for("frontend.index")) + + return render_template("index.html", form=form, current_user=current_user) + + +@frontend.route("/logout") +def logout(): + logout_user() + return redirect(url_for("frontend.index")) + + +@frontend.route("/keys") +@login_required +def keys(): + return render_template("keys.html", keys=Key.query.all()) + + +@frontend.route("/applications") +@login_required +def apps(): + return render_template("applications.html", apps=Application.query.all()) + + +@frontend.route("/logs") +@login_required +def logs(): + return render_template("logs.html", logs=AuditLog.query.all()) + + +@frontend.route("/modify/key/", methods=["GET", "POST"]) +@login_required +def modify_key(key_id: int): + + key = Key.query.get(key_id) + if not key: + abort(404) + + form = KeyForm(request.form) + form.application.choices = [(app.id, app.name) for app in Application.query.all()] + + if request.method == "POST" and form.validate_on_submit(): + changes = [] + + if key.remaining != form.activations.data: + changes.append(f"activations changed from {key.remaining} to {form.activations.data}") + key.remaining = form.activations.data + if key.memo != form.memo.data: + changes.append(f"memo changed from {key.memo!r} to {form.memo.data!r}") + key.memo = form.memo.data + if key.app_id != form.application.data: + changes.append(f"app changed from {key.app} to {form.application.data}") + key.application = form.application.data + if key.enabled != form.active.data: + changes.append(f"active changed from {key.enabled} to {form.active.data}") + key.enabled = form.active.data + + AuditLog.from_key(key, f"edited by {current_user.username} ({request.remote_addr}):" + f" {', '.join(changes)}", Event.KeyModified) + + try: + db.session.commit() + flash("Changes successful!") + return redirect(url_for("frontend.detail_key", key_id=key.id)) + except psycopg2.Error as error: + flash(f"Failed to update key: {error}") + + form.application.data = key.app_id + form.active.data = key.enabled + form.memo.data = key.memo + form.activations.data = key.remaining + + return render_template("add_modify.html", header=f"Modify Key {key.id}", form=form) + + +@frontend.route("/add/key", methods=["GET", "POST"]) +@login_required +def add_key(): + form = KeyForm(request.form) + form.application.choices = [(app.id, app.name) for app in Application.query.all()] + + if request.method == "POST" and form.validate_on_submit(): + try: + token = cut_key_unsafe(form.activations.data, form.application.data, + form.active.data, form.memo.data) + flash(f"Key added! Token: {token}", "success") + except psycopg2.Error as error: + flash(f"Unable to add key: {error}", "error") + + return render_template("add_modify.html", header="Add Key", form=form) + + +@frontend.route("/add/app", methods=["GET", "POST"]) +@login_required +def add_app(): + form = AppForm(request.form) + + if request.method == "POST" and form.validate_on_submit(): + app = Application() + app.name = form.name.data + + db.session.add(app) + try: + db.session.commit() + flash("Success!") + except psycopg2.Error as error: + flash(f"Failed to add application: {error}") + + return render_template("add_modify.html", form=form, header="Add Application") + + +@frontend.route("/modify/app/", methods=["GET", "POST"]) +@login_required +def modify_app(app_id: int): + app = Application.query.get(app_id) + + if not app: + abort(404) + + form = AppForm(request.form) + if request.method == "POST" and form.validate_on_submit(): + + app.name = form.name.data + try: + db.session.commit() + flash("Success.") + except Exception as error: + flash(f"Failed to modify application: {error}", "error") + + form.name.data = app.name + + return render_template("add_modify.html", form=form) + + +@frontend.route("/detail/key/") +@login_required +def detail_key(key_id: int): + + key = Key.query.get(key_id) + + if not key: + abort(404) + + return render_template("detail_key.html", key=key) + + +@frontend.route("/etail/app/") +@login_required +def detail_app(app_id: int): + + app = Application.query.get(app_id) + + if not app: + abort(404) + + return render_template("detail_app.html", app=app) + + +@frontend.route("/keys/app/") +@login_required +def keys_for_app(app_id): + + app = Application.query.get(app_id) + + if not app: + abort(404) + + return render_template("keys.html", keys=app.keys) diff --git a/keyserver.py b/keyserver.py new file mode 100644 index 0000000..aeccb9f --- /dev/null +++ b/keyserver.py @@ -0,0 +1,6 @@ +from keyserv import create_app + +app = create_app("DevelopmentConfig") + +if __name__ == '__main__': + app.run() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bdd00f6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +argon2_cffi +flask +flask_bootstrap +flask_login +flask_restful +flask_sqlalchemy +flask_wtf +psycopg2 +wtforms +uwsgi