From 416c8d46d223f891b25e6c3a2ddbcea9379a0fa2 Mon Sep 17 00:00:00 2001 From: Daniel Muckerman Date: Sat, 12 Dec 2020 01:28:45 -0500 Subject: [PATCH] Big login update, share login cookies with account app --- .gitignore | 4 +- app.py | 179 ++++++++++++++++++++++++++++++++--------- requirements.txt | 4 +- templates/card_list.j2 | 3 +- templates/games.j2 | 4 +- templates/index.j2 | 4 +- templates/login.j2 | 9 +-- 7 files changed, 155 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index 16e6767..ed715f6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ dashboard-env/ .envrc .DS_Store -.vscode/ \ No newline at end of file +.vscode/ + +users.db \ No newline at end of file diff --git a/app.py b/app.py index d38a0bc..4cfe309 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,14 @@ import ldap as l from ldap3 import Server, Connection, ALL, MODIFY_REPLACE -from flask import Flask, g, request, session, redirect, url_for, render_template +from ldap3.core.exceptions import LDAPBindError +from flask import Flask, g, request, session, redirect, url_for, render_template, flash from flask_bs4 import Bootstrap -from flask_simpleldap import LDAP +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager, login_manager, current_user, login_user, \ + logout_user, login_required +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, BooleanField, SubmitField +from wtforms.validators import DataRequired import yaml import datetime as dt import pytz @@ -14,6 +20,10 @@ Bootstrap(app) app.secret_key = 'asdf' app.debug = True +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['WTF_CSRF_SECRET_KEY'] = 'asdf' + # Base app.config['LDAP_REALM_NAME'] = 'OpenLDAP Authentication' app.config['LDAP_HOST'] = os.environ.get('LDAP_HOST') @@ -26,7 +36,18 @@ app.config['LDAP_OBJECTS_DN'] = 'dn' app.config['LDAP_OPENLDAP'] = True app.config['LDAP_USER_OBJECT_FILTER'] = '(&(objectclass=posixAccount)(uid=%s))' -ldap = LDAP(app) +# Login cookies +app.config['REMEMBER_COOKIE_DOMAIN'] = os.environ.get('COOKIE_DOMAIN') + +db = SQLAlchemy(app) + +login_manager = LoginManager(app) +login_manager.init_app(app) +login_manager.login_view = 'login' + +db.create_all() + + eastern = pytz.timezone('US/Eastern') @@ -61,14 +82,74 @@ for itm in yaml_data['games'].items(): server = Server(app.config['LDAP_HOST']) conn = Connection(server, app.config['LDAP_USERNAME'], app.config['LDAP_PASSWORD'], auto_bind=True) + + +class User(db.Model): + + __tablename__ = 'user' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(100)) + password = db.Column(db.String(128)) + authenticated = db.Column(db.Boolean, default=False) + + def __init__(self, username, password): + self.username = username + self.password = password + + @staticmethod + def try_login(username, password): + conn.search(app.config['LDAP_BASE_DN'], app.config['LDAP_USER_OBJECT_FILTER'] % username, attributes=['*']) + if len(conn.entries) > 0: + Connection(app.config['LDAP_HOST'], conn.entries[0].entry_dn, password, auto_bind=True) + return + raise LDAPBindError + + def is_authenticated(self): + return self.authenticated + + def is_active(self): + return True + + def is_anonymous(self): + return False + + def get_id(self): + return self.id + + def get_user_dict(self): + user = {'dn': '', + 'firstName': '', + 'lastName': '', + 'email': '', + 'userName': self.username, + } + + conn.search(app.config['LDAP_BASE_DN'], app.config['LDAP_USER_OBJECT_FILTER'] % self.username, attributes=['*']) + + user['dn'] = conn.entries[0].entry_dn + user['firstName'] = conn.entries[0].givenName.value + user['lastName'] = conn.entries[0].sn.value + user['email'] = conn.entries[0].mail.value + return user + + +class LoginForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired()]) + remember_me = BooleanField('Remember Me') + submit = SubmitField('Sign In') + +@login_manager.user_loader +def load_user(id): + return User.query.get(int(id)) + + @app.before_request -def before_request(): - g.user = None - if 'user_id' in session: - # This is where you'd query your database to get the user info. - g.user = {} +def get_current_user(): + g.user = current_user @app.route('/') @@ -83,46 +164,63 @@ def index(): @app.route('/games') -def game(): - if 'user_id' in session: - user_dict = ldap.get_object_details(session['user_id']) - user = {'dn': 'cn={},cn=usergroup,ou=users,dc=technicalincompetence,dc=club'.format(user_dict['cn'][0].decode('ascii')), - 'firstName': user_dict['givenName'][0].decode('ascii'), - 'lastName': user_dict['sn'][0].decode('ascii'), - 'email': user_dict['mail'][0].decode('ascii'), - 'userName': user_dict['uid'][0].decode('ascii'), - } - +def game(): current_time = eastern.localize(dt.datetime.now()) if final_countdown_data != None: if (final_time - current_time).days > -1: return render_template('final_countdown.j2', final_countdown = final_countdown_data) if countdown_data != None: return render_template('games.j2', apps = games, search = search, account_url = account_url, description = game_description, countdown = countdown_data) - if 'user_id' in session: - return render_template('games.j2', apps = games, search = search, account_url = account_url, description = game_description, user = user, game_list = generate_game_list()) + if current_user.is_authenticated: + return render_template('games.j2', apps = games, search = search, account_url = account_url, description = game_description, user = current_user.get_user_dict(), game_list = generate_game_list()) return render_template('games.j2', apps = games, search = search, account_url = account_url, description = game_description) @app.route('/login', methods=['GET', 'POST']) def login(): - if g.user: - return redirect(url_for('index')) - if request.method == 'POST': - user = request.form['user'] - passwd = request.form['passwd'] - test = ldap.bind_user(user, passwd) - if test is None or passwd == '': - return render_template('login.j2', error='Invalid credentials') - else: - session['user_id'] = request.form['user'] - session['passwd'] = request.form['passwd'] - return redirect('/games') - return render_template('login.j2') - - -@ldap.login_required + if current_user.is_authenticated: + flash('You are already logged in.') + return redirect(url_for('auth.home')) + + form = LoginForm(request.form) + print(form) + print(request.method) + + if request.method == 'POST' and form.validate(): + username = request.form.get('username') + password = request.form.get('password') + print(username) + print(password) + + try: + User.try_login(username, password) + except LDAPBindError: + flash( + 'Invalid username or password. Please try again.', + 'danger') + return render_template('login.j2', form=form) + + user = User.query.filter(User.username == username).first() + + print(user) + if user is None: + user = User(username, password) + db.session.add(user) + user.authenticated = True + db.session.commit() + login_user(user, remember=form.remember_me.data) + + print('You have successfully logged in.') + return redirect('/games') + + if form.errors: + flash(form.errors, 'danger') + + return render_template('login.j2', form=form) + + @app.route('/add', methods=['POST']) +@login_required def add_game(): if request.method == 'POST': game_title = request.form['game_title'] @@ -141,8 +239,8 @@ def add_game(): return 'Error' -@ldap.login_required @app.route('/delete', methods=['POST']) +@login_required def delete_game(): if request.method == 'POST': game_id = request.form['game_id'] @@ -176,8 +274,13 @@ def generate_game_list(): @app.route('/logout') +@login_required def logout(): - session.pop('user_id', None) + user = current_user + user.authenticated = False + db.session.add(user) + db.session.commit() + logout_user() return redirect(url_for('game')) diff --git a/requirements.txt b/requirements.txt index d610db7..ffdd0ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,6 @@ pytz Flask-Login==0.5.0 Flask-SimpleLDAP==1.4.0 python-ldap==3.2.0 -ldap3==2.7 \ No newline at end of file +ldap3==2.8.1 +Flask-SQLAlchemy==2.4.4 +Flask-WTF==0.14.3 \ No newline at end of file diff --git a/templates/card_list.j2 b/templates/card_list.j2 index 40adb4f..b496272 100644 --- a/templates/card_list.j2 +++ b/templates/card_list.j2 @@ -2,7 +2,8 @@
Games In Progress
- {% if user %} + {{ current_user }} + {% if current_user.is_authenticated %} diff --git a/templates/games.j2 b/templates/games.j2 index 2a7fdc0..8f517be 100644 --- a/templates/games.j2 +++ b/templates/games.j2 @@ -64,8 +64,8 @@ {{ super () }} {% if countdown is defined %} {% endif %} diff --git a/templates/index.j2 b/templates/index.j2 index c58941d..bf73b0d 100644 --- a/templates/index.j2 +++ b/templates/index.j2 @@ -59,8 +59,8 @@ {{ super () }} {% if countdown is defined %} {% endif %} diff --git a/templates/login.j2 b/templates/login.j2 index d4935b3..293cf96 100644 --- a/templates/login.j2 +++ b/templates/login.j2 @@ -1,3 +1,4 @@ +{% import "bootstrap/wtf.html" as wtf %} {% extends "bootstrap/base.html" %} {% block title %}Manage your Technical Incompetence account{% endblock %} @@ -26,12 +27,6 @@ {% endif %} - - -
- -
- - + {{wtf.quick_form(form, novalidate=True)}} {% endblock %} \ No newline at end of file