Skip to content

Commit

Permalink
Merge pull request #110 from level12/password-policy
Browse files Browse the repository at this point in the history
Allow apps to enforce their own password policies
  • Loading branch information
bladams committed Jun 11, 2020
2 parents 825d32e + df2317b commit 7111c20
Show file tree
Hide file tree
Showing 15 changed files with 693 additions and 171 deletions.
2 changes: 2 additions & 0 deletions keg_auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
KegAuthenticator,
LdapAuthenticator,
TokenRequestLoader,
PasswordPolicy,
PasswordPolicyError,
)
from keg_auth.libs.decorators import requires_permissions, requires_user
from keg_auth.libs.navigation import NavItem, NavURL
Expand Down
41 changes: 39 additions & 2 deletions keg_auth/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,38 @@

from keg_auth.model import get_username_key
from keg_auth.extensions import gettext as _
from keg_auth.libs.authenticators import PasswordPolicyError
from keg_auth.model.entity_registry import RegistryError


class PasswordType(click.ParamType):
name = 'password'

def __init__(self, policy, user):
self.policy = policy
self.user = user

def convert(self, value, param, ctx):
if not isinstance(value, str):
self.fail(_('Password must be a string'), param, ctx)

errors = []
for check in self.policy.password_checks():
try:
check(value, self.user)
except PasswordPolicyError as e:
errors.append(str(e))

if errors:
error_list = '\n'.join('\t\N{BULLET} {}'.format(e) for e in errors)
self.fail(
_('Password does not meet the following restrictions:\n{errs}', errs=error_list),
param,
ctx,
)
return value


def add_cli_to_app(app, cli_group_name, user_args=['email']):

@app.cli.group(cli_group_name)
Expand All @@ -15,13 +44,21 @@ def auth():
@auth.command('set-password', short_help='Set a user\'s password')
@click.argument('username')
def set_user_password(username):
user_ent = app.auth_manager.entity_registry.user_cls
auth_manager = keg.current_app.auth_manager
user_ent = auth_manager.entity_registry.user_cls
user = user_ent.get_by(**{get_username_key(user_ent): username})

if user is None:
click.echo('Unknown user', err=True)
return

password = click.prompt('Password', hide_input=True, confirmation_prompt=True)
password_policy = auth_manager.password_policy_cls()
password = click.prompt(
'Password',
type=PasswordType(password_policy, user),
hide_input=True,
confirmation_prompt=True
)
user.change_password(user.token_generate(), password)

# note: no group attached here. We will apply the arguments and group it below
Expand Down
11 changes: 9 additions & 2 deletions keg_auth/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@

import keg_auth.cli
from keg_auth import model
from keg_auth.libs.authenticators import KegAuthenticator
from keg_auth.libs.authenticators import (
DefaultPasswordPolicy,
KegAuthenticator,
)

DEFAULT_CRYPTO_SCHEMES = ('bcrypt', 'pbkdf2_sha256',)

Expand All @@ -32,7 +35,8 @@ class AuthManager(object):

def __init__(self, mail_manager=None, blueprint='auth', endpoints=None,
cli_group_name=None, grid_cls=None, login_authenticator=KegAuthenticator,
request_loaders=None, permissions=None, entity_registry=None):
request_loaders=None, permissions=None, entity_registry=None,
password_policy_cls=DefaultPasswordPolicy):
"""Set up an auth management extension
Main manager for keg-auth authentication/authorization functions, and provides a central
Expand All @@ -51,10 +55,13 @@ def __init__(self, mail_manager=None, blueprint='auth', endpoints=None,
:param permissions: permission strings defined for the app, which will be synced to the
database on app init. Can be a single string or an iterable
:param entity_registry: EntityRegistry instance on which User, Group, etc. are registered
:param password_policy_cls: A PasswordPolicy class to check password requirements in
forms and CLI
"""
self.mail_manager = mail_manager
self.blueprint_name = blueprint
self.entity_registry = entity_registry
self.password_policy_cls = password_policy_cls
self.endpoints = self.endpoints.copy()
if endpoints:
self.endpoints.update(endpoints)
Expand Down
9 changes: 9 additions & 0 deletions keg_auth/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import arrow
import flask
import keg
from keg_elements.forms import Form, ModelForm, FieldMeta, MultiCheckboxField
from keg_elements.forms.validators import ValidateUnique
from sqlalchemy.sql.functions import coalesce
Expand Down Expand Up @@ -53,6 +54,14 @@ class SetPassword(Form):
])
confirm = PasswordField(_('Confirm Password'))

def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)

auth_manager = keg.current_app.auth_manager
password_policy = auth_manager.password_policy_cls()
self.password.validators = [*self.password.validators, *password_policy.form_validators()]


