Products.LoginLockout


NameProducts.LoginLockout JSON
Version 0.5.0 PyPI version JSON
download
home_pagehttps://github.com/collective/Products.LoginLockout
SummaryThis Pluggable Authentication Service (PAS) plugin will lock a login after a predetermined number of incorrect attempts. Once locked, the user will be shown a page that tells them to contact their administrator to unlock.
upload_time2024-03-08 07:44:32
maintainer
docs_urlNone
authorDylan Jay
requires_python
licenseGPL
keywords pas plugins zope login lockout plone security
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage
            LoginLockout
============

.. image:: https://github.com/collective/Products.LoginLockout/workflows/CI/badge.svg
  :target: https://github.com/collective/Products.LoginLockout/actions

.. image:: https://coveralls.io/repos/collective/Products.LoginLockout/badge.svg?branch=master&service=github
  :target: https://coveralls.io/github/collective/Products.LoginLockout?branch=master


This Pluggable Authentication Service (PAS) plugin will lock a
login after a predetermined number of incorrect attempts. Once
locked, the user will be shown a page that tells them to contact
their administrator to unlock.


Requires:
---------

- PluggableAuthService and its dependencies

- (optional) PlonePAS and its dependencies

- (optional) Plone 4.1.x-6.0.x

.. image:: http://github-actions.40ants.com/collective/Products.LoginLockout/matrix.svg
   :target: https://github.com/collective/Products.LoginLockout


Features
--------

- Configurable number of allowed incorrect attempts before lockout
- Account will be usable again after a configurable amount of time
  (the "reset period")
  If the first login attempt after the reset period is invalid, the
  invalid login counter is set to 1.
- The user is presented with a message saying that the account was locked,
  and for how long.
  (It doesn't show remaining time, just the total lockout time.)
- You can restrict users to come from certain IP networks. You don't have to
  use the incorrect login attempts to use this feature.


Configuration
-------------

**NOTE** *If upgrading from 0.4.0 you will need run the upgrade or manually reset the PAS plugin order as below as this has changed.*

You can use this plugin with Zope without Plone, or with Plone. When using it with Plone you will configure it via the
Plone registry (plone 5+) or via portal_properties if plone 4.

Go to the Plone Control Panel -> LoginLockout Settings , there you can changes these defaults:

    >>> admin_browser = make_admin_browser('/')
    >>> admin_browser.getLink('Site Setup').click()
    >>> admin_browser.getLink('LoginLockout').click()
    >>> admin_browser.getLink('Lockout Settings').click()

- allowed incorrect attempts: 3
- reset period: 24 hours
- whitelist_ips: [] # any origin IP is allowed
- Fake Client IP: false

    >>> admin_browser.getControl("Max Attempts").value
    '3'
    >>> admin_browser.getControl("Reset Period (hours)").value
    '24.0'
    >>> admin_browser.getControl('Lock logins to IP Ranges').value
    ''
    >>> admin_browser.getControl('Fake Client IP').selected
    False


Let's ensure that the settings actually change

    >>> admin_browser.getControl('Fake Client IP').selected = True
    >>> get_loginlockout_settings().fake_client_ip
    False
    >>> admin_browser.getControl('Save').click()
    >>> 'Changes saved.' in admin_browser.contents
    True
    >>> get_loginlockout_settings().fake_client_ip
    True



Details
-------

LoginLockout can be used as a Plone plugin or with zope and PAS alone.
First we'll show you how it works with Plone.


To Install
----------

Install into Plone via Add/Remove Products. If you are installing into zope without
plone then you will need to follow these manual install steps.

This will install and activate a two PAS plugins.

Manual Installation
-------------------

This plugin needs to be installed in two places, the instance PAS where logins
occur and the root acl_users.

 1. Place the Product directory 'LoginLockout' in your 'Products/'
 directory. Restart Zope.

 2. In your instance PAS 'acl_users', select 'LoginLockout' from the add
 list.  Give it an id and title, and push the add button.

 3. Enable the 'Authentication', and the 'Update Credentials'
 plugin interfaces in the after-add screen.


 4. Repeat the above for your root PAS but as a plugin to

    -  Anonymoususerfactory


   and ensure LoginLockout is the first Anonymoususerfactory plugin

Steps 2 through 4 below will be done for you by the Plone installer.

That's it! Test it out.


Plone LoginLockout PAS Plugin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

It's very important the plugin is the *first* Authentication plugin in the activated plugins list.
This ensures we prevent a person attempting to make a login into a locked account and display a status message.
This also collects the username and login and will prevent a login should it be locked.

   >>> plone_pas = portal.acl_users.plugins
   >>> IAuthenticationPlugin = plone_pas._getInterfaceFromName('IAuthenticationPlugin')
   >>> plone_pas.listPlugins(IAuthenticationPlugin)
   [('login_lockout_plugin', <LoginLockout at /plone/acl_users/login_lockout_plugin>)...]


and a ICredentialsUpdatePlugin. This records when a login was successful to reset attempt data.


   >>> ICredentialsUpdatePlugin = plone_pas._getInterfaceFromName('ICredentialsUpdatePlugin')
   >>> 'login_lockout_plugin' in [p[0] for p in plone_pas.listPlugins(ICredentialsUpdatePlugin)]
   True


Root Zope LoginLockout PAS Plugin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

It will also install a plugin at the root of the zope instance.

It's important this is also the *first* IAnonymousUserFactoryPlugin. On a normal Zope instance it will be the only one.
This ensures it collects data on unsuccessful attempted logins.

   >>> root_pas = portal.getPhysicalRoot().acl_users.plugins
   >>> IAnonymousUserFactoryPlugin = plone_pas._getInterfaceFromName('IAnonymousUserFactoryPlugin')
   >>> root_pas.listPlugins(IAnonymousUserFactoryPlugin)
   [('login_lockout_plugin', <LoginLockout at /acl_users/login_lockout_plugin>)]



Lockout on incorrect password attempts
--------------------------------------

First login as manager

Now we'll open up a new browser and attempt to login::

    >>> anon_browser = make_anon_browser('/login_form')
    >>> anon_browser.getControl('Login Name').value = user_id
    >>> anon_browser.getControl('Password').value = user_password
    >>> anon_browser.getControl('Log in').click()
    >>> 'Login failed' in anon_browser.contents
    False
    >>> print(anon_browser.contents)
    <BLANKLINE>
    ...You are now logged in...

    >>> anon_browser.open(portal.absolute_url()+'/logout')


Let's try again with another password::

    >>> anon_browser = make_anon_browser('/login_form')
    >>> anon_browser.getControl('Login Name').value = user_id
    >>> anon_browser.getControl('Password').value = 'notpassword'
    >>> anon_browser.getControl('Log in').click()
    >>> print(anon_browser.contents)
    <BLANKLINE>
    ...Login failed...
    >>> print(anon_browser.contents)
    <BLANKLINE>
    ...You have 2 attempts left before this account is locked...


this incorrect attempt  will show up in the log


We've installed a Control panel to monitor the login attempts

    >>> admin_browser = make_admin_browser('/loginlockout_settings')
    >>> print(admin_browser.contents)
    <BLANKLINE>
    ...<td>test-user</td>...
    ...<td>1</td>...



If we try twice more we will be locked out::

    >>> anon_browser = make_anon_browser('/login_form')
    >>> anon_browser.getControl('Login Name').value = user_id
    >>> anon_browser.getControl('Password').value = 'notpassword2'
    >>> anon_browser.getControl('Log in').click()
    >>> 'Login failed' in  anon_browser.contents
    True
    >>> print(anon_browser.contents)
    <BLANKLINE>
    ...You have 1 attempts left before this account is locked...
    >>> anon_browser.getControl('Login Name').value = user_id
    >>> anon_browser.getControl('Password').value = 'notpassword3'
    >>> anon_browser.getControl('Log in').click()
    >>> 'Login failed' in  anon_browser.contents
    True
    >>> 'attempts left' not in anon_browser.contents
    True

    >>> print(anon_browser.contents)
    <...
    ...This account has now been locked for security purposes...


Now even the correct password won't work::

    >>> anon_browser = make_anon_browser('/login_form')
    >>> anon_browser.getControl('Login Name').value = user_id
    >>> anon_browser.getControl('Password').value = user_password
    >>> anon_browser.getControl('Log in').click()

    Not logged in
    >>> print(anon_browser.contents)
    <...
    ...This account has now been locked for security purposes...
    ...

    >>> "now logged in" not in anon_browser.contents
    True

    >>> anon_browser.getLink("Home").click()
    >>> anon_browser.getLink('Log in')
    <Link...>



The administrator can reset this persons account::

    >>> admin_browser = make_admin_browser('/loginlockout_settings')
    >>> print(admin_browser.contents)
    <BLANKLINE>
    ...<td>test-user</td>...
    ...<td>3</td>...
    >>> admin_browser.getControl(name='reset_nonploneusers:list').value = ['test-user']
    >>> admin_browser.getControl('Reset selected accounts').click()
    >>> print(admin_browser.contents)
    <BLANKLINE>
    ...Accounts were reset for these login names: test-user...

and now they can log in again::

    >>> anon_browser = make_anon_browser('/login_form')
    >>> anon_browser.getControl('Login Name').value = user_id
    >>> anon_browser.getControl('Password').value = user_password
    >>> anon_browser.getControl('Log in').click()
    >>> print(anon_browser.contents)
    <BLANKLINE>
    ...You are now logged in...




IP Lockdown
-----------

You can optionally ensure logins are only possible for certain IP address ranges.

By default IP Locking is disabled.

NOTE: If you are using Zope behind a proxy then you must enable X-Forward-For headers on
each proxy otherwise this plugin will incorrectly use REMOTE_ADDR which will be a local IP.

To enable this go into the ZMI and enter the ranges in the whitelist_ips property::

    >>> config_property( whitelist_ips = u'10.1.1.1' )

If there are proxies infront of zope you will have to ensure they set the ```X-Forwarded-For``` header.
Note only the first forwarded IP will be used.::

    >>> anon_browser = make_anon_browser('/login_form')
    >>> anon_browser.addHeader('X-Forwarded-For', '10.1.1.1, 192.168.1.1')
    >>> anon_browser.getControl('Login Name').value = user_id
    >>> anon_browser.getControl('Password').value = user_password
    >>> anon_browser.getControl('Log in').click()
    >>> print(anon_browser.contents)
    <BLANKLINE>
    ...You are now logged in...

    >>> anon_browser.open(portal.absolute_url()+'/logout')

If not from a valid IP then the login will fail::

    >>> anon_browser = make_anon_browser('/login_form')
    >>> anon_browser.addHeader('X-Forwarded-For', '2.2.2.2')

    >>> anon_browser.open(portal.absolute_url()+'/login_form')
    >>> anon_browser.getControl('Login Name').value = user_id
    >>> anon_browser.getControl('Password').value = user_password
    >>> anon_browser.getControl('Log in').click()
    >>> print(anon_browser.contents)
    <BLANKLINE>
    ...Login currently unavailable...
    >>> anon_browser.getLink('Log in')
    <Link text='Log in'...>


Basic Auth will works with the right IP::

    >>> anon_browser = make_anon_browser()
    >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (user_id,user_password))
    >>> anon_browser.addHeader('X-Forwarded-For', '10.1.1.1')

    >>> anon_browser.open(portal.absolute_url())
    >>> anon_browser.getLink('Log out')
    <Link text='Log out'...>


