diff --git a/requirements.txt b/requirements.txt index 321e289fb..ab61c7037 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,18 @@ Flask==0.10.1 +Flask-Gravatar==0.4.1 +Flask-Login==0.2.11 +Flask-Mail==0.9.1 +Flask-Principal==0.4.0 +Flask-SQLAlchemy==2.0 +Flask-Security==1.7.4 +Flask-WTF==0.11 Jinja2==2.7.3 MarkupSafe==0.23 +SQLAlchemy==0.9.8 +WTForms==2.0.2 Werkzeug==0.9.6 +blinker==1.3 itsdangerous==0.24 +passlib==1.6.2 psycopg2==2.5.2 wsgiref==0.1.2 diff --git a/web/config.py b/web/config.py index 2535d3396..4afea8460 100644 --- a/web/config.py +++ b/web/config.py @@ -10,6 +10,7 @@ ########################################################################## from logging import * +import os ########################################################################## # Application settings @@ -73,11 +74,48 @@ CSRF_ENABLED = True # Secret key for signing CSRF data. Override this in config_local.py if # running on a web server -CSRF_SESSION_KEY = 'SuperSecret' +CSRF_SESSION_KEY = 'SuperSecret1' # Secret key for signing cookies. Override this in config_local.py if # running on a web server -SECRET_KEY = 'SuperSecret' +SECRET_KEY = 'SuperSecret2' + +# Salt used when hashing passwords. Override this in config_local.py if +# running on a web server +SECURITY_PASSWORD_SALT = 'SuperSecret3' + +# Hashing algorithm used for password storage +SECURITY_PASSWORD_HASH = 'pbkdf2_sha512' + +########################################################################## +# User account and settings storage +########################################################################## + +# The default path to the SQLite database used to store user accounts and +# settings. This default places the file in the same directory as this +# config file, but generates an absolute path for use througout the app. +SQLITE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'pgadmin4.db') + +########################################################################## +# Mail server settings +########################################################################## + +# These settings are used when running in web server mode for confirming +# and resetting passwords etc. +MAIL_SERVER = 'smtp.gmail.com' +MAIL_PORT = 465 +MAIL_USE_SSL = True +MAIL_USERNAME = 'username' +MAIL_PASSWORD = 'SuperSecret' + +########################################################################## +# Mail content settings +########################################################################## + +# These settings define the content of password reset emails +SECURITY_EMAIL_SUBJECT_PASSWORD_RESET = "Password reset instructions for %s" % APP_NAME +SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE = "Your %s password has been reset" % APP_NAME +SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE = "Your password for %s has been changed" % APP_NAME ########################################################################## # Local config settings diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index 67df40f91..13cc2fca8 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -10,8 +10,13 @@ """The main pgAdmin module. This handles the application initialisation tasks, such as setup of logging, dynamic loading of modules etc.""" -import inspect, logging, os from flask import Flask +from flask.ext.sqlalchemy import SQLAlchemy +from flask.ext.security import Security, SQLAlchemyUserDatastore, login_required +from flask_mail import Mail +from settings_model import db, Role, User + +import inspect, logging, os # Configuration settings import config @@ -56,7 +61,26 @@ def create_app(app_name=config.APP_NAME): app.logger.info('Starting %s v%s...', config.APP_NAME, config.APP_VERSION) app.logger.info('################################################################################') - # Register all the modules + ########################################################################## + # Setup authentication + ########################################################################## + + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + config.SQLITE_PATH.replace('\\', '/') + app.config['SECURITY_RECOVERABLE'] = True + app.config['SECURITY_CHANGEABLE'] = True + + # Create database connection object and mailer + db.init_app(app) + mail = Mail(app) + + # Setup Flask-Security + user_datastore = SQLAlchemyUserDatastore(db, User, Role) + security = Security(app, user_datastore) + + ########################################################################## + # Load plugin modules + ########################################################################## + path = os.path.dirname(os.path.realpath(__file__)) files = os.listdir(path) for f in files: @@ -76,7 +100,6 @@ def create_app(app_name=config.APP_NAME): app.logger.info('Registering blueprint module: %s' % f) app.register_blueprint(module.views.blueprint) + # All done! app.logger.debug('URL map: %s' % app.url_map) - return app - diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/web/pgadmin/browser/templates/index.html b/web/pgadmin/browser/templates/index.html new file mode 100644 index 000000000..b01030a99 --- /dev/null +++ b/web/pgadmin/browser/templates/index.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} +{% block body %} + + +
+
+

pgAdmin 4

+

Welcome to pgAdmin 4.

+

Learn more »

