From cb1fcd88c8237dbf482a3c48e7a57cf730520b91 Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Thu, 2 Jul 2015 22:29:05 -0600 Subject: [PATCH 01/13] Database basics added(except delete). Filler for run_query added, but not yet functional. --- app.py | 90 +++++++++++++++++++++++++++++--- db.py | 31 ++++++++++- templates/edit_database.html | 59 +++++++++++++++++++++ templates/index.html | 99 +++++++++++++++++++++++++++++++++++- templates/run_query.html | 46 +++++++++++++++++ templates/view_database.html | 42 +++++++++++++++ 6 files changed, 358 insertions(+), 9 deletions(-) create mode 100644 templates/edit_database.html create mode 100644 templates/run_query.html create mode 100644 templates/view_database.html diff --git a/app.py b/app.py index f7d962a..6daf5f5 100644 --- a/app.py +++ b/app.py @@ -9,18 +9,19 @@ @app.route("/") def index(): queries = db.get_queries() - return render_template("index.html", queries=queries) + databases = db.get_databases() + return render_template("index.html", queries=queries, databases=databases) @app.route("/query/", methods=["POST"]) def query_add(): try: - title = request.form["title"].strip() - sql = request.form["sql"].strip() - tags = request.form["tags"].strip() - desc = request.form["desc"].strip() - who = request.form["who"].strip() + title = request.form.get("title").strip() + sql = request.form.get("sql").strip() + tags = request.form.get("tags").strip() + desc = request.form.get("desc").strip() + who = request.form.get("who").strip() - if len(title) == 0 or len(sql) == 0: + if not title or not sql == 0: flash("Title and Sql are required fields", "error") return redirect(url_for("index")) @@ -75,5 +76,80 @@ def query_delete(id): flash("Delete Successful", "success") return redirect(url_for("index")) +@app.route("/query//run/", methods=["GET", "POST"]) +def query_run(id, database_id): + """ Run a query against a specific database instance """ + if config.enable_run_query: + database = db.get_database_details(database_id) + query = db.get_query_details(id) + query_results = 'wut wut' + else: + database = None + query = None + query_results = None + return render_template("run_query.html", query=query, database=database, query_results=query_results) + +@app.route("/database/", methods=["POST"]) +def database_add(): + try: + name = request.form.get("name").strip() + hostname = request.form.get("hostname").strip() + port = request.form.get("port").strip() + desc = request.form.get("desc").strip() + + if not name or not hostname: + flash("Name and Host/File Name are required fields", "error") + return redirect(url_for("index")) + + db.insert_database(name, hostname, port, desc) + flash("Database Added!", "success") + return redirect(url_for("index")) + + except Exception as e: + app.logger.error("Fatal error", exc_info=True) + flash("Fatal error. Contact Administrator", "error") + return redirect(url_for("index")) + +@app.route("/database//edit/", methods=["GET", "POST"]) +def database_edit(id): + if request.method == "GET": + database = db.get_database_details(id) + return render_template("edit_database.html", database=database) + elif request.method == "POST": + try: + id = request.form["id"].strip() + name = request.form.get("name", "").strip() + hostname = request.form.get("hostname", "").strip() + port = request.form.get("port", "").strip() + desc = request.form.get("desc", "").strip() + + if not name or not hostname: + flash("Name and Host/File Name are required fields", "error") + return redirect(url_for("database_edit", id=id)) + + db.update_database(id, name, hostname, port, desc) + flash("Database Modified!", "success") + return redirect(url_for("database_view", id=id)) + + except Exception as e: + app.logger.error("Fatal error", exc_info=True) + flash("Fatal error. Contact Administrator", "error") + return redirect(url_for("index")) + +@app.route("/database//", methods=["GET", "DELETE"]) +def database_view(id): + database = db.get_database_details(id) + return render_template("view_database.html", database=database) + +@app.route("/database//delete/", methods=["GET", "POST"]) +def database_delete(id): + if request.method == "GET": + database = db.get_database_details(id) + return render_template("delete_database.html", query=query) + elif request.method == "POST": + db.delete_database(id) + flash("Delete Successful", "success") + return redirect(url_for("index")) + if __name__ == "__main__": app.run(debug=config.flask_debug, port=config.flask_port, host=config.flask_bind_address) diff --git a/db.py b/db.py index e275873..99627cb 100644 --- a/db.py +++ b/db.py @@ -5,7 +5,8 @@ client = m.MongoClient(config.mongo_hostname, config.mongo_port) db = client[config.mongo_db] -queries = db[config.mongo_collection] +queries = db[config.query_mongo_collection] +databases = db[config.database_mongo_collection] def get_tags_list(tags): return [ tag.strip() for tag in tags.strip().split(",") \ @@ -42,3 +43,31 @@ def get_queries(): def get_query_details(id): return queries.find_one(ObjectId(id)) + +def insert_database(name, hostname, port, desc): + databases.insert({ + "name": name, + "hostname": hostname, + "port": port, + "desc": desc + }) + +def update_database(id, name, hostname, port, desc): + databases.update({ + "_id": ObjectId(id) + }, + { "$set" : { + "name": name, + "hostname": hostname, + "port": port, + "desc": desc + }}, upsert=False) + +def delete_database(id): + databases.remove({"_id": ObjectId(id)}) + +def get_databases(): + return list(databases.find()) + +def get_database_details(id): + return databases.find_one(ObjectId(id)) diff --git a/templates/edit_database.html b/templates/edit_database.html new file mode 100644 index 0000000..ebf2ae6 --- /dev/null +++ b/templates/edit_database.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} + +{% block container %} + +
+
+
+
+