and basic auth fails with the wrong IP::

    >>> anon_browser = make_anon_browser()
    >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (user_id,user_password))
    >>> anon_browser.addHeader('X-Forwarded-For', '2.2.2.2')

    >>> anon_browser.open(portal.absolute_url())
    >>> print(anon_browser.contents)
    <BLANKLINE>
    ...Login currently unavailable...
    >>> anon_browser.getLink('Log in')
    <Link text='Log in'...>


We can still use a root login at the root::

    >>> anon_browser = make_anon_browser()
    >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (base_id, base_password))
    >>> anon_browser.addHeader('X-Forwarded-For', '2.2.2.2')

    >>> anon_browser.open(portal.absolute_url()+'/../manage_main')
    >>> print(anon_browser.contents)
    <BLANKLINE>
    ...manage_workspace...

But we can't get into the plone site with a root id any more::

    >>> anon_browser.open(portal.absolute_url()+'/manage_main')
    Traceback (most recent call last):
    ...
    Unauthorized: You are not authorized to access this resource.


You can also set IP ranges e.g.::

    >>> config_property( whitelist_ips = u"""10.1.1.1
    ... 10.1.0.0/16 # range 1
    ... 2.2.0.0/16 # range 2
    ... """)

    >>> anon_browser = make_anon_browser('/login_form')
    >>> anon_browser.addHeader('X-Forwarded-For', '2.2.2.2')
    >>> anon_browser.getControl('Login Name').value = user_id
    >>> anon_browser.getControl('Password').value = user_password
    >>> anon_browser.getControl('Log in').click()
    >>> print(anon_browser.contents)
    <BLANKLINE>
    ...You are now logged in...

    >>> anon_browser.open(portal.absolute_url()+'/logout')