+
+
+ +{% include 'messages.html' %} +{% endblock %} diff --git a/web/pgadmin/browser/templates/messages.html b/web/pgadmin/browser/templates/messages.html new file mode 100644 index 000000000..e0c886cd6 --- /dev/null +++ b/web/pgadmin/browser/templates/messages.html @@ -0,0 +1,12 @@ +{%- with messages = get_flashed_messages(with_categories=true) -%} + {% if messages %} +
+ {% for category, message in messages %} + + {% endfor %} +
+ {% endif %} +{%- endwith %} diff --git a/web/pgadmin/browser/views.py b/web/pgadmin/browser/views.py new file mode 100644 index 000000000..ec958eae3 --- /dev/null +++ b/web/pgadmin/browser/views.py @@ -0,0 +1,37 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2014, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""A blueprint module implementing the core pgAdmin browser.""" +MODULE_NAME = 'browser' + +import config +from flask import Blueprint, current_app, render_template +from flaskext.gravatar import Gravatar +from flask.ext.security import login_required +from flask.ext.login import current_user + +# Initialise the module +blueprint = Blueprint(MODULE_NAME, __name__, static_folder='static', static_url_path='', template_folder='templates', url_prefix='/' + MODULE_NAME) + +########################################################################## +# A test page +########################################################################## +@blueprint.route("/") +@login_required +def index(): + """Render and process the main browser window.""" + gravatar = Gravatar(current_app, + size=100, + rating='g', + default='retro', + force_default=False, + use_ssl=False, + base_url=None) + + return render_template('index.html', username=current_user.email) diff --git a/web/pgadmin/redirects/__init__.py b/web/pgadmin/redirects/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/web/pgadmin/redirects/views.py b/web/pgadmin/redirects/views.py new file mode 100644 index 000000000..116d8cddf --- /dev/null +++ b/web/pgadmin/redirects/views.py @@ -0,0 +1,24 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2014, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""A blueprint module providing URL redirects.""" +MODULE_NAME = 'redirects' + +import config +from flask import Blueprint, redirect, url_for +from flask.ext.security import login_required + +# Initialise the module +blueprint = Blueprint(MODULE_NAME, __name__) + +@blueprint.route('/') +@login_required +def index(): + """Redirect users hitting the root to the browser""" + return redirect(url_for('browser.index')) diff --git a/web/pgadmin/static/css/bootstrap-theme.css b/web/pgadmin/static/css/bootstrap-theme.css old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/css/bootstrap-theme.css.map b/web/pgadmin/static/css/bootstrap-theme.css.map old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/css/bootstrap-theme.min.css b/web/pgadmin/static/css/bootstrap-theme.min.css old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/css/bootstrap.css b/web/pgadmin/static/css/bootstrap.css old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/css/bootstrap.css.map b/web/pgadmin/static/css/bootstrap.css.map old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/css/bootstrap.min.css b/web/pgadmin/static/css/bootstrap.min.css old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/css/main.css b/web/pgadmin/static/css/main.css old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/css/overrides.css b/web/pgadmin/static/css/overrides.css index 7bb7a6cf8..f73c3750d 100644 --- a/web/pgadmin/static/css/overrides.css +++ b/web/pgadmin/static/css/overrides.css @@ -1,2 +1 @@ /* Overrides/fixes for pgAdmin specific styling in Bootstrap */ - diff --git a/web/pgadmin/static/fonts/glyphicons-halflings-regular.eot b/web/pgadmin/static/fonts/glyphicons-halflings-regular.eot old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/fonts/glyphicons-halflings-regular.svg b/web/pgadmin/static/fonts/glyphicons-halflings-regular.svg old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/fonts/glyphicons-halflings-regular.ttf b/web/pgadmin/static/fonts/glyphicons-halflings-regular.ttf old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/fonts/glyphicons-halflings-regular.woff b/web/pgadmin/static/fonts/glyphicons-halflings-regular.woff old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/img/logo-256.png b/web/pgadmin/static/img/logo-256.png new file mode 100644 index 000000000..186f17d4f Binary files /dev/null and b/web/pgadmin/static/img/logo-256.png differ diff --git a/web/pgadmin/static/img/logo-right-256.png b/web/pgadmin/static/img/logo-right-256.png new file mode 100644 index 000000000..f9b9a5a37 Binary files /dev/null and b/web/pgadmin/static/img/logo-right-256.png differ diff --git a/web/pgadmin/static/js/main.js b/web/pgadmin/static/js/main.js old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/js/vendor/bootstrap.js b/web/pgadmin/static/js/vendor/bootstrap.js old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/js/vendor/bootstrap.min.js b/web/pgadmin/static/js/vendor/bootstrap.min.js old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/js/vendor/jquery-1.11.1.min.js b/web/pgadmin/static/js/vendor/jquery-1.11.1.min.js old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/js/vendor/modernizr-2.6.2-respond-1.1.0.min.js b/web/pgadmin/static/js/vendor/modernizr-2.6.2-respond-1.1.0.min.js old mode 100755 new mode 100644 diff --git a/web/pgadmin/static/js/vendor/npm.js b/web/pgadmin/static/js/vendor/npm.js old mode 100755 new mode 100644 diff --git a/web/pgadmin/templates/security/change_password.html b/web/pgadmin/templates/security/change_password.html new file mode 100644 index 000000000..60440c38c --- /dev/null +++ b/web/pgadmin/templates/security/change_password.html @@ -0,0 +1,13 @@ +{% extends "security/panel.html" %} +{% block panel_title %}pgAdmin Password Change{% endblock %} +{% block panel_body %} +
+ {{ change_password_form.hidden_tag() }} +
+ {{ render_field_with_errors(change_password_form.password, "password") }} + {{ render_field_with_errors(change_password_form.new_password, "password") }} + {{ render_field_with_errors(change_password_form.new_password_confirm, "password") }} + +
+
+{% endblock %} diff --git a/web/pgadmin/templates/security/fields.html b/web/pgadmin/templates/security/fields.html new file mode 100644 index 000000000..44bbce193 --- /dev/null +++ b/web/pgadmin/templates/security/fields.html @@ -0,0 +1,10 @@ +{% macro render_field_with_errors(field, type) %} +
+ +
+{% if field.errors %} +{% for error in field.errors %} +{{ error }} +{% endfor %} +{% endif %} +{% endmacro %} diff --git a/web/pgadmin/templates/security/forgot_password.html b/web/pgadmin/templates/security/forgot_password.html new file mode 100644 index 000000000..20ef2079f --- /dev/null +++ b/web/pgadmin/templates/security/forgot_password.html @@ -0,0 +1,12 @@ +{% extends "security/panel.html" %} +{% block panel_title %}Recover pgAdmin Password{% endblock %} +{% block panel_body %} +