Edit Database

+
+
+
+ + + +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ cancel +
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/templates/index.html b/templates/index.html index 63af4d7..834cc5a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -26,10 +26,44 @@
+ +
+
+ + + + + + + + {% if databases|length > 0 %} + {% for database in databases %} + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} +
NameHost/File NamePort
{{database.name}}{{database.hostname}}{{database.port}} + View + Edit + Delete +
No Databases added yet. For Queries to run from LSQ, they need to be associated to a database.
+
+
+
+
@@ -47,8 +81,19 @@ {{tag}} {% endfor %} - @@ -111,10 +156,62 @@ + + {% endblock %} diff --git a/templates/run_query.html b/templates/run_query.html new file mode 100644 index 0000000..a7dcf8f --- /dev/null +++ b/templates/run_query.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} + +{% block pagescripts %} + + + + + +{% endblock %} + +{% block container %} + + Back + +
+
+

Results of {{ query.title }}

+
+
+ +
+ + {% if database %} + +

Query

+ +
+
+
{{ query.sql }}
+
+
+ +

Results

+ +
+
+ {{ query_results }} +
+
+ {% else %} +
+
{% if not query %}Query and {% endif %}Database not selected.
+
+ {% endif %} + +{% endblock %} \ No newline at end of file diff --git a/templates/view_database.html b/templates/view_database.html new file mode 100644 index 0000000..1d14efe --- /dev/null +++ b/templates/view_database.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block pagescripts %} + + + + + +{% endblock %} + +{% block container %} + + Back + Edit + Delete + +
+
+

{{ database.title }}

