Skip to content

Commit a1a71be

Browse files
author
Rob Reus
committed
Adding a basic login system to Puppetboard
1 parent 5023d4c commit a1a71be

File tree

11 files changed

+342
-4
lines changed

11 files changed

+342
-4
lines changed

Dockerfile

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ ENV PUPPETBOARD_SETTINGS docker_settings.py
77
RUN mkdir -p /usr/src/app/
88
WORKDIR /usr/src/app/
99

10+
VOLUME /var/lib/puppetboard
11+
1012
COPY requirements*.txt /usr/src/app/
1113
RUN pip install -r requirements-docker.txt
1214

puppetboard/app.py

+77-2
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,22 @@
1313
from flask import (
1414
Flask, render_template, abort, url_for,
1515
Response, stream_with_context, redirect,
16-
request, session, jsonify
16+
request, session, jsonify, flash
17+
)
18+
from flask_login import (
19+
LoginManager, login_required,
20+
login_user, logout_user
1721
)
1822
from jinja2.utils import contextfunction
1923

2024
from pypuppetdb.QueryBuilder import *
2125

22-
from puppetboard.forms import QueryForm
26+
from puppetboard.forms import QueryForm, LoginForm
2327
from puppetboard.utils import (get_or_abort, yield_or_stop,
2428
get_db_version)
2529
from puppetboard.dailychart import get_daily_reports_chart
30+
from puppetboard.models import db, Users
31+
from sqlalchemy.exc import OperationalError
2632

2733
import werkzeug.exceptions as ex
2834
import CommonMark
@@ -51,6 +57,19 @@
5157
]
5258

5359
app = get_app()
60+
login_manager = LoginManager()
61+
login_manager.init_app(app)
62+
login_manager.login_view = "login"
63+
if not app.config['LOGIN_DISABLED']:
64+
try:
65+
users = Users.query.all()
66+
except OperationalError:
67+
db.create_all()
68+
users = Users.query.all()
69+
if len(users) < 1:
70+
admin_user = Users(username='admin', password='admin123')
71+
db.session.add(admin_user)
72+
db.session.commit()
5473
graph_facts = app.config['GRAPH_FACTS']
5574
numeric_level = getattr(logging, app.config['LOGLEVEL'].upper(), None)
5675

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

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

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