Enter the email address for the user account you wish to recover the password for:

+
+ {{ forgot_password_form.hidden_tag() }} +
+ {{ render_field_with_errors(forgot_password_form.email, "text") }} + +
+
+{% endblock %} diff --git a/web/pgadmin/templates/security/login_user.html b/web/pgadmin/templates/security/login_user.html new file mode 100644 index 000000000..cebefa360 --- /dev/null +++ b/web/pgadmin/templates/security/login_user.html @@ -0,0 +1,13 @@ +{% extends "security/panel.html" %} +{% block panel_title %}pgAdmin Login{% endblock %} +{% block panel_body %} +
+ {{ login_user_form.hidden_tag() }} +
+ {{ render_field_with_errors(login_user_form.email, "text") }} + {{ render_field_with_errors(login_user_form.password, "password") }} + +
+
+Forgotten your password? +{% endblock %} diff --git a/web/pgadmin/templates/security/messages.html b/web/pgadmin/templates/security/messages.html new file mode 100644 index 000000000..d6b758da0 --- /dev/null +++ b/web/pgadmin/templates/security/messages.html @@ -0,0 +1,12 @@ +{%- with messages = get_flashed_messages(with_categories=true) -%} + {% if messages %} +
+ {% for category, message in messages %} + + {% endfor %} +
+ {% endif %} +{%- endwith %} diff --git a/web/pgadmin/templates/security/panel.html b/web/pgadmin/templates/security/panel.html new file mode 100644 index 000000000..b511c5508 --- /dev/null +++ b/web/pgadmin/templates/security/panel.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% from "security/fields.html" import render_field_with_errors %} +{% block body %} +
+ {% include "security/messages.html" %} +
+
+
+
+

{% block panel_title %}{% endblock %}

