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) message = _( 'Password does not meet the following restrictions:\n{errs}', errs=error_list) # because we hide the input, click also hides the error message, so echo manually click.echo(message) self.fail( message, param, ctx, ) return value
class Bundle(CrudView): url = '/bundles' object_name = _('Bundle') object_name_plural = _('Bundles') form_cls = staticmethod(forms.bundle_form) def create_form(self, obj): form_cls = self.form_cls(endpoint=self.endpoint_for_action('edit')) return form_cls(obj=obj) @property def orm_cls(self): return flask.current_app.auth_manager.entity_registry.bundle_cls @property def grid_cls(self): return grids.make_bundle_grid( edit_endpoint=self.endpoint_for_action('edit'), edit_permission=self.permissions['edit'], delete_endpoint=self.endpoint_for_action('delete'), delete_permission=self.permissions['delete'] ) def update_obj(self, obj, form): obj = obj or self.add_orm_obj() form.populate_obj(obj) obj.permissions = form.get_selected_permissions() return obj
def check_character_set(self, pw: str, user): """ Raises PasswordPolicyError if a password does not contain at least one character from at least `required_at_least_char_types` of the alphabets in `required_char_sets`. :param pw: password to check :param user: user entity """ missing = [] for name, alphabet in self.required_char_types: if not set(pw) & set(alphabet): missing.append(name) if len(missing) > len(self.required_char_types) - self.required_min_char_types: if len(self.required_char_types) == 1: message = _('Password must include a {type}', type=self.required_char_types[0].name) else: first_part = ', '.join(str(t.name) for t in self.required_char_types[:-1]) message = _( 'Password must include at least {required} of {first} and/or {last}', required=self.required_min_char_types, first=first_part, last=self.required_char_types[-1].name ) raise PasswordPolicyError(message)
class Group(CrudView): """Default Group CRUD view. Uses auth-manage permission for all targets.""" url = '/groups' object_name = _('Group') object_name_plural = _('Groups') form_cls = staticmethod(forms.group_form) def create_form(self, obj): form_cls = self.form_cls(endpoint=self.endpoint_for_action('edit')) return form_cls(obj=obj) @property def orm_cls(self): return flask.current_app.auth_manager.entity_registry.group_cls @property def grid_cls(self): return grids.make_group_grid( edit_endpoint=self.endpoint_for_action('edit'), edit_permission=self.permissions['edit'], delete_endpoint=self.endpoint_for_action('delete'), delete_permission=self.permissions['delete'] ) def update_obj(self, obj, form): obj = obj or self.add_orm_obj() form.populate_obj(obj) obj.permissions = form.get_selected_permissions() obj.bundles = form.get_selected_bundles() return obj
def add_edit(self, meth, obj=None): """Handle form-related requests for add/edit. Form instance comes from `create_form`. Valid form updates the object via `update_obj`. If post successful, returns result of `on_add_edit_success`. If post failure, runs `on_add_edit_failure` and renders the form via `render_form`. If get, renders the form via `render_form`. """ form = self.create_form(obj) if form is None: raise Exception('create_form returned None instead of a form instance') if meth == 'POST': if form.validate(): result = self.update_obj(obj, form) db.session.commit() if result: return self.on_add_edit_success(result, obj is not None) else: self.on_add_edit_failure(obj, obj is not None) return self.render_form( obj=obj, action=_('Edit') if obj else _('Create'), action_button_text=( _('Save Changes') if obj else _('Create {name}').format(name=self.object_name) ), form=form )
class UserResponderMixin(object): flash_invalid_user = _('No user account matches: {}'), 'error' flash_unverified_user = _( 'The user account "{}" has an unverified email address. Please check' ' your email for a verification link from this website. Or, use the "forgot' ' password" link to verify the account.' ), 'error' flash_disabled_user = _( 'The user account "{}" has been disabled. Please contact this' ' site\'s administrators for more information.' ), 'error' def on_inactive_user(self, user): if flask.current_app.auth_manager.mail_manager and not user.is_verified: if self.flash_unverified_user: message, category = self.flash_unverified_user flash(message.format(user.email), category) if not user.is_enabled: self.on_disabled_user(user) def on_invalid_user(self, username): if self.flash_invalid_user: message, category = self.flash_invalid_user flash(message.format(username), category) def on_disabled_user(self, user): if self.flash_disabled_user: message, category = self.flash_disabled_user flash(message.format(user.display_value), category)
class User(grid_cls): action_column_cls( '', user_cls.id, edit_endpoint=edit_endpoint, delete_endpoint=delete_endpoint, edit_permission_for=lambda _: edit_permission, delete_permission_for=lambda _: delete_permission ) webgrid.Column(_('User ID'), user_cls.username, filters.TextFilter) if flask.current_app.auth_manager.mail_manager and hasattr(user_cls, 'is_verified'): webgrid.YesNoColumn(_('Verified'), user_cls.is_verified, filters.YesNoFilter) webgrid.YesNoColumn(_('Superuser'), user_cls.is_superuser, filters.YesNoFilter) if ( flask.current_app.auth_manager.mail_manager and hasattr(user_cls, 'is_verified') and resend_verification_endpoint is not None and flask.current_app.config['KEGAUTH_EMAIL_OPS_ENABLED'] ): ResendVerificationColumn(_('Resend Verification'), resend_verification_endpoint) def query_prep(self, query, has_sort, has_filters): if not has_sort: query = query.order_by(user_cls.username) return query
def on_add_edit_success(self, entity, is_edit): self.flash_success( _('modified') if is_edit else _('created') ) return flask.redirect(self.list_url_with_session)
def page_title(self, action): if action == _('Create'): return _('Create {name}').format(name=self.object_name) if action == _('Edit'): return _('Edit {name}').format(name=self.object_name) return self.object_name_plural
def on_add_edit_success(self, entity, is_edit): """Flash an add/edit success message, and redirect to list view.""" self.flash_success( _('modified') if is_edit else _('created') ) return flask.redirect(self.list_url_with_session)
class VerifyAccountViewResponder(PasswordSetterResponderBase): """ Responder for verifying users via email token for keg-auth logins""" url = '/verify-account/<int:user_id>/<token>' page_title = _('Verify Account & Set Password') submit_button_text = _('Verify & Set Password') flash_success = _('Account verified & password set. Please use the new password to login' ' below.'), 'success' on_success_endpoint = 'after-verify-account'
class Permission(grid_cls): webgrid.Column(_('Name'), permission_cls.token, filters.TextFilter) webgrid.Column(_('Description'), permission_cls.description, filters.TextFilter) def query_prep(self, query, has_sort, has_filters): if not has_sort: query = query.order_by(permission_cls.token) return query
class ResetPasswordViewResponder(AttemptLimitMixin, PasswordSetterResponderBase): """ Responder for resetting passwords via token on keg-auth logins""" url = '/reset-password/<int:user_id>/<token>' page_title = _('Complete Password Reset') submit_button_text = _('Change Password') flash_success = _('Password changed. Please use the new password to login below.'), 'success' on_success_endpoint = 'after-reset' def on_form_valid(self, form): if self.should_limit_attempts(): if self.is_attempt_blocked(get_username(self.user)): self.log_attempt(get_username(self.user), success=False, is_during_lockout=True) self.on_attempt_blocked() return self.log_attempt(get_username(self.user), success=True) new_password = form.password.data self.user.change_password(self.token, new_password) self.flash_and_redirect(self.flash_success, self.on_success_endpoint) def get_flash_attempts_limit_reached(self): return _('Too many password reset attempts.'), 'error' def get_attempt_limit(self): return flask.current_app.config.get('KEGAUTH_RESET_ATTEMPT_LIMIT') def get_attempt_timespan(self): return flask.current_app.config.get('KEGAUTH_RESET_ATTEMPT_TIMESPAN') def get_attempt_lockout_period(self): return flask.current_app.config.get('KEGAUTH_RESET_ATTEMPT_LOCKOUT') def get_attempt_type(self): return 'reset' def get_last_limiting_attempt(self, username): return self.attempt_ent.query.filter_by( is_during_lockout=False, attempt_type=self.get_attempt_type(), ).filter( self.get_input_filters(username) ).order_by( self.attempt_ent.datetime_utc.desc(), ).first() def get_limiting_attempt_count(self, before_time, username): timespan_start = before_time + timedelta(seconds=-self.get_attempt_timespan()) return self.attempt_ent.query.filter( self.get_input_filters(username), self.attempt_ent.is_during_lockout == sa.false(), self.attempt_ent.datetime_utc > timespan_start, self.attempt_ent.datetime_utc <= before_time, self.attempt_ent.attempt_type == self.get_attempt_type(), ).count()
def get_entity_cls(self, type): attr = self._type_to_attr(type) try: cls = getattr(self, attr) except AttributeError: raise RegistryError( _('Attempting to register unknown type {}').format(type)) if cls is None: raise RegistryError(_('No entity registered for {}').format(type)) return cls
def page_title(self, action): """Generates a heading title based on the page action. `action` should be a string. Values "Create" and "Edit" are handled, with a fall-through to return `object_name_plural` (for the list case). """ if action == _('Create'): return _('Create {name}').format(name=self.object_name) if action == _('Edit'): return _('Edit {name}').format(name=self.object_name) return self.object_name_plural
class User(PermissionsMixin, BundlesMixin, GroupsMixin, ModelForm): disabled_utc = DateField('Disable Date', [validators.Optional()], filters=[filter_disabled_utc], render_kw={'type': 'date'}) class Meta: model = user_cls only = _fields class FieldsMeta: is_enabled = FieldMeta('Enabled') is_superuser = FieldMeta('Superuser') __default__ = FieldMeta field_order = tuple(_fields + ['group_ids', 'bundle_ids', 'permission_ids']) setattr( FieldsMeta, username_key, FieldMeta(extra_validators=[ validators.data_required(), ValidateUnique(html_link) ])) if isinstance( flask.current_app.auth_manager.entity_registry.user_cls. username.type, EmailType): getattr(FieldsMeta, username_key).widget = EmailInput() if not config.get('KEGAUTH_EMAIL_OPS_ENABLED'): reset_password = PasswordField( _('New Password'), validators=[ _ValidatePasswordRequired(), validators.EqualTo('confirm', message=_('Passwords must match')) ]) confirm = PasswordField(_('Confirm Password')) field_order = field_order + ('reset_password', 'confirm') def get_object_by_field(self, field): return user_cls.get_by(username=field.data) @property def obj(self): return self._obj def __iter__(self): order = ('csrf_token', ) + self.field_order return (getattr(self, field_id) for field_id in order)
def get(self): grid = self.grid_cls() grid.apply_qs_args() if grid.export_to: return grid.export_as_response() return flask.render_template( self.grid_template, page_title=_('Permissions'), page_heading=_('Permissions'), grid=grid, **self.template_args, )
def register_entity(self, type, cls): attr = self._type_to_attr(type) try: if getattr(self, attr) is not None: raise RegistryError( _('Entity class already registered for {}').format(type)) except AttributeError: raise RegistryError( _('Attempting to register unknown type {}').format(type)) if not inspect.isclass(cls): raise RegistryError(_('Entity must be a class')) setattr(self, attr, cls) return cls
def verify_password(self, user, password): """ Check the given username/password combination at the application's configured LDAP server. Returns `True` if the user authentication is successful, `False` otherwise. NOTE: By request, authentication can be bypassed by setting the KEGAUTH_LDAP_TEST_MODE configuration setting to `True`. When set, all authentication attempts will succeed! :param user: :param password: :return: """ if flask.current_app.config.get('KEGAUTH_LDAP_TEST_MODE', False): return True ldap_url = flask.current_app.config.get('KEGAUTH_LDAP_SERVER_URL') if not ldap_url: raise Exception(_('No KEGAUTH_LDAP_SERVER_URL configured!')) ldap_dn_format = flask.current_app.config.get('KEGAUTH_LDAP_DN_FORMAT') if not ldap_dn_format: raise Exception(_('No KEGAUTH_LDAP_DN_FORMAT configured!')) def ldap_bind(server_url): session = ldap.initialize(server_url) try: dn = ldap_dn_format.format(user.username) result = session.simple_bind_s(dn, password) del session return bool( result and len(result) and result[0] == ldap.RES_BIND ) except (ldap.INVALID_CREDENTIALS, ldap.INVALID_DN_SYNTAX): return False if isinstance(ldap_url, str): return ldap_bind(ldap_url) # We have a list of servers. for server_url in ldap_url: if ldap_bind(server_url): return True return False
def __init__(self, *args, nav_group=None, icon_class=None): self.label = None if len(args) and (isinstance(args[0], str) or is_lazy_string(args[0])): self.label = args[0] args = args[1:] self.route = None self.sub_nodes = None self.nav_group = nav_group self.icon_class = icon_class # cache permission-related items self._is_permitted = {} self._permitted_sub_nodes = {} if len(args) == 0: raise Exception(_('must provide a NavURL or a list of NavItems')) if isinstance(args[0], NavURL): self.route = args[0] if len(args) > 1: args = args[1:] else: return if len(args): self.sub_nodes = args if not self.nav_group: self.nav_group = simplify_string(self.label or '__root__')
def flash_success(self, verb): """Add a flask flash message for success with the given `verb`.""" # i18n: this may require reworking in order to support proper # sentence structures... flash(_('Successfully {verb} {object}').format( verb=verb, object=self.object_name), 'success' )
class LoginResponderMixin(UserResponderMixin): """ Wrap user authentication view-layer logic Flash messages, what to do when a user has been authenticated (by whatever method the parent authenticator uses), redirects to a safe URL after login, etc. """ url = '/login' flash_success = _('Login successful.'), 'success' @staticmethod def is_safe_url(target): """Returns `True` if the target is a valid URL for redirect""" # from http://flask.pocoo.org/snippets/62/ ref_url = urlparse(flask.request.host_url) test_url = urlparse(urljoin(flask.request.host_url, target)) return ( test_url.scheme in ('http', 'https') and ref_url.netloc == test_url.netloc ) def on_success(self, user): flask_login.login_user(user) if self.flash_success: flash(*self.flash_success) # support Flask-Login "next" parameter next_parameter = flask.request.values.get('next') if flask.current_app.config.get('USE_SESSION_FOR_NEXT'): next_parameter = flask.session.get('next') if next_parameter and self.is_safe_url(next_parameter): redirect_to = next_parameter else: redirect_to = flask.current_app.auth_manager.url_for('after-login') return flask.redirect(redirect_to)
def render_grid(self): """Renders the grid template. Grid instance comes from `make_grid`. Grid instance may be customized via `post_args_grid_setup`. If grid is set to export, give that response or handle the limit exceeded error. Otherwise, render `grid_template` with `grid_template_args`. """ grid = self.make_grid() grid = self.post_args_grid_setup(grid) if grid.session_on and flask.request.method.lower() == 'post': return flask.redirect(self.list_url_with_session) if grid.export_to: import webgrid try: return grid.export_as_response() except webgrid.renderers.RenderLimitExceeded: self.on_render_limit_exceeded(grid) # args added with self.assign should be passed through here template_args = self.grid_template_args(dict(self.template_args, **{ 'add_url': self.add_url_with_session(grid.session_key), 'page_title': self.page_title(_('list')), 'page_heading': self.grid_page_heading, 'object_name': self.object_name, 'grid': grid, })) return flask.render_template(self.grid_template, **template_args)
class ForgotPassword(Form): """Returns a form to capture email for password reset.""" email = StringField(_(u'Email'), validators=[ validators.DataRequired(), validators.Email(), ])
def on_delete_failure(self): flash( _('Unable to delete {name}. It may be referenced by other items.').format( name=self.object_name ), 'warning' ) return flask.redirect(self.list_url_with_session)
class Login(Form): next = HiddenField() login_id = StringField(login_id_label, validators=login_id_validators) password = PasswordField(_('Password'), validators=[ validators.DataRequired(), ])
class LogoutViewResponder(ViewResponder): url = '/logout' flash_success = _('You have been logged out.'), 'success' def get(self): flask_login.logout_user() if self.flash_success: flash(*self.flash_success) redirect_to = flask.current_app.auth_manager.url_for('after-logout') flask.abort(flask.redirect(redirect_to))
def _create_user(as_superuser, no_mail, **kwargs): """ Create a user. Create a user record with the given required args and (if a mail manager is configured) send them an email with URL and token to set their password. Any EXTRA_ARGS will be sent to the auth manager for processing. """ auth_manager = keg.current_app.auth_manager user = auth_manager.create_user_cli(is_superuser=as_superuser, mail_enabled=not no_mail, **kwargs) click.echo(_('User created.')) if auth_manager.mail_manager: if not no_mail: click.echo(_('Email sent with verification URL.')) verification_url = auth_manager.mail_manager.verify_account_url( user) click.echo( _('Verification URL: {url}').format(url=verification_url))
class SetPassword(Form): password = PasswordField(_('New Password'), validators=[ validators.DataRequired(), validators.EqualTo( 'confirm', message=_('Passwords must match')) ]) 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 check_length(self, pw: str, user): """ Raises PasswordPolicyError if a password is not at least min_length characters long. :param pw: password to check :param user: user entity """ if len(pw) < self.min_length: raise PasswordPolicyError(_( 'Password must be at least {min_length} characters long', min_length=self.min_length ))