You can also set a env variable LOGINLOCKOUT_IP_WHITELIST which is merged with the config.
This allows those with filesystem access a way to get in if they have set their config wrong.
It also allows a set of IP ranges to be set for any site in a Plone multisite setup as long
as the site has loginlockout installed.::

    >>> anon_browser = make_anon_browser('/login_form')
    >>> anon_browser.getLink('Log in')
    <Link text='Log in'...

    >>> import os; os.environ["LOGINLOCKOUT_IP_WHITELIST"] = "3.3.3.3"

    >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (user_id,user_password))
    >>> anon_browser.addHeader('X-Forwarded-For', '3.3.3.3')

    >>> anon_browser.open(portal.absolute_url())
    >>> anon_browser.getLink('Log out')
    <Link text='Log out'...>


Note that you still have to have the IP lockout config set otherwise logins are allowed from anywhere
even with the env variable set::

    >>> config_property( whitelist_ips = u"""
    ... """)
    >>> anon_browser = make_anon_browser()
    >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (user_id,user_password))
    >>> anon_browser.addHeader('X-Forwarded-For', '4.4.4.4')

    >>> anon_browser.open(portal.absolute_url())
    >>> anon_browser.getLink('Log out')
    <Link text='Log out'...>


    >>> del os.environ["LOGINLOCKOUT_IP_WHITELIST"]


If you are unsure of what is being detected as your current Client IP you can see it in
the control panel::

    >>> admin_browser = make_admin_browser('/')
    >>> admin_browser.addHeader('X-Forwarded-For', '10.1.1.1, 192.168.1.1')

    >>> admin_browser.getLink('Site Setup').click()
    >>> admin_browser.getLink('LoginLockout').click()
    >>> print(admin_browser.contents)
    <BLANKLINE>
    ...Current detected Client IP: <span>10.1.1.1</span>...


Login History
-------------

It is also possible to view a history of successful logins for a particular user. Note this is the user id rather
than user login and they can be different. User ``test_user_1_`` had 4 successful logins.::

    >>> admin_browser = make_admin_browser('/loginlockout_settings')
    >>> admin_browser.getLink('Login history').click()
    >>> admin_browser.getControl('Username pattern').value = 'test_user_1_'
    >>> admin_browser.getControl('Search records').click()
    >>> print(admin_browser.contents)
    <BLANKLINE>
    ...
                        <td valign="top">test_user_1_</td>
                        <td valign="top">
                            <ul>
                                <li>
                                    ...
                                    ()
                                </li>
                                <li>
                                    ...
                                    ()
                                </li>
                                <li>
                                    ...
                                    (10.1.1.1)
                                </li>
                                <li>
                                    ...
                                    (2.2.2.2)
                                </li>
                            </ul>
    ...



Password Reset History
----------------------

When a user changes their password::

    >>> anon_browser = make_anon_browser('/login_form')
    >>> anon_browser.getControl('Login Name').value = user_id
    >>> anon_browser.getControl('Password').value = user_password
    >>> anon_browser.getControl('Log in').click()

    >>> anon_browser.getLink("Preferences").click()
    >>> anon_browser.getLink("Password").click()
    >>> anon_browser.getControl('Current password').value = user_password
    >>> anon_browser.getControl('New password').value = '12345678'
    >>> anon_browser.getControl('Confirm password').value = '12345678'
    >>> anon_browser.getControl('Change Password').click()
    >>> print(anon_browser.contents)
    <...
    ...Password changed... 
    ...

This changed the password::

    >>> anon_browser = make_anon_browser('/login_form')
    >>> anon_browser.getControl('Login Name').value = user_id
    >>> anon_browser.getControl('Password').value = '12345678'
    >>> anon_browser.getControl('Log in').click()
    >>> anon_browser.getLink("Preferences").click()

The the administrators can see the password was changed::

    >>> admin_browser = make_admin_browser('/loginlockout_settings')
    >>> admin_browser.getLink('History password changes').click()
    >>> print(admin_browser.contents)
    <...
    ...
            <tr class="even">
                <td>test_user_1_</td>
                <td>...</td>
            </tr>
    ...

Other support
--------------

Root users can also be locked out and with basic authentication too::

    >>> def try_base_login(pw):
    ...    anon_browser = make_anon_browser()  # Can't redefine header in older testbrowser
    ...    anon_browser.addHeader('Authorization', 'Basic %s:%s' % (base_id, pw))
    ...    anon_browser.open(portal.absolute_url())
    ...    print(anon_browser.contents)
    >>> try_base_login("attempt1")
    <...
    ...You have 2 attempts left before this account is locked...

    >>> try_base_login("attempt2")
    <...
    ...You have 1 attempts left before this account is locked...
    >>> try_base_login("attempt3")
    <...
    ...This account has now been locked for security purposes...
    ...
    >>> try_base_login(base_password)
    <...
    ...This account has now been locked for security purposes...
    ...



Implementation
--------------

If the root anonymoususerfactory plugin is activated following an
authentication plugin activation then this is an unsuccesful login
attempt. If the password was different from the last unsuccessful
attempt then we increment a counter in data stored persistently
in the root plugin.

If the instance plugin tries to authenticate a user that has been
marked has having too many attempts then Unauthorised will be raised.
This will activate the challenge plugin which will display a locked
out message instead of another login form.

updateCredentials is called when the login was successful and in this
case we reset the unsuccessful login count.


Troubleshooting
---------------

AttributeError: manage_addLoginLockout
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If, while running test, you get ``AttributeError: manage_addLoginLockout``,
this is likely due to the fact that the ``initialize()`` method from ``__init__.py``
isn't run during test setup.

To resolve, explicitly call::

    z2.installProduct(portal, 'Products.LoginLockout')


Developing
----------

It's great that you want to help advance this add-on!

To start development:

::

    git clone git@github.com:collective/Products.LoginLockout.git
    cd Products.LoginLockout
    virtualenv .
    ./bin/python bootstrap.py
    ./bin/buildout
    ./bin/test


Please observe the following:

* Only start work when tests are currently passing.
  If not, fix them, or ask someone (*) for help.

* Make your work in a branch and create a pull request for it on github.
  Ask for someone (*) to merge it.

* Please adhere to guidelines: pep8.
  We use plone.recipe.codeanalysis to enforce some of these.

(*) People that might be able to help you out:
    khink, djay, ajung, macagua


TODO
----
Things that could be done on the LoginLockout product:

- Move skins to browser views

- get rid of overrides for pw resets. Should be able to do in PAS or using events

- optional path to store attempts db so it can be stored in historyless db.

- perhaps have a short lock or a captcha to prevent rapid attempts instead of a full lockout

- Only restrict certain groups to certain IP networks e.g. administrators. Maybe roles too?



Copyright, License, Author
--------------------------

Copyright (c) 2007, PretaWeb, Australia,
 and the respective authors. All rights reserved.

Author: Dylan Jay <software pretaweb com>

License BSD-ish, see LICENSE.txt


Credits
-------

Dylan Jay, original code.

Contributors:

* Kees Hink
* Andreas Jung
* Leonardo J. Caballero G.
* Wolfgang Thomas
* Peter Uittenbroek
* Ovidiu Miron
* Ludolf Takens
* Maarten Kling

Thanks to Daniel Nouri and BlueDynamics for their
NoDuplicateLogin which served as the base for this.


Changes
=======

0.5.0 (2024-03-08)
------------------

- Make Python 3 and Plone 5.2 compatible [HybridAU]
- Made changes so the basics works in 5.2 and 6.0 (classic) [djay]
- Changed plugin operation to reset credentials instead of raise UnAuthorised so status messages work and switched to using status messages. 
  This does require a different order of PAS Plugins.
  [djay]
- Added warnings of attempts left
  [djay]
- Included the ability to restrict requests from certain IP networks. Config page shows current client IP [djay]
- Moved attempts storage to Plone site so no data leakage between sites
  [djay]
- Plone 5+ now uses registry for config [djay]
- Remove 'select all' buttons.
  [ivanteoh]
- Corrected private method bug.
  Added french translations.
  Corrected translation domain.
  Added some translations.
  Corrected control panel icon.
  [sgeulette]
- Corrected uninstall profile
  [sgeulette]
- Fixed change password history for versions 4.1-6.0
  [djay]
- Removed testing for 4.1
  [djay]

0.4.0 (2015-11-25)
------------------

- Fix incorrect flake8 in skins template python script.
  [khink]


0.3.9 (2015-11-18)
------------------

- Don't unicode error in portal message when resetting
  [maartenkling]


0.3.8 (2015-10-17)
------------------

- Include Travis build badge.
  Fixed test setup, make code-analysis work, update README with development info.
  (khink)


0.3.7 (2015-06-08)
------------------

- Reset counter after reset period.
  (ltakens)

0.3.6 (2015-04-08)
------------------

- Render the lockout message in the site layout.
  Show the reset period in the lockout message,
  so people don't have to contact the site administrator again.
  (khink)


0.3.5 (2015-04-02)
------------------

- Make number of allowed attempts configurable through the ZMI
  (khink)


0.3.4 (2015-04-01)
------------------

- Make reset_period configurable through the ZMI (khink)
- Added more strings classifiers items for this packages. (macagua)
- Added plone_deprecated skins for gif icon. (macagua)
- Added support for Configlet with GenericSetup profile. (macagua)
- Added Spanish translation. (macagua)
- Added i18n support. (macagua)

- LoginLockout interface updated as follows (omiron):
    - group user lockouts separate from bogus info
    - links to users profile page
    - provide full user name and email to ease "find in page"
- Introduct 'select all' option in configlet (thepjot)
- Re-enable 'reset_period', after reset_period has expired, user gets another chance (thepjot)


0.3.3 (2013-11-20)
------------------

- check for fake_client_ip in a more defensive way (pysailor)


0.3.2 (2012-03-12)
------------------

- fixed deprecation warnings (Andreas Jung)


0.3.1 (2012-02-13)
------------------

- fixed some restructured text bugs in documentation  (Andreas Jung)


0.3 (2011-03-04)
----------------

- internal cleanup

- using GenericSetup where possible

- added support for logging successful login attempts

- added support logging password changes

(Andreas Jung)


0.2 (2009-04-20)
----------------

- Eggified merged configlet version

- Started doctest

(Dylan Jay)


(2009-03-10)
------------

- Added configlet for viewing failed attempts and resetting accounts from the
  plone control panel.

- Quite probably, dropped support for pure Zope usage.

(Kees Hink)


(2008-12-18)
------------

- Added installer, using Extensions/Install.py.
  (Unfortunately, Generic Setup does not yet seem to support uninstalling, but
  the methods in setuphandlers.py and the import profile (profiles/default) are
  there for when you want to use them. Just uncomment the relevant zcml
  directives.)

(Kees Hink)


0.1 (unknown)
-------------