286307
@app.route('/inventory', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
287308
@app.route('/<env>/inventory')
309+
@login_required
288310
def inventory(env):
289311
"""Fetch all (active) nodes from PuppetDB and stream a table displaying
290312
those nodes along with a set of facts about them.
@@ -306,6 +328,7 @@ def inventory(env):
306328
@app.route('/inventory/json',
307329
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
308330
@app.route('/<env>/inventory/json')
331+
@login_required
309332
def inventory_ajax(env):
310333
"""Backend endpoint for inventory table"""
311334
draw = int(request.args.get('draw', 0))
@@ -344,6 +367,7 @@ def inventory_ajax(env):
344367
@app.route('/node/<node_name>',
345368
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
346369
@app.route('/<env>/node/<node_name>')
370+
@login_required
347371
def node(env, node_name):
348372
"""Display a dashboard for a node showing as much data as we have on that
349373
node. This includes facts and reports but not Resources as that is too
@@ -378,6 +402,7 @@ def node(env, node_name):
378402
@app.route('/reports/<node_name>',
379403
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
380404
@app.route('/<env>/reports/<node_name>')
405+
@login_required
381406
def reports(env, node_name):
382407
"""Query and Return JSON data to reports Jquery datatable
383408
@@ -401,6 +426,7 @@ def reports(env, node_name):
401426
@app.route('/reports/<node_name>/json',
402427
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
403428
@app.route('/<env>/reports/<node_name>/json')
429+
@login_required
404430
def reports_ajax(env, node_name):
405431
"""Query and Return JSON data to reports Jquery datatable
406432
@@ -509,6 +535,7 @@ def reports_ajax(env, node_name):
509535
@app.route('/report/<node_name>/<report_id>',
510536
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
511537
@app.route('/<env>/report/<node_name>/<report_id>')
538+
@login_required
512539
def report(env, node_name, report_id):
513540
"""Displays a single report including all the events associated with that
514541
report and their status.
@@ -560,6 +587,7 @@ def report(env, node_name, report_id):
560587

561588
@app.route('/facts', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
562589
@app.route('/<env>/facts')
590+
@login_required
563591
def facts(env):
564592
"""Displays an alphabetical list of all facts currently known to
565593
PuppetDB.
@@ -609,6 +637,7 @@ def facts(env):
609637
@app.route('/fact/<fact>/<value>',
610638
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
611639
@app.route('/<env>/fact/<fact>/<value>')
640+
@login_required
612641
def fact(env, fact, value):
613642
"""Fetches the specific fact(/value) from PuppetDB and displays per
614643
node for which this fact is known.
@@ -655,6 +684,7 @@ def fact(env, fact, value):
655684
'fact': None, 'value': None})
656685
@app.route('/<env>/node/<node>/facts/json',
657686
defaults={'fact': None, 'value': None})
687+
@login_required
658688
def fact_ajax(env, node, fact, value):
659689
"""Fetches the specific facts matching (node/fact/value) from PuppetDB and
660690
return a JSON table
@@ -747,6 +777,7 @@ def fact_ajax(env, node, fact, value):
747777
@app.route('/query', methods=('GET', 'POST'),
748778
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
749779
@app.route('/<env>/query', methods=('GET', 'POST'))
780+
@login_required
750781
def query(env):
751782
"""Allows to execute raw, user created querries against PuppetDB. This is
752783
currently highly experimental and explodes in interesting ways since none
@@ -792,6 +823,7 @@ def query(env):
792823

793824
@app.route('/metrics', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
794825
@app.route('/<env>/metrics')
826+
@login_required
795827
def metrics(env):
796828
"""Lists all available metrics that PuppetDB is aware of.
797829
@@ -812,6 +844,7 @@ def metrics(env):
812844
@app.route('/metric/<path:metric>',
813845
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
814846
@app.route('/<env>/metric/<path:metric>')
847+
@login_required
815848
def metric(env, metric):
816849
"""Lists all information about the metric of the given name.
817850
@@ -839,6 +872,7 @@ def metric(env, metric):
839872
@app.route('/catalogs/compare/<compare>',
840873
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
841874
@app.route('/<env>/catalogs/compare/<compare>')
875+
@login_required
842876
def catalogs(env, compare):
843877
"""Lists all nodes with a compiled catalog.
844878
@@ -867,6 +901,7 @@ def catalogs(env, compare):
867901
@app.route('/catalogs/compare/<compare>/json',
868902
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
869903
@app.route('/<env>/catalogs/compare/<compare>/json')
904+
@login_required
870905
def catalogs_ajax(env, compare):
871906
"""Server data to catalogs as JSON to Jquery datatables
872907
"""
@@ -926,6 +961,7 @@ def catalogs_ajax(env, compare):
926961
@app.route('/catalog/<node_name>',
927962
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
928963
@app.route('/<env>/catalog/<node_name>')
964+
@login_required
929965
def catalog_node(env, node_name):
930966
"""Fetches from PuppetDB the compiled catalog of a given node.
931967
@@ -950,6 +986,7 @@ def catalog_node(env, node_name):
950986
@app.route('/catalogs/compare/<compare>...<against>',
951987
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
952988
@app.route('/<env>/catalogs/compare/<compare>...<against>')
989+
@login_required
953990
def catalog_compare(env, compare, against):
954991
"""Compares the catalog of one node, parameter compare, with that of
955992
with that of another node, parameter against.
@@ -978,6 +1015,7 @@ def catalog_compare(env, compare, against):
9781015

9791016
@app.route('/radiator', defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
9801017
@app.route('/<env>/radiator')
1018+
@login_required
9811019
def radiator(env):
9821020
"""This view generates a simplified monitoring page
9831021
akin to the radiator view in puppet dashboard
@@ -1077,6 +1115,7 @@ def radiator(env):
10771115
@app.route('/daily_reports_chart.json',
10781116
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
10791117
@app.route('/<env>/daily_reports_chart.json')
1118+
@login_required
10801119
def daily_reports_chart(env):
10811120
"""Return JSON data to generate a bar chart of daily runs.
10821121
@@ -1104,3 +1143,39 @@ def offline_static(filename):
11041143

11051144
return Response(response=render_template('static/%s' % filename),
11061145
status=200, mimetype=mimetype)
1146+
1147+
1148+
@app.route("/login", methods=["GET", "POST"])
1149+
def login():
1150+
form = LoginForm(meta={
1151+
'csrf_secret': app.config['SECRET_KEY'],
1152+
'csrf_context': session})
1153+
if form.validate_on_submit():
1154+
user = Users.query.filter_by(username=form.username.data).first()
1155+
if user and user.password == form.password.data:
1156+
login_user(user, remember=form.remember.data)
1157+
return redirect(url_for('index'))
1158+
else:
1159+
flash('Login failed.', 'error')
1160+
return render_template('login.html', form=form)
1161+
1162+
1163+
@app.route("/users")
1164+
@login_required
1165+
def users():
1166+
users = Users.query.all()
1167+
return render_template('users.html', users=users)
1168+
1169+
1170+
@app.route("/logout")
1171+
@login_required
1172+
def logout():
1173+
logout_user()
1174+
flash('You have been logged out.', 'info')
1175+
return redirect(url_for('login'))
1176+
1177+
1178+
@login_manager.user_loader
1179+
def load_user(user_id):
1180+
return Users.query.filter_by(id=int(user_id)).first()
1181+

puppetboard/default_settings.py

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
PUPPETDB_CERT = None
99
PUPPETDB_TIMEOUT = 20
1010
DEFAULT_ENVIRONMENT = 'production'
11+
LOGIN_DISABLED = True
12+
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/puppetboard.db'
13+
SQLALCHEMY_TRACK_MODIFICATIONS = False
1114
SECRET_KEY = os.urandom(24)
1215
DEV_LISTEN_HOST = '127.0.0.1'
1316
DEV_LISTEN_PORT = 5000

puppetboard/docker_settings.py

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
PUPPETDB_CERT = os.getenv('PUPPETDB_CERT', None)
1717
PUPPETDB_PROTO = os.getenv('PUPPETDB_PROTO', None)
1818
PUPPETDB_TIMEOUT = int(os.getenv('PUPPETDB_TIMEOUT', '20'))
19+
LOGIN_DISABLED = os.getenv('LOGIN_DISABLED', True)
20+
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI',
21+
'sqlite:////var/lib/puppetboard/database.db')
1922
DEFAULT_ENVIRONMENT = os.getenv('DEFAULT_ENVIRONMENT', 'production')
2023
SECRET_KEY = os.getenv('SECRET_KEY', os.urandom(24))
2124
DEV_LISTEN_HOST = os.getenv('DEV_LISTEN_HOST', '127.0.0.1')

puppetboard/forms.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from flask_wtf import FlaskForm
55
from wtforms import (
66
HiddenField, RadioField, SelectField,
7-
TextAreaField, BooleanField, validators
7+
TextAreaField, BooleanField, StringField,
8+
PasswordField, validators
89
)
910

1011

@@ -28,3 +29,12 @@ class QueryForm(FlaskForm):
2829
('pql', 'PQL'),
2930
])
3031
rawjson = BooleanField('Raw JSON')
32+
33+
34+
class LoginForm(FlaskForm):
35+
"""The form used to login to Puppetboard"""
36+
username = StringField('Username', [validators.DataRequired(
37+
message = 'Username is required')])
38+
password = PasswordField('Password', [validators.DataRequired(
39+
message = 'Password is required')])
40+
remember = BooleanField('Remember me')

puppetboard/models.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from puppetboard.core import get_app
2+
from flask_sqlalchemy import SQLAlchemy
3+
from flask_migrate import Migrate
4+
import datetime
5+
6+
7+
app = get_app()
8+
db = SQLAlchemy(app)
9+
10+
11+
class Users(db.Model):
12+
created = db.Column(db.DateTime, default=datetime.datetime.now, nullable=False)
13+
modified = db.Column(db.DateTime, default=datetime.datetime.now,
14+
onupdate=datetime.datetime.now, nullable=False)
15+
id = db.Column(db.Integer, unique=True, primary_key=True, nullable=False)
16+
username = db.Column(db.String(80), unique=True, nullable=False)
17+
password = db.Column(db.String(80), nullable=False)
18+
19+
def __repr__(self):
20+
return '{}/{}/{}'.format(self.id, self.username, self.password)
21+
22+
23+
def is_authenticated(self):
24+
return True
25+
26+
27+
def is_active(self):
28+
return True
29+
30+
31+
def is_anonymous(self):
32+
return False
33+
34+
35+
def get_id(self):
36+
return self.id

puppetboard/static/css/puppetboard.css

+18
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,24 @@ h1.ui.header.no-margin-bottom {
124124
color: #FFF;
125125
}
126126

127+
.ui.toggle.checkbox input:focus:checked ~ .box:before, .ui.toggle.checkbox input:focus:checked ~ label:before {
128+
background-color: #2C3E50 !important;
129+
}
130+
131+
.ui.button.darkblue {
132+
background-color: #2C3E50;
133+
border-color: #2C3E50;
134+
color: #FFF;
135+
}
136+
137+
.ui.button.darkblue:hover {
138+
background-color: #2C3E50DB;
139+
}
140+
141+
.inline.field.left {
142+
text-align: left;
143+
}
144+
127145
.ui.menu.yellow {
128146
background-color: #F0E965;
129147
}

puppetboard/templates/layout.html

+22-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,28 @@
8989
{% endfor %}
9090
</div>
9191
</div>
92-
<div class="item right"><a href="https://github.com/voxpupuli/puppetboard" target="_blank">{{version()}}</a></div>
92+
{%- if current_user.is_authenticated -%}
93+
<div class="ui dropdown item">
94+
Settings
95+
<i class="dropdown icon"></i>
96+
<div class="menu">
97+
<a class="item" href="{{url_for('users')}}">Users</a>
98+
</div>
99+
</div>
100+
{%- endif -%}
101+
<div class="right menu">
102+
{%- if current_user.is_authenticated -%}
103+
<div class="ui dropdown item">
104+
Logged in as: {{ current_user.username }}
105+
<i class="dropdown icon"></i>
106+
<div class="menu">
107+
<a class="item" href="{{url_for('index')}}">Change Password</a>
108+
<a class="item" href="{{url_for('logout')}}">Logout</a>
109+
</div>
110+
</div>
111+
{%- endif -%}
112+
<a class= "item" href="https://github.com/voxpupuli/puppetboard" target="_blank">{{version()}}</a>
113+
</div>
93114
</div>
94115
<div class="ui grid padding-bottom">
95116
<div class="one wide column"></div>

0 commit comments

Comments
 (0)