def get_permission_options():
perm_cls = flask.current_app.auth_manager.entity_registry.permission_cls
Expand Down
Binary file modified keg_auth/i18n/es/LC_MESSAGES/keg_auth.mo
Binary file not shown.
118 changes: 81 additions & 37 deletions keg_auth/i18n/es/LC_MESSAGES/keg_auth.po
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Keg Auth 0.2.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2020-04-09 15:26-0400\n"
"POT-Creation-Date: 2020-06-08 13:29-0400\n"
"PO-Revision-Date: 2018-08-23 13:49-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es\n"
Expand All @@ -16,45 +16,57 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
"Generated-By: Babel 2.8.0\n"

#: keg_auth/cli.py:41
#: keg_auth/cli.py:19
msgid "Password must be a string"
msgstr "La contraseña debe ser una cadena"

#: keg_auth/cli.py:31
msgid ""
"Password does not meet the following restrictions:\n"
"{errs}"
msgstr ""
"La contraseña no cumple las siguientes restricciones:\n"
"{errs}"

#: keg_auth/cli.py:78
msgid "User created."
msgstr "Creado por el usuario."

#: keg_auth/cli.py:44
#: keg_auth/cli.py:81
msgid "Email sent with verification URL."
msgstr "Correo electrónico enviado con la URL de verificación."

#: keg_auth/cli.py:46
#: keg_auth/cli.py:83
msgid "Verification URL: {url}"
msgstr "URL de verificación: {url}"

#: keg_auth/forms.py:24 keg_auth/grids.py:184
#: keg_auth/forms.py:25 keg_auth/grids.py:184
msgid "User ID"
msgstr "Identidad de usuario"

#: keg_auth/forms.py:28 keg_auth/forms.py:43
#: keg_auth/forms.py:29 keg_auth/forms.py:44
msgid "Email"
msgstr ""

#: keg_auth/forms.py:35
#: keg_auth/forms.py:36
msgid "Password"
msgstr "Contraseña"

#: keg_auth/forms.py:50 keg_auth/forms.py:188
#: keg_auth/forms.py:51 keg_auth/forms.py:197
msgid "New Password"
msgstr "Nueva Contraseña"

#: keg_auth/forms.py:52 keg_auth/forms.py:190
#: keg_auth/forms.py:53 keg_auth/forms.py:199
msgid "Passwords must match"
msgstr "Las contraseñas deben coincidir"

#: keg_auth/forms.py:54 keg_auth/forms.py:192
#: keg_auth/forms.py:55 keg_auth/forms.py:201
msgid "Confirm Password"
msgstr "Confirmar Contraseña"

#: keg_auth/forms.py:130
#: keg_auth/forms.py:139
msgid "This field is required."
msgstr "Este campo es requerido."

Expand Down Expand Up @@ -167,11 +179,11 @@ msgstr "Manojos"
msgid "Permissions"
msgstr "Permisos"

#: keg_auth/libs/authenticators.py:122
#: keg_auth/libs/authenticators.py:125
msgid "No user account matches: {}"
msgstr "Ninguna cuenta de usuario coincide: {}"

#: keg_auth/libs/authenticators.py:123
#: keg_auth/libs/authenticators.py:126
msgid ""
"The user account \"{}\" has an unverified email address. Please check "
"your email for a verification link from this website. Or, use the "
Expand All @@ -182,102 +194,134 @@ msgstr ""
"verificación desde este sitio web. O bien, use el enlace \"olvidó la "
"contraseña\" para verificar la cuenta."

#: keg_auth/libs/authenticators.py:128
#: keg_auth/libs/authenticators.py:131
msgid ""
"The user account \"{}\" has been disabled. Please contact this site's "
"administrators for more information."
msgstr ""
"La cuenta de usuario \"{}\" ha sido desactivada. Por favor, póngase en "
"contacto con los administradores de este sitio para más información."

#: keg_auth/libs/authenticators.py:159
#: keg_auth/libs/authenticators.py:162
msgid "Login successful."
msgstr "Inicio de sesión correcto."

#: keg_auth/libs/authenticators.py:191
#: keg_auth/libs/authenticators.py:194
msgid "The form has errors, please see below."
msgstr "El formulario tiene errores, ve a continuación."

#: keg_auth/libs/authenticators.py:331
#: keg_auth/libs/authenticators.py:337
msgid ""
"Authentication token was invalid or expired. Please fill out the form "
"below to get a new token."
msgstr ""
"El token de autenticación no es válido o ha caducado. Complete el "
"formulario a continuación para obtener un nuevo token."

#: keg_auth/libs/authenticators.py:381
#: keg_auth/libs/authenticators.py:390
msgid "Complete Password Reset"
msgstr "Restablecer Contraseña Completa"

#: keg_auth/libs/authenticators.py:382
#: keg_auth/libs/authenticators.py:391
msgid "Change Password"
msgstr "Cambia la Contraseña"

#: keg_auth/libs/authenticators.py:383
#: keg_auth/libs/authenticators.py:392
msgid "Password changed. Please use the new password to login below."
msgstr ""
"Contraseña cambiada. Utilice la nueva contraseña para iniciar sesión a "
"continuación."

