added password validation; changed create account layout

Password validation added, configurable under the [password_validation]
section in config.ini. Each criterion is displayed next to the form, in
red or green depending on whether the password passes it. form.html now
looks different because of it, whether it is enabled or disabled. An
error message is now displayed if the user already exists.
This commit is contained in:
2020-04-14 21:31:44 +01:00
parent 0ab93990e8
commit 690de58e9f
7 changed files with 261 additions and 56 deletions

View File

@@ -0,0 +1,45 @@
specials = ['[', '@', '_', '!', '#', '$', '%', '^', '&', '*', '(', ')',
'<', '>', '?', '/', '\\', '|', '}', '{', '~', ':', ']']
class PasswordValidator:
def __init__(self, min_length, upper, number, special):
self.criteria = {'characters': int(min_length),
'uppercase characters': int(upper),
'numbers': int(number),
'special characters': int(special)}
def validate(self, password):
count = {'characters': 0,
'uppercase characters': 0,
'numbers': 0,
'special characters': 0}
for c in password:
count['characters'] += 1
if c.isupper():
count['uppercase characters'] += 1
elif c.isnumeric():
count['numbers'] += 1
elif c in specials:
count['special characters'] += 1
for criterion in count:
if count[criterion] < self.criteria[criterion]:
count[criterion] = False
else:
count[criterion] = True
return count
def getCriteria(self):
lines = {}
for criterion in self.criteria:
min = self.criteria[criterion]
if min > 0:
text = f"Must have at least {min} "
if min == 1:
text += criterion[:-1]
else:
text += criterion
lines[criterion] = text
return lines

View File

@@ -1,12 +1,14 @@
from flask import request, jsonify
from configparser import RawConfigParser
from jellyfin_accounts.jf_api import Jellyfin
import json
import datetime
import secrets
import time
from __main__ import config, app, g
from __main__ import config, config_path, app, g
from __main__ import web_log as log
from jellyfin_accounts.login import auth
from jellyfin_accounts.validate_password import PasswordValidator
def resp(success=True, code=500):
if success:
@@ -57,43 +59,76 @@ while attempts != 3:
log.info(('Successfully authenticated with ' +
config['jellyfin']['server']))
break
except jellyfin_accounts.jf_api.AuthenticationError:
except Jellyfin.AuthenticationError:
attempts += 1
log.error(('Failed to authenticate with ' +
config['jellyfin']['server'] +
'. Retrying...'))
time.sleep(5)
if config.getboolean('password_validation', 'enabled'):
validator = PasswordValidator(config['password_validation']['min_length'],
config['password_validation']['upper'],
config['password_validation']['number'],
config['password_validation']['special'])
else:
validator = PasswordValidator(0, 0, 0, 0)
@app.route('/getRequirements', methods=['GET', 'POST'])
def getRequirements():
data = request.get_json()
log.debug('Password Requirements requested')
if checkInvite(data['code']):
return jsonify(validator.getCriteria())
@app.route('/newUser', methods=['GET', 'POST'])
def newUser():
data = request.get_json()
log.debug('Attempted newUser')
if checkInvite(data['code'], delete=True):
user = jf.newUser(data['username'], data['password'])
if user.status_code == 200:
if checkInvite(data['code']):
validation = validator.validate(data['password'])
valid = True
for criterion in validation:
if validation[criterion] == False:
valid = False
if valid:
log.debug('User password valid')
try:
with open(config['files']['user_template'], 'r') as f:
default_policy = json.load(f)
jf.setPolicy(user.json()['Id'], default_policy)
user = jf.newUser(data['username'], data['password'])
except Jellyfin.UserExistsError:
error = 'User already exists with name '
error += data['username']
log.debug(error)
return jsonify({'error': error})
except:
log.debug('setPolicy failed')
pass
if config.getboolean('email', 'enabled'):
return jsonify({'error': 'Unknown error'})
checkInvite(data['code'], delete=True)
if user.status_code == 200:
try:
with open(config['files']['emails'], 'r') as f:
emails = json.load(f)
except (FileNotFoundError, json.decoder.JSONDecodeError):
emails = {}
emails[data['username']] = data['email']
with open(config['files']['emails'], 'w') as f:
f.write(json.dumps(emails, indent=4))
log.debug('Email address stored')
log.info('New User created.')
return resp()
with open(config['files']['user_template'], 'r') as f:
default_policy = json.load(f)
jf.setPolicy(user.json()['Id'], default_policy)
except:
log.debug('setPolicy failed')
if config.getboolean('email', 'enabled'):
try:
with open(config['files']['emails'], 'r') as f:
emails = json.load(f)
except (FileNotFoundError, json.decoder.JSONDecodeError):
emails = {}
emails[data['username']] = data['email']
with open(config['files']['emails'], 'w') as f:
f.write(json.dumps(emails, indent=4))
log.debug('Email address stored')
log.info('New User created.')
else:
log.error(f'New user creation failed: {user.status_code}')
return resp(False)
else:
log.error(f'New user creation failed: {user.status_code}')
return resp(False)
log.debug('User password invalid')
return jsonify(validation)
else:
log.debug('Attempted newUser unauthorized')
return resp(False, code=401)
@@ -176,4 +211,27 @@ def get_token():
return jsonify({'token': token.decode('ascii')})
@app.route('/modifyConfig', methods=['POST'])
@auth.login_required
def modifyConfig():
log.info('Config modification requested')
data = request.get_json()
temp_config = RawConfigParser(comment_prefixes='/',
allow_no_value=True)
temp_config.read(config_path)
for section in data:
if section in temp_config:
for item in data[section]:
if item in temp_config[section]:
temp_config[section][item] = data[section][item]
data[section][item] = True
log.debug(f'{section}/{item} modified')
else:
data[section][item] = False
log.debug(f'{section}/{item} does not exist in config')
with open(config_path, 'w') as config_file:
temp_config.write(config_file)
log.debug('Config written')
return jsonify(data)