예제 #1
0
파일: cli.py 프로젝트: level12/keg-auth
    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
예제 #2
0
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
예제 #3
0
    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)
예제 #4
0
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
예제 #5
0
    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
        )
예제 #6
0
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)
예제 #7
0
    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
예제 #8
0
 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)
예제 #9
0
    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
예제 #10
0
 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)
예제 #11
0
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'
예제 #12
0
    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
예제 #13
0
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()
예제 #14
0
    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
예제 #15
0
    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
예제 #16
0
    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)
예제 #17
0
    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,
        )
예제 #18
0
    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
예제 #19
0
    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
예제 #20
0
    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__')
예제 #21
0
 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'
     )
예제 #22
0
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)
예제 #23
0
    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)
예제 #24
0
class ForgotPassword(Form):
    """Returns a form to capture email for password reset."""
    email = StringField(_(u'Email'),
                        validators=[
                            validators.DataRequired(),
                            validators.Email(),
                        ])
예제 #25
0
 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)
예제 #26
0
    class Login(Form):
        next = HiddenField()

        login_id = StringField(login_id_label, validators=login_id_validators)
        password = PasswordField(_('Password'),
                                 validators=[
                                     validators.DataRequired(),
                                 ])
예제 #27
0
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))
예제 #28
0
파일: cli.py 프로젝트: level12/keg-auth
    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))
예제 #29
0
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()
        ]
예제 #30
0
 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
         ))