Skip to content

Commit

Permalink
Adding a basic login system to Puppetboard
Browse files Browse the repository at this point in the history
  • Loading branch information
Rob Reus committed Apr 30, 2018
1 parent 5023d4c commit a1a71be
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 4 deletions.
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ ENV PUPPETBOARD_SETTINGS docker_settings.py
RUN mkdir -p /usr/src/app/
WORKDIR /usr/src/app/

VOLUME /var/lib/puppetboard

COPY requirements*.txt /usr/src/app/
RUN pip install -r requirements-docker.txt

Expand Down
79 changes: 77 additions & 2 deletions puppetboard/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,22 @@
from flask import (
Flask, render_template, abort, url_for,
Response, stream_with_context, redirect,
request, session, jsonify
request, session, jsonify, flash
)
from flask_login import (
LoginManager, login_required,
login_user, logout_user
)
from jinja2.utils import contextfunction

from pypuppetdb.QueryBuilder import *

from puppetboard.forms import QueryForm
from puppetboard.forms import QueryForm, LoginForm
from puppetboard.utils import (get_or_abort, yield_or_stop,
get_db_version)
from puppetboard.dailychart import get_daily_reports_chart
from puppetboard.models import db, Users
from sqlalchemy.exc import OperationalError

import werkzeug.exceptions as ex
import CommonMark
Expand Down Expand Up @@ -51,6 +57,19 @@
]

app = get_app()
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "login"
if not app.config['LOGIN_DISABLED']:
try:
users = Users.query.all()
except OperationalError:
db.create_all()
users = Users.query.all()
if len(users) < 1:
admin_user = Users(username='admin', password='admin123')
db.session.add(admin_user)
db.session.commit()
graph_facts = app.config['GRAPH_FACTS']
numeric_level = getattr(logging, app.config['LOGLEVEL'].upper(), None)

Expand Down Expand Up @@ -88,6 +107,7 @@ def now(format='%m/%d/%Y %H:%M:%S'):

