Merge branch 'dev' of gitea.planet-casio.com:devs/PCv5 into dev

This commit is contained in:
Eragon 2019-11-29 11:19:59 +01:00
commit f3e47bd082
No known key found for this signature in database
GPG Key ID: B2B1BF4DA61BBB85
21 changed files with 180 additions and 29 deletions

View File

@ -1,6 +1,6 @@
# Bibliothèques nécessaires
Attention, l'environnement est sous `python3`. Vérifiez que ce soit bien le cas sur votre config, quitte
Attention, l'environnement est sous `python3`. Vérifiez que ce soit bien le cas sur votre config, quitte
à faire un virtual environment.
La liste de paquets fourni est pour Archlinux, les paquets peuvent avoir des noms légèrement différents dans votre distribution.
@ -12,6 +12,7 @@ python-flask-migrate
python-flask-script
python-flask-sqlalchemy
python-flask-wtf
python-ldap
python-uwsgi
python-psycopg2
python-pyyaml

View File

@ -32,7 +32,7 @@ from app.models.forum import Forum
from app.models.topic import Topic
from app.models.notification import Notification
from app.routes import index, search, users # To load routes at initialization
from app.routes import index, search, users, tools # To load routes at initialization
from app.routes.account import login, account, notification
from app.routes.admin import index, groups, account, trophies, forums
from app.routes.forum import index

View File

@ -9,6 +9,8 @@ from app.models.notification import Notification
import app.utils.unicode_names as unicode_names
from app.utils.notify import notify
from config import V5Config
from local_config import USE_LDAP
import app.utils.ldap as ldap
import werkzeug.security
import re
@ -108,7 +110,9 @@ class Member(User):
self.name = name
self.norm = unicode_names.normalize(name)
self.email = email
self.set_password(password)
if not USE_LDAP:
self.set_password(password)
# Workflow with LDAP enabled is User → Postgresql → LDAP → set password
self.xp = 0
self.bio = ""
@ -163,8 +167,11 @@ class Member(User):
data = {key: data[key] for key in data if data[key] is not None}
# TODO: verify good type of those args, think about the password mgt
# Beware of LDAP injections
if "email" in data:
self.email = data["email"]
if USE_LDAP:
ldap.set_email(self.norm, self.email)
if "password" in data:
self.set_password(data["password"])
if "bio" in data:
@ -203,13 +210,19 @@ class Member(User):
Set the user's password. Check whether the request sender has the right
to do this!
"""
self.password_hash = werkzeug.security.generate_password_hash(password,
method='pbkdf2:sha512', salt_length=10)
if USE_LDAP:
ldap.set_password(self, password)
else:
self.password_hash = werkzeug.security.generate_password_hash(
password, method='pbkdf2:sha512', salt_length=10)
def check_password(self, password):
"""Compares password against member hash."""
return werkzeug.security.check_password_hash(self.password_hash,
password)
if USE_LDAP:
return ldap.check_password(self, password)
else:
return werkzeug.security.check_password_hash(self.password_hash,
password)
def notify(self, message, href=None):
""" Notify a user with a message.

View File

@ -4,6 +4,8 @@ from app import app, db
from app.forms.account import UpdateAccountForm, RegistrationForm, DeleteAccountForm
from app.models.users import Member
from app.utils.render import render
import app.utils.ldap as ldap
from local_config import USE_LDAP
@app.route('/account', methods=['GET', 'POST'])
@ -30,7 +32,7 @@ def edit_account():
else:
flash('Erreur lors de la modification', 'error')
return render('account.html', form=form)
return render('account/account.html', form=form)
@app.route('/account/delete', methods=['GET', 'POST'])
@ -47,7 +49,7 @@ def delete_account():
else:
flash('Erreur lors de la suppression du compte', 'error')
del_form.delete.data = False # Force to tick to delete the account
return render('delete_account.html', del_form=del_form)
return render('account/delete_account.html', del_form=del_form)
@app.route('/register', methods=['GET', 'POST'])
@ -60,9 +62,13 @@ def register():
member.newsletter = form.newsletter.data
db.session.add(member)
db.session.commit()
# Workflow with LDAP is User → Postgresql → LDAP → Change password
if USE_LDAP:
ldap.add_member(member)
ldap.set_password(member, form.password.data)
flash('Inscription réussie', 'ok')
return redirect(url_for('validation') + "?email=" + form.email.data)
return render('register.html', title='Register', form=form)
return render('account/register.html', title='Register', form=form)
@app.route('/register/validation/', methods=['GET', 'POST'])
@ -70,4 +76,4 @@ def validation():
mail = request.args['email']
if current_user.is_authenticated:
return redirect(url_for('index'))
return render('validation.html', mail=mail)
return render('account/validation.html', mail=mail)

View File