- Initial Version (Dylan Jay)



            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/collective/Products.LoginLockout",
    "name": "Products.LoginLockout",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "",
    "maintainer_email": "",
    "keywords": "PAS Plugins Zope Login Lockout Plone security",
    "author": "Dylan Jay",
    "author_email": "software@pretaweb.com",
    "download_url": "https://files.pythonhosted.org/packages/0b/19/e441b4a3a1b86e2cee310a70e0c0915168bbfbccab7943439bb33701a8e7/Products.LoginLockout-0.5.0.tar.gz",
    "platform": null,
    "description": "LoginLockout\n============\n\n.. image:: https://github.com/collective/Products.LoginLockout/workflows/CI/badge.svg\n  :target: https://github.com/collective/Products.LoginLockout/actions\n\n.. image:: https://coveralls.io/repos/collective/Products.LoginLockout/badge.svg?branch=master&service=github\n  :target: https://coveralls.io/github/collective/Products.LoginLockout?branch=master\n\n\nThis Pluggable Authentication Service (PAS) plugin will lock a\nlogin after a predetermined number of incorrect attempts. Once\nlocked, the user will be shown a page that tells them to contact\ntheir administrator to unlock.\n\n\nRequires:\n---------\n\n- PluggableAuthService and its dependencies\n\n- (optional) PlonePAS and its dependencies\n\n- (optional) Plone 4.1.x-6.0.x\n\n.. image:: http://github-actions.40ants.com/collective/Products.LoginLockout/matrix.svg\n   :target: https://github.com/collective/Products.LoginLockout\n\n\nFeatures\n--------\n\n- Configurable number of allowed incorrect attempts before lockout\n- Account will be usable again after a configurable amount of time\n  (the \"reset period\")\n  If the first login attempt after the reset period is invalid, the\n  invalid login counter is set to 1.\n- The user is presented with a message saying that the account was locked,\n  and for how long.\n  (It doesn't show remaining time, just the total lockout time.)\n- You can restrict users to come from certain IP networks. You don't have to\n  use the incorrect login attempts to use this feature.\n\n\nConfiguration\n-------------\n\n**NOTE** *If upgrading from 0.4.0 you will need run the upgrade or manually reset the PAS plugin order as below as this has changed.*\n\nYou can use this plugin with Zope without Plone, or with Plone. When using it with Plone you will configure it via the\nPlone registry (plone 5+) or via portal_properties if plone 4.\n\nGo to the Plone Control Panel -> LoginLockout Settings , there you can changes these defaults:\n\n    >>> admin_browser = make_admin_browser('/')\n    >>> admin_browser.getLink('Site Setup').click()\n    >>> admin_browser.getLink('LoginLockout').click()\n    >>> admin_browser.getLink('Lockout Settings').click()\n\n- allowed incorrect attempts: 3\n- reset period: 24 hours\n- whitelist_ips: [] # any origin IP is allowed\n- Fake Client IP: false\n\n    >>> admin_browser.getControl(\"Max Attempts\").value\n    '3'\n    >>> admin_browser.getControl(\"Reset Period (hours)\").value\n    '24.0'\n    >>> admin_browser.getControl('Lock logins to IP Ranges').value\n    ''\n    >>> admin_browser.getControl('Fake Client IP').selected\n    False\n\n\nLet's ensure that the settings actually change\n\n    >>> admin_browser.getControl('Fake Client IP').selected = True\n    >>> get_loginlockout_settings().fake_client_ip\n    False\n    >>> admin_browser.getControl('Save').click()\n    >>> 'Changes saved.' in admin_browser.contents\n    True\n    >>> get_loginlockout_settings().fake_client_ip\n    True\n\n\n\nDetails\n-------\n\nLoginLockout can be used as a Plone plugin or with zope and PAS alone.\nFirst we'll show you how it works with Plone.\n\n\nTo Install\n----------\n\nInstall into Plone via Add/Remove Products. If you are installing into zope without\nplone then you will need to follow these manual install steps.\n\nThis will install and activate a two PAS plugins.\n\nManual Installation\n-------------------\n\nThis plugin needs to be installed in two places, the instance PAS where logins\noccur and the root acl_users.\n\n 1. Place the Product directory 'LoginLockout' in your 'Products/'\n directory. Restart Zope.\n\n 2. In your instance PAS 'acl_users', select 'LoginLockout' from the add\n list.  Give it an id and title, and push the add button.\n\n 3. Enable the 'Authentication', and the 'Update Credentials'\n plugin interfaces in the after-add screen.\n\n\n 4. Repeat the above for your root PAS but as a plugin to\n\n    -  Anonymoususerfactory\n\n\n   and ensure LoginLockout is the first Anonymoususerfactory plugin\n\nSteps 2 through 4 below will be done for you by the Plone installer.\n\nThat's it! Test it out.\n\n\nPlone LoginLockout PAS Plugin\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIt's very important the plugin is the *first* Authentication plugin in the activated plugins list.\nThis ensures we prevent a person attempting to make a login into a locked account and display a status message.\nThis also collects the username and login and will prevent a login should it be locked.\n\n   >>> plone_pas = portal.acl_users.plugins\n   >>> IAuthenticationPlugin = plone_pas._getInterfaceFromName('IAuthenticationPlugin')\n   >>> plone_pas.listPlugins(IAuthenticationPlugin)\n   [('login_lockout_plugin', <LoginLockout at /plone/acl_users/login_lockout_plugin>)...]\n\n\nand a ICredentialsUpdatePlugin. This records when a login was successful to reset attempt data.\n\n\n   >>> ICredentialsUpdatePlugin = plone_pas._getInterfaceFromName('ICredentialsUpdatePlugin')\n   >>> 'login_lockout_plugin' in [p[0] for p in plone_pas.listPlugins(ICredentialsUpdatePlugin)]\n   True\n\n\nRoot Zope LoginLockout PAS Plugin\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIt will also install a plugin at the root of the zope instance.\n\nIt's important this is also the *first* IAnonymousUserFactoryPlugin. On a normal Zope instance it will be the only one.\nThis ensures it collects data on unsuccessful attempted logins.\n\n   >>> root_pas = portal.getPhysicalRoot().acl_users.plugins\n   >>> IAnonymousUserFactoryPlugin = plone_pas._getInterfaceFromName('IAnonymousUserFactoryPlugin')\n   >>> root_pas.listPlugins(IAnonymousUserFactoryPlugin)\n   [('login_lockout_plugin', <LoginLockout at /acl_users/login_lockout_plugin>)]\n\n\n\nLockout on incorrect password attempts\n--------------------------------------\n\nFirst login as manager\n\nNow we'll open up a new browser and attempt to login::\n\n    >>> anon_browser = make_anon_browser('/login_form')\n    >>> anon_browser.getControl('Login Name').value = user_id\n    >>> anon_browser.getControl('Password').value = user_password\n    >>> anon_browser.getControl('Log in').click()\n    >>> 'Login failed' in anon_browser.contents\n    False\n    >>> print(anon_browser.contents)\n    <BLANKLINE>\n    ...You are now logged in...\n\n    >>> anon_browser.open(portal.absolute_url()+'/logout')\n\n\nLet's try again with another password::\n\n    >>> anon_browser = make_anon_browser('/login_form')\n    >>> anon_browser.getControl('Login Name').value = user_id\n    >>> anon_browser.getControl('Password').value = 'notpassword'\n    >>> anon_browser.getControl('Log in').click()\n    >>> print(anon_browser.contents)\n    <BLANKLINE>\n    ...Login failed...\n    >>> print(anon_browser.contents)\n    <BLANKLINE>\n    ...You have 2 attempts left before this account is locked...\n\n\nthis incorrect attempt  will show up in the log\n\n\nWe've installed a Control panel to monitor the login attempts\n\n    >>> admin_browser = make_admin_browser('/loginlockout_settings')\n    >>> print(admin_browser.contents)\n    <BLANKLINE>\n    ...<td>test-user</td>...\n    ...<td>1</td>...\n\n\n\nIf we try twice more we will be locked out::\n\n    >>> anon_browser = make_anon_browser('/login_form')\n    >>> anon_browser.getControl('Login Name').value = user_id\n    >>> anon_browser.getControl('Password').value = 'notpassword2'\n    >>> anon_browser.getControl('Log in').click()\n    >>> 'Login failed' in  anon_browser.contents\n    True\n    >>> print(anon_browser.contents)\n    <BLANKLINE>\n    ...You have 1 attempts left before this account is locked...\n    >>> anon_browser.getControl('Login Name').value = user_id\n    >>> anon_browser.getControl('Password').value = 'notpassword3'\n    >>> anon_browser.getControl('Log in').click()\n    >>> 'Login failed' in  anon_browser.contents\n    True\n    >>> 'attempts left' not in anon_browser.contents\n    True\n\n    >>> print(anon_browser.contents)\n    <...\n    ...This account has now been locked for security purposes...\n\n\nNow even the correct password won't work::\n\n    >>> anon_browser = make_anon_browser('/login_form')\n    >>> anon_browser.getControl('Login Name').value = user_id\n    >>> anon_browser.getControl('Password').value = user_password\n    >>> anon_browser.getControl('Log in').click()\n\n    Not logged in\n    >>> print(anon_browser.contents)\n    <...\n    ...This account has now been locked for security purposes...\n    ...\n\n    >>> \"now logged in\" not in anon_browser.contents\n    True\n\n    >>> anon_browser.getLink(\"Home\").click()\n    >>> anon_browser.getLink('Log in')\n    <Link...>\n\n\n\nThe administrator can reset this persons account::\n\n    >>> admin_browser = make_admin_browser('/loginlockout_settings')\n    >>> print(admin_browser.contents)\n    <BLANKLINE>\n    ...<td>test-user</td>...\n    ...<td>3</td>...\n    >>> admin_browser.getControl(name='reset_nonploneusers:list').value = ['test-user']\n    >>> admin_browser.getControl('Reset selected accounts').click()\n    >>> print(admin_browser.contents)\n    <BLANKLINE>\n    ...Accounts were reset for these login names: test-user...\n\nand now they can log in again::\n\n    >>> anon_browser = make_anon_browser('/login_form')\n    >>> anon_browser.getControl('Login Name').value = user_id\n    >>> anon_browser.getControl('Password').value = user_password\n    >>> anon_browser.getControl('Log in').click()\n    >>> print(anon_browser.contents)\n    <BLANKLINE>\n    ...You are now logged in...\n\n\n\n\nIP Lockdown\n-----------\n\nYou can optionally ensure logins are only possible for certain IP address ranges.\n\nBy default IP Locking is disabled.\n\nNOTE: If you are using Zope behind a proxy then you must enable X-Forward-For headers on\neach proxy otherwise this plugin will incorrectly use REMOTE_ADDR which will be a local IP.\n\nTo enable this go into the ZMI and enter the ranges in the whitelist_ips property::\n\n    >>> config_property( whitelist_ips = u'10.1.1.1' )\n\nIf there are proxies infront of zope you will have to ensure they set the ```X-Forwarded-For``` header.\nNote only the first forwarded IP will be used.::\n\n    >>> anon_browser = make_anon_browser('/login_form')\n    >>> anon_browser.addHeader('X-Forwarded-For', '10.1.1.1, 192.168.1.1')\n    >>> anon_browser.getControl('Login Name').value = user_id\n    >>> anon_browser.getControl('Password').value = user_password\n    >>> anon_browser.getControl('Log in').click()\n    >>> print(anon_browser.contents)\n    <BLANKLINE>\n    ...You are now logged in...\n\n    >>> anon_browser.open(portal.absolute_url()+'/logout')\n\nIf not from a valid IP then the login will fail::\n\n    >>> anon_browser = make_anon_browser('/login_form')\n    >>> anon_browser.addHeader('X-Forwarded-For', '2.2.2.2')\n\n    >>> anon_browser.open(portal.absolute_url()+'/login_form')\n    >>> anon_browser.getControl('Login Name').value = user_id\n    >>> anon_browser.getControl('Password').value = user_password\n    >>> anon_browser.getControl('Log in').click()\n    >>> print(anon_browser.contents)\n    <BLANKLINE>\n    ...Login currently unavailable...\n    >>> anon_browser.getLink('Log in')\n    <Link text='Log in'...>\n\n\nBasic Auth will works with the right IP::\n\n    >>> anon_browser = make_anon_browser()\n    >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (user_id,user_password))\n    >>> anon_browser.addHeader('X-Forwarded-For', '10.1.1.1')\n\n    >>> anon_browser.open(portal.absolute_url())\n    >>> anon_browser.getLink('Log out')\n    <Link text='Log out'...>\n\n\nand basic auth fails with the wrong IP::\n\n    >>> anon_browser = make_anon_browser()\n    >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (user_id,user_password))\n    >>> anon_browser.addHeader('X-Forwarded-For', '2.2.2.2')\n\n    >>> anon_browser.open(portal.absolute_url())\n    >>> print(anon_browser.contents)\n    <BLANKLINE>\n    ...Login currently unavailable...\n    >>> anon_browser.getLink('Log in')\n    <Link text='Log in'...>\n\n\nWe can still use a root login at the root::\n\n    >>> anon_browser = make_anon_browser()\n    >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (base_id, base_password))\n    >>> anon_browser.addHeader('X-Forwarded-For', '2.2.2.2')\n\n    >>> anon_browser.open(portal.absolute_url()+'/../manage_main')\n    >>> print(anon_browser.contents)\n    <BLANKLINE>\n    ...manage_workspace...\n\nBut we can't get into the plone site with a root id any more::\n\n    >>> anon_browser.open(portal.absolute_url()+'/manage_main')\n    Traceback (most recent call last):\n    ...\n    Unauthorized: You are not authorized to access this resource.\n\n\nYou can also set IP ranges e.g.::\n\n    >>> config_property( whitelist_ips = u\"\"\"10.1.1.1\n    ... 10.1.0.0/16 # range 1\n    ... 2.2.0.0/16 # range 2\n    ... \"\"\")\n\n    >>> anon_browser = make_anon_browser('/login_form')\n    >>> anon_browser.addHeader('X-Forwarded-For', '2.2.2.2')\n    >>> anon_browser.getControl('Login Name').value = user_id\n    >>> anon_browser.getControl('Password').value = user_password\n    >>> anon_browser.getControl('Log in').click()\n    >>> print(anon_browser.contents)\n    <BLANKLINE>\n    ...You are now logged in...\n\n    >>> anon_browser.open(portal.absolute_url()+'/logout')\n\nYou can also set a env variable LOGINLOCKOUT_IP_WHITELIST which is merged with the config.\nThis allows those with filesystem access a way to get in if they have set their config wrong.\nIt also allows a set of IP ranges to be set for any site in a Plone multisite setup as long\nas the site has loginlockout installed.::\n\n    >>> anon_browser = make_anon_browser('/login_form')\n    >>> anon_browser.getLink('Log in')\n    <Link text='Log in'...\n\n    >>> import os; os.environ[\"LOGINLOCKOUT_IP_WHITELIST\"] = \"3.3.3.3\"\n\n    >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (user_id,user_password))\n    >>> anon_browser.addHeader('X-Forwarded-For', '3.3.3.3')\n\n    >>> anon_browser.open(portal.absolute_url())\n    >>> anon_browser.getLink('Log out')\n    <Link text='Log out'...>\n\n\nNote that you still have to have the IP lockout config set otherwise logins are allowed from anywhere\neven with the env variable set::\n\n    >>> config_property( whitelist_ips = u\"\"\"\n    ... \"\"\")\n    >>> anon_browser = make_anon_browser()\n    >>> anon_browser.addHeader('Authorization', 'Basic %s:%s' % (user_id,user_password))\n    >>> anon_browser.addHeader('X-Forwarded-For', '4.4.4.4')\n\n    >>> anon_browser.open(portal.absolute_url())\n    >>> anon_browser.getLink('Log out')\n    <Link text='Log out'...>\n\n\n    >>> del os.environ[\"LOGINLOCKOUT_IP_WHITELIST\"]\n\n\nIf you are unsure of what is being detected as your current Client IP you can see it in\nthe control panel::\n\n    >>> admin_browser = make_admin_browser('/')\n    >>> admin_browser.addHeader('X-Forwarded-For', '10.1.1.1, 192.168.1.1')\n\n    >>> admin_browser.getLink('Site Setup').click()\n    >>> admin_browser.getLink('LoginLockout').click()\n    >>> print(admin_browser.contents)\n    <BLANKLINE>\n    ...Current detected Client IP: <span>10.1.1.1</span>...\n\n\nLogin History\n-------------\n\nIt is also possible to view a history of successful logins for a particular user. Note this is the user id rather\nthan user login and they can be different. User ``test_user_1_`` had 4 successful logins.::\n\n    >>> admin_browser = make_admin_browser('/loginlockout_settings')\n    >>> admin_browser.getLink('Login history').click()\n    >>> admin_browser.getControl('Username pattern').value = 'test_user_1_'\n    >>> admin_browser.getControl('Search records').click()\n    >>> print(admin_browser.contents)\n    <BLANKLINE>\n    ...\n                        <td valign=\"top\">test_user_1_</td>\n                        <td valign=\"top\">\n                            <ul>\n                                <li>\n                                    ...\n                                    ()\n                                </li>\n                                <li>\n                                    ...\n                                    ()\n                                </li>\n                                <li>\n                                    ...\n                                    (10.1.1.1)\n                                </li>\n                                <li>\n                                    ...\n                                    (2.2.2.2)\n                                </li>\n                            </ul>\n    ...\n\n\n\nPassword Reset History\n----------------------\n\nWhen a user changes their password::\n\n    >>> anon_browser = make_anon_browser('/login_form')\n    >>> anon_browser.getControl('Login Name').value = user_id\n    >>> anon_browser.getControl('Password').value = user_password\n    >>> anon_browser.getControl('Log in').click()\n\n    >>> anon_browser.getLink(\"Preferences\").click()\n    >>> anon_browser.getLink(\"Password\").click()\n    >>> anon_browser.getControl('Current password').value = user_password\n    >>> anon_browser.getControl('New password').value = '12345678'\n    >>> anon_browser.getControl('Confirm password').value = '12345678'\n    >>> anon_browser.getControl('Change Password').click()\n    >>> print(anon_browser.contents)\n    <...\n    ...Password changed... \n    ...\n\nThis changed the password::\n\n    >>> anon_browser = make_anon_browser('/login_form')\n    >>> anon_browser.getControl('Login Name').value = user_id\n    >>> anon_browser.getControl('Password').value = '12345678'\n    >>> anon_browser.getControl('Log in').click()\n    >>> anon_browser.getLink(\"Preferences\").click()\n\nThe the administrators can see the password was changed::\n\n    >>> admin_browser = make_admin_browser('/loginlockout_settings')\n    >>> admin_browser.getLink('History password changes').click()\n    >>> print(admin_browser.contents)\n    <...\n    ...\n            <tr class=\"even\">\n                <td>test_user_1_</td>\n                <td>...</td>\n            </tr>\n    ...\n\nOther support\n--------------\n\nRoot users can also be locked out and with basic authentication too::\n\n    >>> def try_base_login(pw):\n    ...    anon_browser = make_anon_browser()  # Can't redefine header in older testbrowser\n    ...    anon_browser.addHeader('Authorization', 'Basic %s:%s' % (base_id, pw))\n    ...    anon_browser.open(portal.absolute_url())\n    ...    print(anon_browser.contents)\n    >>> try_base_login(\"attempt1\")\n    <...\n    ...You have 2 attempts left before this account is locked...\n\n    >>> try_base_login(\"attempt2\")\n    <...\n    ...You have 1 attempts left before this account is locked...\n    >>> try_base_login(\"attempt3\")\n    <...\n    ...This account has now been locked for security purposes...\n    ...\n    >>> try_base_login(base_password)\n    <...\n    ...This account has now been locked for security purposes...\n    ...\n\n\n\nImplementation\n--------------\n\nIf the root anonymoususerfactory plugin is activated following an\nauthentication plugin activation then this is an unsuccesful login\nattempt. If the password was different from the last unsuccessful\nattempt then we increment a counter in data stored persistently\nin the root plugin.\n\nIf the instance plugin tries to authenticate a user that has been\nmarked has having too many attempts then Unauthorised will be raised.\nThis will activate the challenge plugin which will display a locked\nout message instead of another login form.\n\nupdateCredentials is called when the login was successful and in this\ncase we reset the unsuccessful login count.\n\n\nTroubleshooting\n---------------\n\nAttributeError: manage_addLoginLockout\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIf, while running test, you get ``AttributeError: manage_addLoginLockout``,\nthis is likely due to the fact that the ``initialize()`` method from ``__init__.py``\nisn't run during test setup.\n\nTo resolve, explicitly call::\n\n    z2.installProduct(portal, 'Products.LoginLockout')\n\n\nDeveloping\n----------\n\nIt's great that you want to help advance this add-on!\n\nTo start development:\n\n::\n\n    git clone git@github.com:collective/Products.LoginLockout.git\n    cd Products.LoginLockout\n    virtualenv .\n    ./bin/python bootstrap.py\n    ./bin/buildout\n    ./bin/test\n\n\nPlease observe the following:\n\n* Only start work when tests are currently passing.\n  If not, fix them, or ask someone (*) for help.\n\n* Make your work in a branch and create a pull request for it on github.\n  Ask for someone (*) to merge it.\n\n* Please adhere to guidelines: pep8.\n  We use plone.recipe.codeanalysis to enforce some of these.\n\n(*) People that might be able to help you out:\n    khink, djay, ajung, macagua\n\n\nTODO\n----\nThings that could be done on the LoginLockout product:\n\n- Move skins to browser views\n\n- get rid of overrides for pw resets. Should be able to do in PAS or using events\n\n- optional path to store attempts db so it can be stored in historyless db.\n\n- perhaps have a short lock or a captcha to prevent rapid attempts instead of a full lockout\n\n- Only restrict certain groups to certain IP networks e.g. administrators. Maybe roles too?\n\n\n\nCopyright, License, Author\n--------------------------\n\nCopyright (c) 2007, PretaWeb, Australia,\n and the respective authors. All rights reserved.\n\nAuthor: Dylan Jay <software pretaweb com>\n\nLicense BSD-ish, see LICENSE.txt\n\n\nCredits\n-------\n\nDylan Jay, original code.\n\nContributors:\n\n* Kees Hink\n* Andreas Jung\n* Leonardo J. Caballero G.\n* Wolfgang Thomas\n* Peter Uittenbroek\n* Ovidiu Miron\n* Ludolf Takens\n* Maarten Kling\n\nThanks to Daniel Nouri and BlueDynamics for their\nNoDuplicateLogin which served as the base for this.\n\n\nChanges\n=======\n\n0.5.0 (2024-03-08)\n------------------\n\n- Make Python 3 and Plone 5.2 compatible [HybridAU]\n- Made changes so the basics works in 5.2 and 6.0 (classic) [djay]\n- Changed plugin operation to reset credentials instead of raise UnAuthorised so status messages work and switched to using status messages. \n  This does require a different order of PAS Plugins.\n  [djay]\n- Added warnings of attempts left\n  [djay]\n- Included the ability to restrict requests from certain IP networks. Config page shows current client IP [djay]\n- Moved attempts storage to Plone site so no data leakage between sites\n  [djay]\n- Plone 5+ now uses registry for config [djay]\n- Remove 'select all' buttons.\n  [ivanteoh]\n- Corrected private method bug.\n  Added french translations.\n  Corrected translation domain.\n  Added some translations.\n  Corrected control panel icon.\n  [sgeulette]\n- Corrected uninstall profile\n  [sgeulette]\n- Fixed change password history for versions 4.1-6.0\n  [djay]\n- Removed testing for 4.1\n  [djay]\n\n0.4.0 (2015-11-25)\n------------------\n\n- Fix incorrect flake8 in skins template python script.\n  [khink]\n\n\n0.3.9 (2015-11-18)\n------------------\n\n- Don't unicode error in portal message when resetting\n  [maartenkling]\n\n\n0.3.8 (2015-10-17)\n------------------\n\n- Include Travis build badge.\n  Fixed test setup, make code-analysis work, update README with development info.\n  (khink)\n\n\n0.3.7 (2015-06-08)\n------------------\n\n- Reset counter after reset period.\n  (ltakens)\n\n0.3.6 (2015-04-08)\n------------------\n\n- Render the lockout message in the site layout.\n  Show the reset period in the lockout message,\n  so people don't have to contact the site administrator again.\n  (khink)\n\n\n0.3.5 (2015-04-02)\n------------------\n\n- Make number of allowed attempts configurable through the ZMI\n  (khink)\n\n\n0.3.4 (2015-04-01)\n------------------\n\n- Make reset_period configurable through the ZMI (khink)\n- Added more strings classifiers items for this packages. (macagua)\n- Added plone_deprecated skins for gif icon. (macagua)\n- Added support for Configlet with GenericSetup profile. (macagua)\n- Added Spanish translation. (macagua)\n- Added i18n support. (macagua)\n\n- LoginLockout interface updated as follows (omiron):\n    - group user lockouts separate from bogus info\n    - links to users profile page\n    - provide full user name and email to ease \"find in page\"\n- Introduct 'select all' option in configlet (thepjot)\n- Re-enable 'reset_period', after reset_period has expired, user gets another chance (thepjot)\n\n\n0.3.3 (2013-11-20)\n------------------\n\n- check for fake_client_ip in a more defensive way (pysailor)\n\n\n0.3.2 (2012-03-12)\n------------------\n\n- fixed deprecation warnings (Andreas Jung)\n\n\n0.3.1 (2012-02-13)\n------------------\n\n- fixed some restructured text bugs in documentation  (Andreas Jung)\n\n\n0.3 (2011-03-04)\n----------------\n\n- internal cleanup\n\n- using GenericSetup where possible\n\n- added support for logging successful login attempts\n\n- added support logging password changes\n\n(Andreas Jung)\n\n\n0.2 (2009-04-20)\n----------------\n\n- Eggified merged configlet version\n\n- Started doctest\n\n(Dylan Jay)\n\n\n(2009-03-10)\n------------\n\n- Added configlet for viewing failed attempts and resetting accounts from the\n  plone control panel.\n\n- Quite probably, dropped support for pure Zope usage.\n\n(Kees Hink)\n\n\n(2008-12-18)\n------------\n\n- Added installer, using Extensions/Install.py.\n  (Unfortunately, Generic Setup does not yet seem to support uninstalling, but\n  the methods in setuphandlers.py and the import profile (profiles/default) are\n  there for when you want to use them. Just uncomment the relevant zcml\n  directives.)\n\n(Kees Hink)\n\n\n0.1 (unknown)\n-------------\n\n- Initial Version (Dylan Jay)\n\n\n",
    "bugtrack_url": null,
    "license": "GPL",
    "summary": "This Pluggable Authentication Service (PAS) plugin will lock a                  login after a predetermined number of incorrect attempts. Once                  locked, the user will be shown a page that tells them to contact                  their administrator to unlock.",
    "version": "0.5.0",
    "project_urls": {
        "Homepage": "https://github.com/collective/Products.LoginLockout"
    },
    "split_keywords": [
        "pas",
        "plugins",
        "zope",
        "login",
        "lockout",
        "plone",
        "security"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "9146751f0702358bb02521a93707beffe2fe37c853bb872a84470783290ae3ad",
                "md5": "7201e1b4f34622186c516ff09550ffbc",
                "sha256": "9f930ca9dc803764a07f6f322a7982532ed07e5c08413d3e29364b531772ee56"
            },
            "downloads": -1,
            "filename": "Products.LoginLockout-0.5.0-py2-none-any.whl",
            "has_sig": false,
            "md5_digest": "7201e1b4f34622186c516ff09550ffbc",
            "packagetype": "bdist_wheel",
            "python_version": "py2",
            "requires_python": null,
            "size": 52460,
            "upload_time": "2024-03-08T07:44:29",
            "upload_time_iso_8601": "2024-03-08T07:44:29.619003Z",
            "url": "https://files.pythonhosted.org/packages/91/46/751f0702358bb02521a93707beffe2fe37c853bb872a84470783290ae3ad/Products.LoginLockout-0.5.0-py2-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "0b19e441b4a3a1b86e2cee310a70e0c0915168bbfbccab7943439bb33701a8e7",
                "md5": "10ae420a38ea7d3a3e7ffb62ab01f247",
                "sha256": "97b952548e79708472dbd808a957212aa21a42e42da48f281c0bfc8d2d7a9981"
            },
            "downloads": -1,
            "filename": "Products.LoginLockout-0.5.0.tar.gz",
            "has_sig": false,
            "md5_digest": "10ae420a38ea7d3a3e7ffb62ab01f247",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 57495,
            "upload_time": "2024-03-08T07:44:32",
            "upload_time_iso_8601": "2024-03-08T07:44:32.408558Z",
            "url": "https://files.pythonhosted.org/packages/0b/19/e441b4a3a1b86e2cee310a70e0c0915168bbfbccab7943439bb33701a8e7/Products.LoginLockout-0.5.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-03-08 07:44:32",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "collective",
    "github_project": "Products.LoginLockout",
    "travis_ci": false,
    "coveralls": true,
    "github_actions": true,
    "lcname": "products.loginlockout"
}
        
Elapsed time: 0.19461s