+
+
+ +
+ +
+
+
+ View +
+ + +
Edit Delete
+ + + + + + + + + + +
Host/File NamePortDescription
{{ database.hostname }}{{ database.port }}{{ database.desc }}
+
+
+ +{% endblock %} From 0ad8c96e1c020ab13c185215bd736104616e303b Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Fri, 3 Jul 2015 21:05:46 -0600 Subject: [PATCH 02/13] A whole bunch of stuff added for storing databases and running queries against a backend database. --- aes.py | 29 +++++++++++++ app.py | 76 ++++++++++++++++++++++++++++++----- db.py | 36 +++++++++++++---- templates/edit_database.html | 14 +++++++ templates/index.html | 51 +---------------------- templates/modal_database.html | 62 ++++++++++++++++++++++++++++ templates/run_query.html | 32 ++++++++++++++- templates/view_database.html | 2 + templates/view_query.html | 32 +++++++++++++-- 9 files changed, 261 insertions(+), 73 deletions(-) create mode 100644 aes.py create mode 100644 templates/modal_database.html diff --git a/aes.py b/aes.py new file mode 100644 index 0000000..70a33e4 --- /dev/null +++ b/aes.py @@ -0,0 +1,29 @@ +import base64 +import hashlib +from Crypto import Random +from Crypto.Cipher import AES + +class AESCipher(object): + + def __init__(self, key): + self.bs = 32 + self.key = hashlib.sha256(key.encode()).digest() + + def encrypt(self, raw): + raw = self._pad(raw) + iv = Random.new().read(AES.block_size) + cipher = AES.new(self.key, AES.MODE_CBC, iv) + return base64.b64encode(iv + cipher.encrypt(raw)) + + def decrypt(self, enc): + enc = base64.b64decode(enc) + iv = enc[:AES.block_size] + cipher = AES.new(self.key, AES.MODE_CBC, iv) + return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8') + + def _pad(self, s): + return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs) + + @staticmethod + def _unpad(s): + return s[:-ord(s[len(s)-1:])] \ No newline at end of file diff --git a/app.py b/app.py index 6daf5f5..c77a028 100644 --- a/app.py +++ b/app.py @@ -2,6 +2,7 @@ import db import config +from aes import AESCipher app = Flask(__name__) app.secret_key = config.flask_secret_key @@ -21,7 +22,7 @@ def query_add(): desc = request.form.get("desc").strip() who = request.form.get("who").strip() - if not title or not sql == 0: + if not title or not sql: flash("Title and Sql are required fields", "error") return redirect(url_for("index")) @@ -36,8 +37,9 @@ def query_add(): @app.route("/query//", methods=["GET", "DELETE"]) def query_view(id): + databases = db.get_databases() query = db.get_query_details(id) - return render_template("view_query.html", query=query) + return render_template("view_query.html", databases=databases, query=query) @app.route("/query//edit/", methods=["GET", "POST"]) def query_edit(id): @@ -82,26 +84,74 @@ def query_run(id, database_id): if config.enable_run_query: database = db.get_database_details(database_id) query = db.get_query_details(id) - query_results = 'wut wut' + query_results = None + query_results_cols = [] + error = None + + # try and import the DB engine + try: + dbapi2 = __import__(config.database_engine) + except ImportError as e: + app.logger.error("Fatal error", exc_info=True) + flash("Fatal error. Contact Administrator", "error") + return redirect(url_for("index")) + finally: + # try and make the connection and run the query + try: + if database.get("password"): + crypt = AESCipher(config.flask_secret_key) + password = crypt.decrypt(database.get("password")) + else: + password = None + + connect = dbapi2.connect(database=database.get("name"), + host=database.get("hostname"), + port=database.get("port"), + user=database.get("user"), + password=password) + curse = connect.cursor() + curse.execute(query["sql"]) + query_results = curse.fetchall() + # Assemble column names so the table makes sense + for col in curse.description: + query_results_cols.append(col.name) + + except dbapi2.ProgrammingError, e: + # TODO: Exceptions don't seem to be standard in DB-API2, + # so this will likely have to be checked against other + # engines. The following works with psycopg2. + if hasattr(e, "pgerror"): + error = e.pgerror + else: + error = "There was an error with your query." + except dbapi2.Error as e: + if hasattr(e, "pgerror"): + error = e.pgerror + else: + error = e.msg + else: database = None query = None query_results = None - return render_template("run_query.html", query=query, database=database, query_results=query_results) + error = None + return render_template("run_query.html", query=query, database=database, query_results=query_results, query_results_cols=query_results_cols, error=error) @app.route("/database/", methods=["POST"]) def database_add(): try: name = request.form.get("name").strip() hostname = request.form.get("hostname").strip() - port = request.form.get("port").strip() - desc = request.form.get("desc").strip() + port = request.form.get("port", "").strip() + user = request.form.get("user", "").strip() + password = request.form.get("password", "").strip() + desc = request.form.get("desc", "").strip() if not name or not hostname: flash("Name and Host/File Name are required fields", "error") return redirect(url_for("index")) - db.insert_database(name, hostname, port, desc) + db.insert_database(name, hostname, port, user, password, desc) flash("Database Added!", "success") return redirect(url_for("index")) @@ -114,20 +164,26 @@ def database_add(): def database_edit(id): if request.method == "GET": database = db.get_database_details(id) + if database['password']: + database['password'] = 'placeholder' return render_template("edit_database.html", database=database) elif request.method == "POST": try: id = request.form["id"].strip() - name = request.form.get("name", "").strip() - hostname = request.form.get("hostname", "").strip() + name = request.form.get("name").strip() + hostname = request.form.get("hostname").strip() port = request.form.get("port", "").strip() + user = request.form.get("user", "").strip() + password = request.form.get("password", "").strip() + if password == 'placeholder': + password = None desc = request.form.get("desc", "").strip() if not name or not hostname: flash("Name and Host/File Name are required fields", "error") return redirect(url_for("database_edit", id=id)) - db.update_database(id, name, hostname, port, desc) + db.update_database(id, name, hostname, port, user, password, desc) flash("Database Modified!", "success") return redirect(url_for("database_view", id=id)) diff --git a/db.py b/db.py index 99627cb..aebf7bd 100644 --- a/db.py +++ b/db.py @@ -2,6 +2,7 @@ from bson.objectid import ObjectId import config +from aes import AESCipher client = m.MongoClient(config.mongo_hostname, config.mongo_port) db = client[config.mongo_db] @@ -44,24 +45,43 @@ def get_queries(): def get_query_details(id): return queries.find_one(ObjectId(id)) -def insert_database(name, hostname, port, desc): +def insert_database(name, hostname, port=None, user=None, password=None, desc=None): + if password: + crypt = AESCipher(config.flask_secret_key) + encrypted_password = crypt.encrypt(password) + else: + encrypted_password = None + databases.insert({ "name": name, "hostname": hostname, "port": port, + "user": user, + "password": encrypted_password, "desc": desc }) -def update_database(id, name, hostname, port, desc): +def update_database(id, name, hostname, port=None, user=None, password=None, desc=None): + + db_set = { + "name": name, + "hostname": hostname, + "port": port, + "user": user, + "desc": desc + } + + if password: + crypt = AESCipher(config.flask_secret_key) + encrypted_password = crypt.encrypt(password) + db_set["password"] = encrypted_password + else: + encrypted_password = None + databases.update({ "_id": ObjectId(id) }, - { "$set" : { - "name": name, - "hostname": hostname, - "port": port, - "desc": desc - }}, upsert=False) + { "$set" : db_set}, upsert=False) def delete_database(id): databases.remove({"_id": ObjectId(id)}) diff --git a/templates/edit_database.html b/templates/edit_database.html index ebf2ae6..6294dd3 100644 --- a/templates/edit_database.html +++ b/templates/edit_database.html @@ -34,6 +34,20 @@

Edit Database