@app.route('/', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/')
@login_required
def index(env):
"""This view generates the index page and displays a set of metrics and
latest reports on nodes fetched from PuppetDB.
Expand Down Expand Up @@ -200,6 +220,7 @@ def index(env):

@app.route('/nodes', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/nodes')
@login_required
def nodes(env):
"""Fetch all (active) nodes from PuppetDB and stream a table displaying
those nodes.
Expand Down Expand Up @@ -285,6 +306,7 @@ def inventory_facts():

@app.route('/inventory', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/inventory')
@login_required
def inventory(env):
"""Fetch all (active) nodes from PuppetDB and stream a table displaying
those nodes along with a set of facts about them.
Expand All @@ -306,6 +328,7 @@ def inventory(env):
@app.route('/inventory/json',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/inventory/json')
@login_required
def inventory_ajax(env):
"""Backend endpoint for inventory table"""
draw = int(request.args.get('draw', 0))
Expand Down Expand Up @@ -344,6 +367,7 @@ def inventory_ajax(env):
@app.route('/node/<node_name>',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/node/<node_name>')
@login_required
def node(env, node_name):
"""Display a dashboard for a node showing as much data as we have on that
node. This includes facts and reports but not Resources as that is too
Expand Down Expand Up @@ -378,6 +402,7 @@ def node(env, node_name):
@app.route('/reports/<node_name>',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/reports/<node_name>')
@login_required
def reports(env, node_name):
"""Query and Return JSON data to reports Jquery datatable
Expand All @@ -401,6 +426,7 @@ def reports(env, node_name):
@app.route('/reports/<node_name>/json',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/reports/<node_name>/json')
@login_required
def reports_ajax(env, node_name):
"""Query and Return JSON data to reports Jquery datatable
Expand Down Expand Up @@ -509,6 +535,7 @@ def reports_ajax(env, node_name):
@app.route('/report/<node_name>/<report_id>',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/report/<node_name>/<report_id>')
@login_required
def report(env, node_name, report_id):
"""Displays a single report including all the events associated with that
report and their status.
Expand Down Expand Up @@ -560,6 +587,7 @@ def report(env, node_name, report_id):

@app.route('/facts', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/facts')
@login_required
def facts(env):
"""Displays an alphabetical list of all facts currently known to
PuppetDB.
Expand Down Expand Up @@ -609,6 +637,7 @@ def facts(env):
@app.route('/fact/<fact>/<value>',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/fact/<fact>/<value>')
@login_required
def fact(env, fact, value):
"""Fetches the specific fact(/value) from PuppetDB and displays per
node for which this fact is known.
Expand Down Expand Up @@ -655,6 +684,7 @@ def fact(env, fact, value):
'fact': None, 'value': None})
@app.route('/<env>/node/<node>/facts/json',
defaults={'fact': None, 'value': None})
@login_required
def fact_ajax(env, node, fact, value):
"""Fetches the specific facts matching (node/fact/value) from PuppetDB and
return a JSON table
Expand Down Expand Up @@ -747,6 +777,7 @@ def fact_ajax(env, node, fact, value):
@app.route('/query', methods=('GET', 'POST'),
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/query', methods=('GET', 'POST'))
@login_required
def query(env):
"""Allows to execute raw, user created querries against PuppetDB. This is
currently highly experimental and explodes in interesting ways since none
Expand Down Expand Up @@ -792,6 +823,7 @@ def query(env):

@app.route('/metrics', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/metrics')
@login_required
def metrics(env):
"""Lists all available metrics that PuppetDB is aware of.
Expand All @@ -812,6 +844,7 @@ def metrics(env):
@app.route('/metric/<path:metric>',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/metric/<path:metric>')
@login_required
def metric(env, metric):
"""Lists all information about the metric of the given name.
Expand Down Expand Up @@ -839,6 +872,7 @@ def metric(env, metric):
@app.route('/catalogs/compare/<compare>',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/catalogs/compare/<compare>')
@login_required
def catalogs(env, compare):
"""Lists all nodes with a compiled catalog.
Expand Down Expand Up @@ -867,6 +901,7 @@ def catalogs(env, compare):
@app.route('/catalogs/compare/<compare>/json',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/catalogs/compare/<compare>/json')
@login_required
def catalogs_ajax(env, compare):
"""Server data to catalogs as JSON to Jquery datatables
"""
Expand Down Expand Up @@ -926,6 +961,7 @@ def catalogs_ajax(env, compare):
@app.route('/catalog/<node_name>',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/catalog/<node_name>')
@login_required
def catalog_node(env, node_name):
"""Fetches from PuppetDB the compiled catalog of a given node.
Expand All @@ -950,6 +986,7 @@ def catalog_node(env, node_name):
@app.route('/catalogs/compare/<compare>...<against>',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/catalogs/compare/<compare>...<against>')
@login_required
def catalog_compare(env, compare, against):
"""Compares the catalog of one node, parameter compare, with that of
with that of another node, parameter against.
Expand Down Expand Up @@ -978,6 +1015,7 @@ def catalog_compare(env, compare, against):

@app.route('/radiator', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/radiator')
@login_required
def radiator(env):
"""This view generates a simplified monitoring page
akin to the radiator view in puppet dashboard
Expand Down Expand Up @@ -1077,6 +1115,7 @@ def radiator(env):
@app.route('/daily_reports_chart.json',
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
@app.route('/<env>/daily_reports_chart.json')
@login_required
def daily_reports_chart(env):
"""Return JSON data to generate a bar chart of daily runs.
Expand Down Expand Up @@ -1104,3 +1143,39 @@ def offline_static(filename):

return Response(response=render_template('static/%s' % filename),
status=200, mimetype=mimetype)


@app.route("/login", methods=["GET", "POST"])
def login():
form = LoginForm(meta={
'csrf_secret': app.config['SECRET_KEY'],
'csrf_context': session})
if form.validate_on_submit():
user = Users.query.filter_by(username=form.username.data).first()
if user and user.password == form.password.data:
login_user(user, remember=form.remember.data)
return redirect(url_for('index'))
else:
flash('Login failed.', 'error')
return render_template('login.html', form=form)


@app.route("/users")
@login_required
def users():
users = Users.query.all()
return render_template('users.html', users=users)


@app.route("/logout")
@login_required
def logout():
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('login'))


@login_manager.user_loader
def load_user(user_id):
return Users.query.filter_by(id=int(user_id)).first()

3 changes: 3 additions & 0 deletions puppetboard/default_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
PUPPETDB_CERT = None
PUPPETDB_TIMEOUT = 20
DEFAULT_ENVIRONMENT = 'production'
LOGIN_DISABLED = True
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/puppetboard.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SECRET_KEY = os.urandom(24)
DEV_LISTEN_HOST = '127.0.0.1'
DEV_LISTEN_PORT = 5000
Expand Down
3 changes: 3 additions & 0 deletions puppetboard/docker_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
PUPPETDB_CERT = os.getenv('PUPPETDB_CERT', None)
PUPPETDB_PROTO = os.getenv('PUPPETDB_PROTO', None)
PUPPETDB_TIMEOUT = int(os.getenv('PUPPETDB_TIMEOUT', '20'))
LOGIN_DISABLED = os.getenv('LOGIN_DISABLED', True)
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI',
'sqlite:////var/lib/puppetboard/database.db')
DEFAULT_ENVIRONMENT = os.getenv('DEFAULT_ENVIRONMENT', 'production')
SECRET_KEY = os.getenv('SECRET_KEY', os.urandom(24))
DEV_LISTEN_HOST = os.getenv('DEV_LISTEN_HOST', '127.0.0.1')
Expand Down
12 changes: 11 additions & 1 deletion puppetboard/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from flask_wtf import FlaskForm
from wtforms import (
HiddenField, RadioField, SelectField,
TextAreaField, BooleanField, validators
TextAreaField, BooleanField, StringField,
PasswordField, validators
)


Expand All @@ -28,3 +29,12 @@ class QueryForm(FlaskForm):
('pql', 'PQL'),
])
rawjson = BooleanField('Raw JSON')


class LoginForm(FlaskForm):
"""The form used to login to Puppetboard"""
username = StringField('Username', [validators.DataRequired(
message = 'Username is required')])
password = PasswordField('Password', [validators.DataRequired(
message = 'Password is required')])
remember = BooleanField('Remember me')
36 changes: 36 additions & 0 deletions puppetboard/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from puppetboard.core import get_app
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import datetime


app = get_app()
db = SQLAlchemy(app)


class Users(db.Model):
created = db.Column(db.DateTime, default=datetime.datetime.now, nullable=False)
modified = db.Column(db.DateTime, default=datetime.datetime.now,
onupdate=datetime.datetime.now, nullable=False)
id = db.Column(db.Integer, unique=True, primary_key=True, nullable=False)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)

def __repr__(self):
return '{}/{}/{}'.format(self.id, self.username, self.password)


def is_authenticated(self):
return True


def is_active(self):
return True


def is_anonymous(self):
return False


def get_id(self):
return self.id
18 changes: 18 additions & 0 deletions puppetboard/static/css/puppetboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,24 @@ h1.ui.header.no-margin-bottom {
color: #FFF;
}

.ui.toggle.checkbox input:focus:checked ~ .box:before, .ui.toggle.checkbox input:focus:checked ~ label:before {
background-color: #2C3E50 !important;
}

.ui.button.darkblue {
background-color: #2C3E50;
border-color: #2C3E50;
color: #FFF;
}

.ui.button.darkblue:hover {
background-color: #2C3E50DB;
}

.inline.field.left {
text-align: left;
}

.ui.menu.yellow {
background-color: #F0E965;
}
Expand Down
23 changes: 22 additions & 1 deletion puppetboard/templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,28 @@
{% endfor %}
</div>
</div>
<div class="item right"><a href="https://github.com/voxpupuli/puppetboard" target="_blank">{{version()}}</a></div>
{%- if current_user.is_authenticated -%}
<div class="ui dropdown item">
Settings
<i class="dropdown icon"></i>
<div class="menu">
<a class="item" href="{{url_for('users')}}">Users</a>
</div>
</div>
{%- endif -%}
<div class="right menu">
{%- if current_user.is_authenticated -%}
<div class="ui dropdown item">
Logged in as: {{ current_user.username }}
<i class="dropdown icon"></i>
<div class="menu">
<a class="item" href="{{url_for('index')}}">Change Password</a>
<a class="item" href="{{url_for('logout')}}">Logout</a>
</div>
</div>
{%- endif -%}
<a class= "item" href="https://github.com/voxpupuli/puppetboard" target="_blank">{{version()}}</a>
</div>
</div>
<div class="ui grid padding-bottom">
<div class="one wide column"></div>
Expand Down
Loading

0 comments on commit a1a71be

Please sign in to comment.