@ -1,6 +1,6 @@
from flask import redirect, url_for, request, flash
from flask_login import login_user, logout_user, login_required, current_user
from urllib.parse import urlparse
from urllib.parse import urlparse, urljoin
from app import app
from app.forms.login import LoginForm
from app.models.users import Member
@ -15,6 +15,9 @@ def login():
return redirect(url_for('index'))
form = LoginForm()
lateral = LoginForm(prefix="menu_")
if lateral.validate_on_submit():
form = lateral
if form.validate_on_submit():
member = Member.query.filter_by(name=form.username.data).first()
@ -51,7 +54,7 @@ def login():
return redirect(request.referrer)
return redirect(url_for('index'))
return render('login.html', form=form)
return render('account/login.html', form=form)
@app.route('/logout')

8
app/routes/tools.py Normal file
View File

@ -0,0 +1,8 @@
from app import app
from app.utils.render import render
@app.route('/tools')
def tools():
return render('tools.html')

View File

@ -9,7 +9,7 @@ from app.utils.render import render
def user(username):
norm = unicode_names.normalize(username)
member = Member.query.filter_by(norm=norm).first_or_404()
return render('user.html', member=member)
return render('account/user.html', member=member)
@app.route('/user/id/<int:user_id>')

View File

@ -1,10 +1,12 @@
/* Trigger actions for the menu */
/* Initialization */
var b = document.getElementById('light-menu').getElementsByTagName('a')
var b = document.querySelectorAll('#light-menu a');
for(var i = 1; i < b.length; i++) {
b[i].setAttribute('onfocus', "this.setAttribute('f', 'true');");
b[i].setAttribute('onblur', "this.setAttribute('f', 'false');");
b[i].removeAttribute('href');
console.log("Removed");
}
var trigger_menu = function(active) {
@ -15,8 +17,8 @@ var trigger_menu = function(active) {
element.classList.remove('opened');
}
var menu = document.getElementById('menu');
var buttons = document.getElementById('light-menu').getElementsByTagName('li');
var menu = document.querySelector('#menu');
var buttons = document.querySelectorAll('#light-menu li');
var menus = document.querySelectorAll('#menu > div');
if(active == -1 || buttons[active].classList.contains('opened')) {
@ -39,8 +41,8 @@ var trigger_menu = function(active) {
}
var mouse_trigger = function(event) {
var menu = document.getElementById('menu');
var buttons = document.getElementById('light-menu').getElementsByTagName('li');
var menu = document.querySelector('#menu');
var buttons = document.querySelectorAll('#light-menu li');
if(!menu.contains(event.target)) {
var active = -1;
@ -48,7 +50,7 @@ var mouse_trigger = function(event) {
for(i = 0; i < buttons.length; i++) {
if(buttons[i].contains(event.target))
active = i;
buttons[i].getElementsByTagName('a')[0].blur();
buttons[i].querySelector('a').blur();
}
trigger_menu(active);
@ -57,11 +59,11 @@ var mouse_trigger = function(event) {
var keyboard_trigger = function(event) {
var menu = document.getElementById('menu');
var buttons = document.getElementById('light-menu').getElementsByTagName('li');
var buttons = document.querySelectorAll('#light-menu li');
if(event.keyCode == 13) {
for(var i = 0; i < buttons.length; i++) {
if(buttons[i].getElementsByTagName('a')[0].getAttribute('f') == 'true') {
if(buttons[i].querySelector('a').getAttribute('f') == 'true') {
trigger_menu(i);
}
}

View File

@ -9,7 +9,7 @@
<section>
{{ widget_member.profile(member) }}
{% if current_user.is_authenticated and current_user.priv('access-admin-panel') %}
{% if current_user.is_authenticated and (current_user == member or current_user.priv('access-admin-panel')) %}
<div><a href="{{ url_for('adm_edit_account', user_id=member.id) }}">Modifier</a></div>
{% endif %}

View File

@ -5,7 +5,11 @@
</a>
<li>
<a role="button" label="Compte" tabindex="0">
{% if current_user.is_authenticated %}
<a href="{{ url_for('user', username=current_user.name) }}" role="button" label="Compte" tabindex="0">
{% else %}
<a href="{{ url_for('login') }}" role="button" label="Compte" tabindex="0">
{% endif %}
<svg viewBox="0 0 24 24">
<path fill="#ffffff" d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"></path>
</svg>
@ -23,7 +27,7 @@
</li>
<li>
<a role="button" label="Forum" tabindex="0">
<a href="{{ url_for('forum_index') }}" role="button" label="Forum" tabindex="0">
<svg viewBox="0 0 24 24">
<path fill="#ffffff" d="M17,12V3A1,1 0 0,0 16,2H3A1,1 0 0,0 2,3V17L6,13H16A1,1 0 0,0 17,12M21,6H19V15H6V17A1,1 0 0,0 7,18H18L22,22V7A1,1 0 0,0 21,6Z"></path>
</svg>
@ -59,7 +63,7 @@
</li>
<li>
<a role="button" label="Outils" tabindex="0">
<a href="{{ url_for('tools') }}" role="button" label="Outils" tabindex="0">
<svg viewBox="0 0 24 24">
<path fill="#ffffff" d="M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z"></path>
</svg>

18
app/templates/tools.html Normal file
View File

@ -0,0 +1,18 @@
{% extends "base/base.html" %}
{% block title %}
<h1>Outils</h1>
{% endblock %}
{% block content %}
<section>
<p>Planète Casio met à votre disposition divers outils. Pour vous connecter,
utilisez votre identifiant unique et votre mot de passe habituel.
<ul>
<li><a href="https://gitea.planet-casio.com">Gitea</a> (forge Git)</li>
<li><a href="https://wiki.planet-casio.com">Wiki</a> (wiki répétoriant tout un tas de trucs)</li>
<li><a href="https://bible.planet-casio.com">Bible</a> (la bible du programmeur Casio bas niveau)</li>
</ul>
</p>
</section>
{% endblock %}

83
app/utils/ldap.py Normal file
View File

@ -0,0 +1,83 @@
import ldap
from ldap.modlist import addModlist, modifyModlist
from local_config import LDAP_PASSWORD, LDAP_ORGANIZATION
def get_member(username):
""" Get informations about member. Username must be normalized! """
conn = ldap.initialize("ldap://localhost")
# Search for user
r = conn.search_s(LDAP_ORGANIZATION, ldap.SCOPE_SUBTREE, f'(cn={username})')
if len(r) > 0:
return r[0]
else:
return None
def edit(user, fields):
""" Edit a user. Fields is {'name': ['value'], …} """
conn = ldap.initialize("ldap://localhost")
# Connect as root
# conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}', LDAP_PASSWORD)
# old_value = {"userPassword": ["my_old_password"]}
# new_value = {"userPassword": ["my_new_password"]}
modlist = ldap.modlist.modifyModlist(old_value, new_value)
con.modify_s(dn, modlist)
def set_email(user, email):
pass
def set_password(user, password):
""" Set password for a user. """
conn = ldap.initialize("ldap://localhost")
# Connect as root
conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}',
LDAP_PASSWORD)
conn.passwd_s(f"cn={user.norm},{LDAP_ORGANIZATION}", None, password)
def check_password(user, password):
""" Try to login a user through LDAP register. """
conn = ldap.initialize("ldap://localhost")
try:
conn.simple_bind_s(f"cn={user.norm},{LDAP_ORGANIZATION}", password)
except ldap.INVALID_CREDENTIALS:
return False
return True
def add_member(member):
""" Add a member to LDAP register. Fields must have been sanitized! """
if get_member(member.norm) is not None:
print("User already exists")
return
conn = ldap.initialize("ldap://localhost")
# Connect as root
conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}', LDAP_PASSWORD)
# Create fields
dn = f'cn={member.norm},{LDAP_ORGANIZATION}'
modlist = addModlist({
'objectClass': [bytes('inetOrgPerson', 'UTF8')],
'cn': [bytes(member.norm, 'UTF8')],
'sn': [bytes(member.norm, 'UTF8')],
'displayName': [bytes(member.name, 'UTF8')],
'mail': [bytes(member.email, 'UTF8')],
'uid': [bytes(str(member.id), 'UTF8')],
'userPassword': [bytes("", 'UTF8')]
})
# Add the member
conn.add_s(dn, modlist)
def delete_member(member):
""" Remove a member from LDAP register """
conn = ldap.initialize("ldap://localhost")
# Connect as root
conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}', LDAP_PASSWORD)
# Create fields
dn = f'cn={member.norm},{LDAP_ORGANIZATION}'
# Delete the user
conn.delete_s(dn)

View File

@ -31,7 +31,7 @@ def render(*args, styles=[], **kwargs):
if s[0] == '+':
styles_.append(s[1:])
login_form = LoginForm()
login_form = LoginForm(prefix="menu_")
search_form = SearchForm()
return render_template(*args, **kwargs,
login_form=login_form, search_form=search_form, styles=styles_)

View File

@ -27,7 +27,7 @@ def valid_name(name, msg=False):
# Rule 2
try:
normalize(name)
normalized_name = normalize(name)
except ValueError:
errors.append("cant-normalize")
@ -36,7 +36,7 @@ def valid_name(name, msg=False):
errors.append("no-letter")
# Rule 4
if name in V5Config.FORBIDDEN_USERNAMES:
if normalized_name in V5Config.FORBIDDEN_USERNAMES:
errors.append("forbidden")
return True if errors == [] else errors

View File

@ -3,7 +3,9 @@ from wtforms.validators import ValidationError
from app.models.users import Member
from app.utils.valid_name import valid_name
from app.utils.unicode_names import normalize
import app.utils.ldap as ldap
from config import V5Config
from local_config import USE_LDAP
def name_valid(form, name):
@ -42,6 +44,13 @@ def name_available(form, name):
if member is not None:
raise ValidationError("Ce nom d'utilisateur est indisponible.")
# Double check with LDAP if needed
if USE_LDAP:
member = ldap.get_member(norm)
if member is not None:
raise ValidationError("Ce nom d'utilisateur est indisponible.")
def email(form, email):
member = Member.query.filter_by(email=email.data).first()

4
local_config.py.default Normal file
View File

@ -0,0 +1,4 @@
DB_NAME = "pcv5"
USE_LDAP = False
LDAP_PASSWORD = "openldap"
LDAP_ORGANIZATION = "o=planet-casio"