+
+
+ +
+
+
+ +
+
+ +
+
+
+
diff --git a/templates/index.html b/templates/index.html index 834cc5a..27dbd00 100644 --- a/templates/index.html +++ b/templates/index.html @@ -85,7 +85,7 @@ View
- + {% include "modal_database.html" %} {% endblock %} From 98892604dec5aa46dff41ed4cc32cb865d81e256 Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Fri, 3 Jul 2015 21:08:09 -0600 Subject: [PATCH 03/13] Updated config file for new configuration for databases and running queries. --- config.py.sample | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/config.py.sample b/config.py.sample index b02e5d7..369e586 100644 --- a/config.py.sample +++ b/config.py.sample @@ -2,10 +2,19 @@ flask_debug=True flask_bind_address="0.0.0.0" flask_port=5000 +# For security purposes, this field should be sufficiently randomized flask_secret_key="" # Mongo properties mongo_hostname="localhost" mongo_port=27017 mongo_db="company_queries" -mongo_collection="queries" +query_mongo_collection="queries" +database_mongo_collection="databases" + +# Whether or not running queries is allowed +enable_run_query = False + +# The database engine should be DB-API2 compatible module that can be +# imported. More info: https://wiki.python.org/moin/DbApiFaq +database_engine = 'psycopg2' \ No newline at end of file From 20d11585faef414c4f4e5a09c666b6b54743eb6b Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Tue, 28 Mar 2017 15:30:16 -0600 Subject: [PATCH 04/13] Better name to fit within the naming scheme. --- app.py | 91 ++++++++++++++++++++++++++++++--- config.py.sample | 1 + db.py | 29 +++++++++++ templates/edit_database.html | 59 ++++++++++++++++++++++ templates/index.html | 97 ++++++++++++++++++++++++++++++++++++ templates/run_query.html | 46 +++++++++++++++++ templates/view_database.html | 42 ++++++++++++++++ 7 files changed, 358 insertions(+), 7 deletions(-) create mode 100644 templates/edit_database.html create mode 100644 templates/run_query.html create mode 100644 templates/view_database.html diff --git a/app.py b/app.py index 4929e64..830bdc2 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,4 @@ -from flask import Flask, render_template, request, redirect, url_for, flash, jsonify +from flask import Flask, render_template, request, redirect, url_for, flash import db import config @@ -16,6 +16,8 @@ def index(): queries = db.get_queries() else: queries = db.get_queries() + + databases = db.get_databases() return render_template("index.html", queries=queries, issearchword=searchword) @@ -35,13 +37,13 @@ def query_list(): @app.route("/query/", methods=["POST"]) def query_add(): try: - title = request.form["title"].strip() - sql = request.form["sql"].strip() - tags = request.form["tags"].strip() - desc = request.form["desc"].strip() - who = request.form["who"].strip() + title = request.form.get("title").strip() + sql = request.form.get("sql").strip() + tags = request.form.get("tags").strip() + desc = request.form.get("desc").strip() + who = request.form.get("who").strip() - if len(title) == 0 or len(sql) == 0: + if not title or not sql: flash("Title and Sql are required fields", "error") return redirect(url_for("index")) @@ -108,5 +110,80 @@ def query_delete(id): flash("Delete Successful", "success") return redirect(url_for("index")) +@app.route("/query//run/", methods=["GET", "POST"]) +def query_run(id, database_id): + """ Run a query against a specific database instance """ + if config.enable_run_query: + database = db.get_database_details(database_id) + query = db.get_query_details(id) + query_results = 'wut wut' + else: + database = None + query = None + query_results = None + return render_template("run_query.html", query=query, database=database, query_results=query_results) + +@app.route("/database/", methods=["POST"]) +def database_add(): + try: + name = request.form.get("name").strip() + hostname = request.form.get("hostname").strip() + port = request.form.get("port").strip() + desc = request.form.get("desc").strip() + + if not name or not hostname: + flash("Name and Host/File Name are required fields", "error") + return redirect(url_for("index")) + + db.insert_database(name, hostname, port, desc) + flash("Database Added!", "success") + return redirect(url_for("index")) + + except Exception as e: + app.logger.error("Fatal error", exc_info=True) + flash("Fatal error. Contact Administrator", "error") + return redirect(url_for("index")) + +@app.route("/database//edit/", methods=["GET", "POST"]) +def database_edit(id): + if request.method == "GET": + database = db.get_database_details(id) + return render_template("edit_database.html", database=database) + elif request.method == "POST": + try: + id = request.form["id"].strip() + name = request.form.get("name", "").strip() + hostname = request.form.get("hostname", "").strip() + port = request.form.get("port", "").strip() + desc = request.form.get("desc", "").strip() + + if not name or not hostname: + flash("Name and Host/File Name are required fields", "error") + return redirect(url_for("database_edit", id=id)) + + db.update_database(id, name, hostname, port, desc) + flash("Database Modified!", "success") + return redirect(url_for("database_view", id=id)) + + except Exception as e: + app.logger.error("Fatal error", exc_info=True) + flash("Fatal error. Contact Administrator", "error") + return redirect(url_for("index")) + +@app.route("/database//", methods=["GET", "DELETE"]) +def database_view(id): + database = db.get_database_details(id) + return render_template("view_database.html", database=database) + +@app.route("/database//delete/", methods=["GET", "POST"]) +def database_delete(id): + if request.method == "GET": + database = db.get_database_details(id) + return render_template("delete_database.html", query=query) + elif request.method == "POST": + db.delete_database(id) + flash("Delete Successful", "success") + return redirect(url_for("index")) + if __name__ == "__main__": app.run(debug=config.flask_debug, port=config.flask_port, host=config.flask_bind_address) diff --git a/config.py.sample b/config.py.sample index cc96f5d..d4d0c97 100644 --- a/config.py.sample +++ b/config.py.sample @@ -9,5 +9,6 @@ mongo_hostname="localhost" mongo_port=27017 mongo_db="company_queries" mongo_collection="queries" +mongo_collection_database="databases" mongo_username="" mongo_password="" diff --git a/db.py b/db.py index 9aaf4d6..08a11be 100644 --- a/db.py +++ b/db.py @@ -11,6 +11,7 @@ db.authenticate(config.mongo_username, config.mongo_password) queries = db[config.mongo_collection] +databases = db[config.mongo_collection_database] def get_tags_list(tags): return [ tag.strip() for tag in tags.strip().split(",") \ @@ -61,3 +62,31 @@ def get_queries(): def get_query_details(id): return queries.find_one(ObjectId(id)) + +def insert_database(name, hostname, port, desc): + databases.insert({ + "name": name, + "hostname": hostname, + "port": port, + "desc": desc + }) + +def update_database(id, name, hostname, port, desc): + databases.update({ + "_id": ObjectId(id) + }, + { "$set" : { + "name": name, + "hostname": hostname, + "port": port, + "desc": desc + }}, upsert=False) + +def delete_database(id): + databases.remove({"_id": ObjectId(id)}) + +def get_databases(): + return list(databases.find()) + +def get_database_details(id): + return databases.find_one(ObjectId(id)) diff --git a/templates/edit_database.html b/templates/edit_database.html new file mode 100644 index 0000000..ebf2ae6 --- /dev/null +++ b/templates/edit_database.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} + +{% block container %} + +
+
+
+
+

