Add authentication and the basis of the browser module.

A user authentication module based on flask-security is added, which
allows users to login and change/recover passwords etc. Custom templates
are included for the user/password UIs.

A new setup script will initialise the user (and later settings) DB,
adding the first user and granting them an Administrator role.

A redirects blueprint module is added to handle simple URL redirects.

A browser module is added and currently renders a skeleton page with
a menu bar, gravatar and jumbotron.

NOTE FOR LATER: Currently this code might make the nice basis for any
web app that needs user management and plugins. Hmmm....
This commit is contained in:
Dave Page 2015-01-22 15:56:23 +00:00
parent 7fa40d7671
commit 10515431c7
40 changed files with 408 additions and 7 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

View File

@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block body %}
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">pgAdmin 4</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">File <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="{{ url_for('security.change_password') }}">Change Password</a></li>
<li><a href="{{ url_for('utils.test') }}">Test URL</a></li>
<li><a href="{{ url_for('utils.ping') }}">Ping</a></li>
<li class="divider"></li>
<li><a href="{{ url_for('security.logout') }}">Logout</a></li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="#"><img src="{{ username | gravatar }}" width="18" height="18"> {{ username }}</a></li>
</li>
</ul>
</div>
</div>
</nav>
<div class="jumbotron">
<div class="container">
<h1>pgAdmin 4</h1>
<p>Welcome to pgAdmin 4.</p>
<p><a class="btn btn-primary btn-lg" href="http://www.pgadmin.org/" role="button">Learn more &raquo;</a></p>
</div>
</div>
{% include 'messages.html' %}
{% endblock %}

View File

@ -0,0 +1,12 @@
{%- with messages = get_flashed_messages(with_categories=true) -%}
{% if messages %}
<div style="position: fixed; top: 70px; right: 20px; width: 400px; z-index: 9999">
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{%- endwith %}

View File

@ -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)

View File

View File

@ -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'))

0
web/pgadmin/static/css/bootstrap-theme.css vendored Executable file → Normal file
View File

0
web/pgadmin/static/css/bootstrap-theme.css.map Executable file → Normal file
View File

0
web/pgadmin/static/css/bootstrap-theme.min.css vendored Executable file → Normal file
View File

0
web/pgadmin/static/css/bootstrap.css vendored Executable file → Normal file
View File

0
web/pgadmin/static/css/bootstrap.css.map Executable file → Normal file
View File

0
web/pgadmin/static/css/bootstrap.min.css vendored Executable file → Normal file
View File

0
web/pgadmin/static/css/main.css Executable file → Normal file
View File

View File

@ -1,2 +1 @@
/* Overrides/fixes for pgAdmin specific styling in Bootstrap */

View File

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

0
web/pgadmin/static/js/main.js Executable file → Normal file
View File

0
web/pgadmin/static/js/vendor/bootstrap.js vendored Executable file → Normal file
View File

0
web/pgadmin/static/js/vendor/bootstrap.min.js vendored Executable file → Normal file
View File

0
web/pgadmin/static/js/vendor/jquery-1.11.1.min.js vendored Executable file → Normal file
View File

0
web/pgadmin/static/js/vendor/modernizr-2.6.2-respond-1.1.0.min.js vendored Executable file → Normal file
View File

0
web/pgadmin/static/js/vendor/npm.js vendored Executable file → Normal file
View File

View File

@ -0,0 +1,13 @@
{% extends "security/panel.html" %}
{% block panel_title %}pgAdmin Password Change{% endblock %}
{% block panel_body %}
<form action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form">
{{ change_password_form.hidden_tag() }}
<fieldset>
{{ 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") }}
<input class="btn btn-lg btn-success btn-block" type="submit" value="Change Password">
</fieldset>
</form>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% macro render_field_with_errors(field, type) %}
<div class="form-group{% if field.errors %} has-error{% endif %}">
<input class="form-control" placeholder="{{ field.label.text }}" name="{{ field.name }}" type="{{ type }}">
</div>
{% if field.errors %}
{% for error in field.errors %}
<span class="help-block">{{ error }}</span>
{% endfor %}
{% endif %}
{% endmacro %}

View File

@ -0,0 +1,12 @@
{% extends "security/panel.html" %}
{% block panel_title %}Recover pgAdmin Password{% endblock %}
{% block panel_body %}
<p>Enter the email address for the user account you wish to recover the password for:</p>
<form action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form">
{{ forgot_password_form.hidden_tag() }}
<fieldset>
{{ render_field_with_errors(forgot_password_form.email, "text") }}
<input class="btn btn-lg btn-success btn-block" type="submit" value="Recover Password">
</fieldset>
</form>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "security/panel.html" %}
{% block panel_title %}pgAdmin Login{% endblock %}
{% block panel_body %}
<form action="{{ url_for_security('login') }}" method="POST" name="login_user_form">
{{ login_user_form.hidden_tag() }}
<fieldset>
{{ render_field_with_errors(login_user_form.email, "text") }}
{{ render_field_with_errors(login_user_form.password, "password") }}
<input class="btn btn-lg btn-success btn-block" type="submit" value="Login">
</fieldset>
</form>
<span class="help-block">Forgotten your <a href="{{ url_for('security.forgot_password') }}">password</a>?</span>
{% endblock %}

View File

@ -0,0 +1,12 @@
{%- with messages = get_flashed_messages(with_categories=true) -%}
{% if messages %}
<div style="position: fixed; top: 20px; right: 20px; width: 400px; z-index: 9999">
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{%- endwith %}

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% from "security/fields.html" import render_field_with_errors %}
{% block body %}
<div class="container">
{% include "security/messages.html" %}
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% block panel_title %}{% endblock %}</h3>
</div>
<div class="panel-body">
{% block panel_body %}
{% endblock %}
</div>
</div>
</div>
</div>
</div>
{% include 'security/watermark.html' %}
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "security/panel.html" %}
{% block panel_title %}pgAdmin Password Reset{% endblock %}
{% block panel_body %}
<form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST" name="reset_password_form">
{{ reset_password_form.hidden_tag() }}
<fieldset>
{{ render_field_with_errors(reset_password_form.password, "password") }}
{{ render_field_with_errors(reset_password_form.password_confirm, "password") }}
<input class="btn btn-lg btn-success btn-block" type="submit" value="Reset Password">
</fieldset>
</form>
{% endblock %}

View File

@ -0,0 +1,5 @@
{% block watermark %}
<div style="position: fixed; bottom: 0; right: 0;">
<img src="img/logo-right-256.png" alt="pgAdmin 4">
</div>
{% endblock %}

View File

@ -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 = """

37
web/settings_model.py Normal file
View File

@ -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'))

74
web/setup.py Normal file
View File

@ -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