#: keg_auth/libs/authenticators.py:400
#: keg_auth/libs/authenticators.py:409
msgid "Too many password reset attempts."
msgstr "Demasiados intentos de restablecimiento de contraseña."

#: keg_auth/libs/authenticators.py:438
#: keg_auth/libs/authenticators.py:447
msgid "Verify Account & Set Password"
msgstr "Verificar Cuenta y Establecer Contraseña"

#: keg_auth/libs/authenticators.py:439
#: keg_auth/libs/authenticators.py:448
msgid "Verify & Set Password"
msgstr "Verificar y Configurar la Contraseña"

#: keg_auth/libs/authenticators.py:440
#: keg_auth/libs/authenticators.py:449
msgid ""
"Account verified & password set. Please use the new password to login "
"below."
msgstr ""
"Cuenta verificada y contraseña establecida. Utilice la nueva contraseña "
"para iniciar sesión a continuación."

#: keg_auth/libs/authenticators.py:449
#: keg_auth/libs/authenticators.py:458
msgid "Log In"
msgstr "Iniciar Sesión"

#: keg_auth/libs/authenticators.py:450
#: keg_auth/libs/authenticators.py:459
msgid "Invalid password."
msgstr "Contraseña invalida."

#: keg_auth/libs/authenticators.py:497
#: keg_auth/libs/authenticators.py:506
msgid "Too many failed login attempts."
msgstr "Demasiados intentos fallidos de inicio de sesión."

#: keg_auth/libs/authenticators.py:557
#: keg_auth/libs/authenticators.py:566
msgid "Initiate Password Reset"
msgstr "Iniciar Restablecimiento de Contraseña"

#: keg_auth/libs/authenticators.py:559
#: keg_auth/libs/authenticators.py:568
msgid "Please check your email for the link to change your password."
msgstr ""
"Por favor revise su correo electrónico para ver el enlace para cambiar su"
" contraseña."

#: keg_auth/libs/authenticators.py:604
#: keg_auth/libs/authenticators.py:613
msgid "Too many failed attempts."
msgstr "Demasiados intentos fallidos."

#: keg_auth/libs/authenticators.py:661
#: keg_auth/libs/authenticators.py:670
msgid "You have been logged out."
msgstr "Has sido desconectado."

#: keg_auth/libs/authenticators.py:756
#: keg_auth/libs/authenticators.py:765
msgid "No KEGAUTH_LDAP_SERVER_URL configured!"
msgstr "¡No se configuró KEGAUTH_LDAP_SERVER_URL!"

#: keg_auth/libs/authenticators.py:760
#: keg_auth/libs/authenticators.py:769
msgid "No KEGAUTH_LDAP_DN_FORMAT configured!"
msgstr "¡No se configuró KEGAUTH_LDAP_DN_FORMAT!"

#: keg_auth/libs/authenticators.py:1017
msgid "lowercase letter"
msgstr "letra minúscula"

#: keg_auth/libs/authenticators.py:1018
msgid "uppercase letter"
msgstr "letra mayúscula"

#: keg_auth/libs/authenticators.py:1019
msgid "number"
msgstr "número"

#: keg_auth/libs/authenticators.py:1020
msgid "symbol"
msgstr "símbolo"

#: keg_auth/libs/authenticators.py:1038
msgid "Password must be at least {min_length} characters long"
msgstr "La contraseña debe tener al menos {min_length} caracteres de longitud"

#: keg_auth/libs/authenticators.py:1058
msgid "Password must include a {type}"
msgstr "La contraseña debe incluir un {type}"

#: keg_auth/libs/authenticators.py:1061
msgid "Password must include at least {required} of {first} and/or {last}"
msgstr "La contraseña debe incluir al menos {required} de {first} y / o {last}"

#: keg_auth/libs/authenticators.py:1082
msgid "Password may not contain username"
msgstr "La contraseña no puede contener el nombre de usuario"

#: keg_auth/libs/decorators.py:75
#, python-format
msgid ""
Expand Down Expand Up @@ -315,16 +359,16 @@ msgstr "Ninguna entidad registrada para {}"
msgid "At least one permission or condition is required"
msgstr "Se requiere al menos un permiso o condición"

#: keg_auth/templates/keg_auth/crud-list.html:18
#: keg_auth/templates/keg_auth/crud-list.html:15
#, python-format
msgid "Create %(name)s"
msgstr "Crear %(name)s"

#: keg_auth/templates/keg_auth/forgot-password.html:9
#: keg_auth/templates/keg_auth/forgot-password.html:7
msgid "Send Reset Email"
msgstr "Enviar Restablecer correo electrónico"

#: keg_auth/templates/keg_auth/login.html:10
#: keg_auth/templates/keg_auth/login.html:8
msgid "I forgot my password"
msgstr "Olvidé mi contraseña"

Expand Down

0 comments on commit 7111c20

Please sign in to comment.