Edit Database

+
+
+
+ + + +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ cancel +
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/templates/index.html b/templates/index.html index 7045399..e3c5b0d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -43,10 +43,44 @@

+ +
+
+ + + + + + + + {% if databases|length > 0 %} + {% for database in databases %} + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} +
NameHost/File NamePort
{{database.name}}{{database.hostname}}{{database.port}} + View + Edit + Delete +
No Databases added yet. For Queries to run from LSQ, they need to be associated to a database.
+
+
+
+
@@ -68,6 +102,17 @@ @@ -130,10 +175,62 @@ + + {% endblock %} diff --git a/templates/run_query.html b/templates/run_query.html new file mode 100644 index 0000000..a7dcf8f --- /dev/null +++ b/templates/run_query.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} + +{% block pagescripts %} + + + + + +{% endblock %} + +{% block container %} + + Back + +
+
+

Results of {{ query.title }}

+
+
+ +
+ + {% if database %} + +

Query

+ +
+
+
{{ query.sql }}
+
+
+ +

Results

+ +
+
+ {{ query_results }} +
+
+ {% else %} +
+
{% if not query %}Query and {% endif %}Database not selected.
+
+ {% endif %} + +{% endblock %} \ No newline at end of file diff --git a/templates/view_database.html b/templates/view_database.html new file mode 100644 index 0000000..1d14efe --- /dev/null +++ b/templates/view_database.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block pagescripts %} + + + + + +{% endblock %} + +{% block container %} + + Back + Edit + Delete + +
+
+

{{ database.title }}