+
+
+ {% block panel_body %} + {% endblock %} +
+
+
+
+
+{% include 'security/watermark.html' %} +{% endblock %} diff --git a/web/pgadmin/templates/security/reset_password.html b/web/pgadmin/templates/security/reset_password.html new file mode 100644 index 000000000..e04085e36 --- /dev/null +++ b/web/pgadmin/templates/security/reset_password.html @@ -0,0 +1,12 @@ +{% extends "security/panel.html" %} +{% block panel_title %}pgAdmin Password Reset{% endblock %} +{% block panel_body %} +
+ {{ reset_password_form.hidden_tag() }} +
+ {{ render_field_with_errors(reset_password_form.password, "password") }} + {{ render_field_with_errors(reset_password_form.password_confirm, "password") }} + +
+
+{% endblock %} diff --git a/web/pgadmin/templates/security/watermark.html b/web/pgadmin/templates/security/watermark.html new file mode 100644 index 000000000..1496d31c3 --- /dev/null +++ b/web/pgadmin/templates/security/watermark.html @@ -0,0 +1,5 @@ +{% block watermark %} +
+ pgAdmin 4 +
+{% endblock %} diff --git a/web/pgadmin/utils/views.py b/web/pgadmin/utils/views.py index 89f8a2e37..b464e9027 100644 --- a/web/pgadmin/utils/views.py +++ b/web/pgadmin/utils/views.py @@ -12,6 +12,7 @@ MODULE_NAME = 'utils' import config from flask import Blueprint, render_template +from flask.ext.security import login_required from time import time, ctime # Initialise the module @@ -21,6 +22,7 @@ blueprint = Blueprint(MODULE_NAME, __name__, static_folder='static', static_url # A test page ########################################################################## @blueprint.route("/test") +@login_required def test(): """Generate a simple test page to demonstrate that output can be rendered.""" output = """ diff --git a/web/settings_model.py b/web/settings_model.py new file mode 100644 index 000000000..6ff9c5e99 --- /dev/null +++ b/web/settings_model.py @@ -0,0 +1,37 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2014, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Defines the models for the configuration database.""" + +from flask.ext.sqlalchemy import SQLAlchemy +from flask.ext.security import UserMixin, RoleMixin + +db = SQLAlchemy() + +# Define models +roles_users = db.Table('roles_users', + db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), + db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))) + +class Role(db.Model, RoleMixin): + """Define a security role""" + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(80), unique=True) + description = db.Column(db.String(255)) + +class User(db.Model, UserMixin): + """Define a user object""" + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(255), unique=True) + password = db.Column(db.String(255)) + active = db.Column(db.Boolean()) + active = db.Column(db.Boolean()) + confirmed_at = db.Column(db.DateTime()) + roles = db.relationship('Role', secondary=roles_users, + backref=db.backref('users', lazy='dynamic')) diff --git a/web/setup.py b/web/setup.py new file mode 100644 index 000000000..2e41c791f --- /dev/null +++ b/web/setup.py @@ -0,0 +1,74 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2014, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Perform the initial setup of the application, by creating the auth +and settings database.""" + +from flask import Flask +from flask.ext.sqlalchemy import SQLAlchemy +from flask.ext.security import Security, SQLAlchemyUserDatastore +from flask.ext.security.utils import encrypt_password +from settings_model import db, Role, User + +import getpass, os, sys + +# Configuration settings +import config + +app = Flask(__name__) +app.config.from_object(config) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + config.SQLITE_PATH.replace('\\', '/') +db.init_app(app) + +print "pgAdmin 4 - Application Initialisation" +print "======================================\n" + +local_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config_local.py') +if not os.path.isfile(local_config): + print "%s does not exist.\n" % local_config + print "Before running this script, ensure that config_local.py has been created" + print "and sets values for SECRET_KEY, SECURITY_PASSWORD_SALT and CSRF_SESSION_KEY" + print "at bare minimum. See config.py for more information and a complete list of" + print "settings. Exiting..." + sys.exit(1) + +# Check if the database exists. If it does, tell the user and exit. +if os.path.isfile(config.SQLITE_PATH): + print "The configuration database %s already exists and will not be overwritten.\nExiting..." % config.SQLITE_PATH + sys.exit(1) + +# Prompt the user for their default username and password. +print "Enter the email address and password to use for the initial pgAdmin user account:\n" +email = '' +while email == '': + email = raw_input("Email address: ") + +pprompt = lambda: (getpass.getpass(), getpass.getpass('Retype password: ')) + +p1, p2 = pprompt() +while p1 != p2: + print('Passwords do not match. Try again') + p1, p2 = pprompt() + +# Setup Flask-Security +user_datastore = SQLAlchemyUserDatastore(db, User, Role) +security = Security(app, user_datastore) + +with app.app_context(): + password = encrypt_password(p1) + + db.create_all() + user_datastore.create_role(name='Administrators', description='pgAdmin Administrators Role') + user_datastore.create_user(email=email, password=password) + user_datastore.add_role_to_user(email, 'Administrators') + db.session.commit() + +# Done! +print "" +print "The configuration database has been created at %s" % config.SQLITE_PATH