+
+
+ +
+ +
+
+
{{query.who}} View +
+ + +
Edit Delete
+ + + + + + + + + + +
Host/File NamePortDescription
{{ database.hostname }}{{ database.port }}{{ database.desc }}
+
+
+ +{% endblock %} From b83dbba86ef540ea5cc2944fdede467911dfa3a0 Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Fri, 3 Jul 2015 21:05:46 -0600 Subject: [PATCH 05/13] A whole bunch of stuff added for storing databases and running queries against a backend database. --- aes.py | 29 ++++++++++++++ app.py | 74 ++++++++++++++++++++++++++++++----- db.py | 36 +++++++++++++---- templates/edit_database.html | 14 +++++++ templates/index.html | 51 +----------------------- templates/modal_database.html | 62 +++++++++++++++++++++++++++++ templates/run_query.html | 32 ++++++++++++++- templates/view_database.html | 2 + templates/view_query.html | 32 +++++++++++++-- 9 files changed, 260 insertions(+), 72 deletions(-) create mode 100644 aes.py create mode 100644 templates/modal_database.html diff --git a/aes.py b/aes.py new file mode 100644 index 0000000..70a33e4 --- /dev/null +++ b/aes.py @@ -0,0 +1,29 @@ +import base64 +import hashlib +from Crypto import Random +from Crypto.Cipher import AES + +class AESCipher(object): + + def __init__(self, key): + self.bs = 32 + self.key = hashlib.sha256(key.encode()).digest() + + def encrypt(self, raw): + raw = self._pad(raw) + iv = Random.new().read(AES.block_size) + cipher = AES.new(self.key, AES.MODE_CBC, iv) + return base64.b64encode(iv + cipher.encrypt(raw)) + + def decrypt(self, enc): + enc = base64.b64decode(enc) + iv = enc[:AES.block_size] + cipher = AES.new(self.key, AES.MODE_CBC, iv) + return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8') + + def _pad(self, s): + return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs) + + @staticmethod + def _unpad(s): + return s[:-ord(s[len(s)-1:])] \ No newline at end of file diff --git a/app.py b/app.py index 830bdc2..39ccd51 100644 --- a/app.py +++ b/app.py @@ -2,6 +2,7 @@ import db import config +from aes import AESCipher app = Flask(__name__) app.secret_key = config.flask_secret_key @@ -58,8 +59,9 @@ def query_add(): @app.route("/query//", methods=["GET", "DELETE"]) def query_view(id): + databases = db.get_databases() query = db.get_query_details(id) - return render_template("view_query.html", query=query) + return render_template("view_query.html", databases=databases, query=query) @app.route("/query//json/", methods=["GET"]) def query_json_view(id): @@ -116,26 +118,74 @@ def query_run(id, database_id): if config.enable_run_query: database = db.get_database_details(database_id) query = db.get_query_details(id) - query_results = 'wut wut' + query_results = None + query_results_cols = [] + error = None + + # try and import the DB engine + try: + dbapi2 = __import__(config.database_engine) + except ImportError as e: + app.logger.error("Fatal error", exc_info=True) + flash("Fatal error. Contact Administrator", "error") + return redirect(url_for("index")) + finally: + # try and make the connection and run the query + try: + if database.get("password"): + crypt = AESCipher(config.flask_secret_key) + password = crypt.decrypt(database.get("password")) + else: + password = None + + connect = dbapi2.connect(database=database.get("name"), + host=database.get("hostname"), + port=database.get("port"), + user=database.get("user"), + password=password) + curse = connect.cursor() + curse.execute(query["sql"]) + query_results = curse.fetchall() + # Assemble column names so the table makes sense + for col in curse.description: + query_results_cols.append(col.name) + + except dbapi2.ProgrammingError, e: + # TODO: Exceptions don't seem to be standard in DB-API2, + # so this will likely have to be checked against other + # engines. The following works with psycopg2. + if hasattr(e, "pgerror"): + error = e.pgerror + else: + error = "There was an error with your query." + except dbapi2.Error as e: + if hasattr(e, "pgerror"): + error = e.pgerror + else: + error = e.msg + else: database = None query = None query_results = None - return render_template("run_query.html", query=query, database=database, query_results=query_results) + error = None + return render_template("run_query.html", query=query, database=database, query_results=query_results, query_results_cols=query_results_cols, error=error) @app.route("/database/", methods=["POST"]) def database_add(): try: name = request.form.get("name").strip() hostname = request.form.get("hostname").strip() - port = request.form.get("port").strip() - desc = request.form.get("desc").strip() + port = request.form.get("port", "").strip() + user = request.form.get("user", "").strip() + password = request.form.get("password", "").strip() + desc = request.form.get("desc", "").strip() if not name or not hostname: flash("Name and Host/File Name are required fields", "error") return redirect(url_for("index")) - db.insert_database(name, hostname, port, desc) + db.insert_database(name, hostname, port, user, password, desc) flash("Database Added!", "success") return redirect(url_for("index")) @@ -148,20 +198,26 @@ def database_add(): def database_edit(id): if request.method == "GET": database = db.get_database_details(id) + if database['password']: + database['password'] = 'placeholder' return render_template("edit_database.html", database=database) elif request.method == "POST": try: id = request.form["id"].strip() - name = request.form.get("name", "").strip() - hostname = request.form.get("hostname", "").strip() + name = request.form.get("name").strip() + hostname = request.form.get("hostname").strip() port = request.form.get("port", "").strip() + user = request.form.get("user", "").strip() + password = request.form.get("password", "").strip() + if password == 'placeholder': + password = None desc = request.form.get("desc", "").strip() if not name or not hostname: flash("Name and Host/File Name are required fields", "error") return redirect(url_for("database_edit", id=id)) - db.update_database(id, name, hostname, port, desc) + db.update_database(id, name, hostname, port, user, password, desc) flash("Database Modified!", "success") return redirect(url_for("database_view", id=id)) diff --git a/db.py b/db.py index 08a11be..8927c3c 100644 --- a/db.py +++ b/db.py @@ -2,6 +2,7 @@ from bson.objectid import ObjectId import config +from aes import AESCipher client = m.MongoClient(config.mongo_hostname, config.mongo_port) db = client[config.mongo_db] @@ -63,24 +64,43 @@ def get_queries(): def get_query_details(id): return queries.find_one(ObjectId(id)) -def insert_database(name, hostname, port, desc): +def insert_database(name, hostname, port=None, user=None, password=None, desc=None): + if password: + crypt = AESCipher(config.flask_secret_key) + encrypted_password = crypt.encrypt(password) + else: + encrypted_password = None + databases.insert({ "name": name, "hostname": hostname, "port": port, + "user": user, + "password": encrypted_password, "desc": desc }) -def update_database(id, name, hostname, port, desc): +def update_database(id, name, hostname, port=None, user=None, password=None, desc=None): + + db_set = { + "name": name, + "hostname": hostname, + "port": port, + "user": user, + "desc": desc + } + + if password: + crypt = AESCipher(config.flask_secret_key) + encrypted_password = crypt.encrypt(password) + db_set["password"] = encrypted_password + else: + encrypted_password = None + databases.update({ "_id": ObjectId(id) }, - { "$set" : { - "name": name, - "hostname": hostname, - "port": port, - "desc": desc - }}, upsert=False) + { "$set" : db_set}, upsert=False) def delete_database(id): databases.remove({"_id": ObjectId(id)}) diff --git a/templates/edit_database.html b/templates/edit_database.html index ebf2ae6..6294dd3 100644 --- a/templates/edit_database.html +++ b/templates/edit_database.html @@ -34,6 +34,20 @@

Edit Database


+
+
+ +
+
+
+ +
+
+ +
+
+
+
diff --git a/templates/index.html b/templates/index.html index e3c5b0d..717dcea 100644 --- a/templates/index.html +++ b/templates/index.html @@ -104,7 +104,7 @@ View
- + {% include "modal_database.html" %} {% endblock %} From 1bdacea7e6461b771f3960fc1697119adda1d753 Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Fri, 3 Jul 2015 21:08:09 -0600 Subject: [PATCH 06/13] Updated config file for new configuration for databases and running queries. --- config.py.sample | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config.py.sample b/config.py.sample index d4d0c97..ea12f49 100644 --- a/config.py.sample +++ b/config.py.sample @@ -2,6 +2,7 @@ flask_debug=True flask_bind_address="0.0.0.0" flask_port=5000 +# For security purposes, this field should be sufficiently randomized flask_secret_key="" # Mongo properties @@ -12,3 +13,10 @@ mongo_collection="queries" mongo_collection_database="databases" mongo_username="" mongo_password="" + +# Whether or not running queries is allowed +enable_run_query = False + +# The database engine should be DB-API2 compatible module that can be +# imported. More info: https://wiki.python.org/moin/DbApiFaq +database_engine = 'psycopg2' \ No newline at end of file From 36c922e0ca17d4cdf2e880461906f8f1645550a3 Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Tue, 28 Mar 2017 15:15:56 -0600 Subject: [PATCH 07/13] Perhaps the module name changed since previous versions. --- aes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aes.py b/aes.py index 70a33e4..341c522 100644 --- a/aes.py +++ b/aes.py @@ -1,7 +1,7 @@ import base64 import hashlib -from Crypto import Random -from Crypto.Cipher import AES +from crypto import Random +from crypto.Cipher import AES class AESCipher(object): @@ -26,4 +26,4 @@ def _pad(self, s): @staticmethod def _unpad(s): - return s[:-ord(s[len(s)-1:])] \ No newline at end of file + return s[:-ord(s[len(s)-1:])] From 78b8d470847aae3933841208809e7e21466baaa3 Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Tue, 28 Mar 2017 15:19:03 -0600 Subject: [PATCH 08/13] pycrypto was missing from requirements and previous fix was wrong. --- aes.py | 4 ++-- requirements.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/aes.py b/aes.py index 341c522..627c3bf 100644 --- a/aes.py +++ b/aes.py @@ -1,7 +1,7 @@ import base64 import hashlib -from crypto import Random -from crypto.Cipher import AES +from Crypto import Random +from Crypto.Cipher import AES class AESCipher(object): diff --git a/requirements.txt b/requirements.txt index 453306b..3ee61b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ itsdangerous==0.24 nose==1.3.6 pymongo==2.8 wsgiref==0.1.2 +pycrypto>=2.6.1 From 627c0419937aa6dff01cad670a70d30337a1064f Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Tue, 28 Mar 2017 16:09:27 -0600 Subject: [PATCH 09/13] Possible merge error fixed. --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 81152c6..45fdedd 100644 --- a/app.py +++ b/app.py @@ -20,7 +20,7 @@ def index(): databases = db.get_databases() - return render_template("index.html", queries=queries, issearchword=searchword) + return render_template("index.html", databases=databases, queries=queries, issearchword=searchword) @app.route("/queries.json/") def query_list(): From b43c39e3fd07f1a87e7b66733dff93c1c179665f Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Tue, 28 Mar 2017 16:17:06 -0600 Subject: [PATCH 10/13] Looks like database delete wasn't fully implemented. Or possibly a regression from a merge, not sure. Fixed. --- app.py | 2 +- templates/delete_database.html | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 templates/delete_database.html diff --git a/app.py b/app.py index 45fdedd..b23b3aa 100644 --- a/app.py +++ b/app.py @@ -236,7 +236,7 @@ def database_view(id): def database_delete(id): if request.method == "GET": database = db.get_database_details(id) - return render_template("delete_database.html", query=query) + return render_template("delete_database.html", database=database) elif request.method == "POST": db.delete_database(id) flash("Delete Successful", "success") diff --git a/templates/delete_database.html b/templates/delete_database.html new file mode 100644 index 0000000..0b47ee1 --- /dev/null +++ b/templates/delete_database.html @@ -0,0 +1,50 @@ +{% extends "base.html" %} + +{% block pagescripts %} + + + + + + + +{% endblock %} + +{% block container %} + +
+
+
+
+

Are you sure to delete this Database?

+
+
+
+ +
+
+

{{ database.name }}

+
+
+ +
+ +
+
+ +
+
+
+
+
+
+
+ + +{% endblock %} From c308c2b419864c942936a76a9ab8f1ac959e22a0 Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Tue, 28 Mar 2017 17:42:14 -0600 Subject: [PATCH 11/13] Fixed an undefined error --- app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app.py b/app.py index b23b3aa..9870ac0 100644 --- a/app.py +++ b/app.py @@ -169,6 +169,7 @@ def query_run(id, database_id): database = None query = None query_results = None + query_results_cols = None error = None return render_template("run_query.html", query=query, database=database, query_results=query_results, query_results_cols=query_results_cols, error=error) From bfd5bba97dde718cca5d085b95db0248cccc12cd Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Tue, 28 Mar 2017 17:48:20 -0600 Subject: [PATCH 12/13] Better handling of display if query running has been disabled. --- app.py | 2 +- templates/run_query.html | 82 ++++++++++++++++++++++------------------ 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/app.py b/app.py index 9870ac0..f1fdf82 100644 --- a/app.py +++ b/app.py @@ -171,7 +171,7 @@ def query_run(id, database_id): query_results = None query_results_cols = None error = None - return render_template("run_query.html", query=query, database=database, query_results=query_results, query_results_cols=query_results_cols, error=error) + return render_template("run_query.html", run_enabled=config.enable_run_query, query=query, database=database, query_results=query_results, query_results_cols=query_results_cols, error=error) @app.route("/database/", methods=["POST"]) def database_add(): diff --git a/templates/run_query.html b/templates/run_query.html index 0be212d..5b96667 100644 --- a/templates/run_query.html +++ b/templates/run_query.html @@ -11,64 +11,72 @@ {% block container %} Back - Edit + {% if run_enabled %} Edit{% endif %}
-

Results of {{ query.title }}

+

{% if run_enabled %}Results of {{ query.title }}{% else %}Disabled{% endif %}


- {% if database %} + {% if run_enabled %} -

Query

+ {% if database %} -
-
-
{{ query.sql }}
+

Query

+ +
+
+
{{ query.sql }}
+
-
-

Results

+

Results

- {% if error %} + {% if error %} -
-
-
{{ error }}
+
+
+
{{ error }}
+
-
- {% endif %} + {% endif %} -
-
- {% if query_results %} - - - {% for col in query_results_cols %} - - {% endfor %} - - {% for row in query_results %} - - {% for col in row %} - +
+
+ {% if query_results %} +
{{ col }}
{{ col }}
+ + {% for col in query_results_cols %} + + {% endfor %} + + {% for row in query_results %} + + {% for col in row %} + + {% endfor %} + {% endfor %} - - {% endfor %} -
{{ col }}
{{ col }}
- {% else %} -
No results returned
- {% endif %} + + {% else %} +
No results returned
+ {% endif %} +
+ {% else %} +
+
{% if not query %}Query and {% endif %}Database not selected.
+ {% endif %} {% else %} -
-
{% if not query %}Query and {% endif %}Database not selected.
-
+
+
The ability to run queries has not been enabled.
+
{% endif %} + {% endblock %} \ No newline at end of file From 8ba93d7a9c6d8750f435a9067b695965b1f08ba9 Mon Sep 17 00:00:00 2001 From: Mike Shultz Date: Tue, 28 Mar 2017 21:11:30 -0600 Subject: [PATCH 13/13] Improved SQL execution error handling. --- app.py | 74 +++++++++++++++++++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/app.py b/app.py index f1fdf82..1bbc05e 100644 --- a/app.py +++ b/app.py @@ -127,43 +127,47 @@ def query_run(id, database_id): try: dbapi2 = __import__(config.database_engine) except ImportError as e: - app.logger.error("Fatal error", exc_info=True) + app.logger.error("Fatal error. Could not import DB engine.", exc_info=True) flash("Fatal error. Contact Administrator", "error") return redirect(url_for("index")) - finally: - # try and make the connection and run the query - try: - if database.get("password"): - crypt = AESCipher(config.flask_secret_key) - password = crypt.decrypt(database.get("password")) - else: - password = None - - connect = dbapi2.connect(database=database.get("name"), - host=database.get("hostname"), - port=database.get("port"), - user=database.get("user"), - password=password) - curse = connect.cursor() - curse.execute(query["sql"]) - query_results = curse.fetchall() - # Assemble column names so the table makes sense - for col in curse.description: - query_results_cols.append(col.name) - - except dbapi2.ProgrammingError, e: - # TODO: Exceptions don't seem to be standard in DB-API2, - # so this will likely have to be checked against other - # engines. The following works with psycopg2. - if hasattr(e, "pgerror"): - error = e.pgerror - else: - error = "There was an error with your query." - except dbapi2.Error as e: - if hasattr(e, "pgerror"): - error = e.pgerror - else: - error = e.msg + + # try and make the connection and run the query + try: + if database.get("password"): + crypt = AESCipher(config.flask_secret_key) + password = crypt.decrypt(database.get("password")) + else: + password = None + + connect = dbapi2.connect(database=database.get("name"), + host=database.get("hostname"), + port=database.get("port"), + user=database.get("user"), + password=password) + + curse = connect.cursor() + curse.execute(query["sql"]) + query_results = curse.fetchall() + + # Assemble column names so the table makes sense + for col in curse.description: + query_results_cols.append(col.name) + + except dbapi2.ProgrammingError, e: + # TODO: Exceptions don't seem to be standard in DB-API2, + # so this will likely have to be checked against other + # engines. The following works with psycopg2. + if hasattr(e, "pgerror"): + error = e.pgerror + else: + error = "There was an error with your query." + except dbapi2.Error as e: + if hasattr(e, "pgerror"): + error = e.pgerror or e.message + app.logger.error(error) + else: + error = e.msg + app.logger.error(error) else: database = None