Skip to content

Commit

Permalink
Merge pull request #3 from dnadesign/feature/refinements
Browse files Browse the repository at this point in the history
Functional refinements to the module
  • Loading branch information
michalkleiner authored May 28, 2024
2 parents f19de3e + c759a01 commit 475f164
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 93 deletions.
37 changes: 24 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
# IdleLock
They say that idle hands are the devils playthings, so idle accounts are the security vulnerability's gateway.

## Introduction
Safeguard your SilverStripe site by automatically locking idle member accounts.

Safeguard your SilverStripe site with this module that automatically locks idle accounts, fortifying your security by closing the gateway to potential vulnerabilities.

The default idle period can be configured, and may be specified per security group.
The default threshold can be configured, and may be specified per security group.

A Locked User report is included.

Expand All @@ -19,23 +16,37 @@ A Locked User report is included.

## Configuration

To automatically lock idle accounts, set up a cron to run the LockMembersTask task at your desired frequency, e.g. daily
To automatically lock idle member accounts, set up a cron to run the **LockMembersTask** task at your desired frequency.

To update the global default lockout threshold, set the config in your project:
To set a global default lockout threshold, set the config in your project:

```
SilverStripe\Security\Member:
lockout_threshold_days: 30
```

To allow users to be exempt from lockout, controlled by a checkbox on the member profile or a security group, set the config in your project:

```
SilverStripe\Security\Member:
lockout_exempt: true
SilverStripe\Security\Group:
lockout_exempt: true
```

To update the default message shown to locked out users at the login screen, use the LockoutMessage field in the CMS > Settings > Access tab

## Usage

Once the cron is set, the users will automatically lock if they don't lock in for a period longer than the lockout threshold.
Once the cron is set, unless exempt, member accounts will automatically lock if they don't log in for a period longer than the lockout threshold.

Locked users will see a message on the login screen indicating why they can't log in.

Locked users will see a different message on the login screen indicating why they can't log in.
To unlock the user, an admin must view the *Member* record in the CMS and uncheck the 'Locked' checkbox.

To unlock the user, an admin must view the Member record in the CMS and uncheck the 'Locked' checkbox.
To set a custom lockout threshold for members of a group, update the *LockoutThresholdDays* field for that Group in the security admin.
* If the user is a member of multiple groups, the lowest threshold will apply.
* The group threshold cannot be used to increase the threshold beyond the global default.

To set a custom lockout threshold for members of a group, update the LockoutThresholdDays field for that Group in the security admin.
If the user is a member of multiple groups, the lowest threshold will apply.
The group threshold cannot be used to increase the threshold beyond the global default.
Use the *LockoutExempt* field on a Member or a Group to excuse the member or members of the group from the lockout feature.
14 changes: 9 additions & 5 deletions _config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
Name: idlelock
---

# Add a field to Group to configure the lockout threshold
SilverStripe\Security\Member:
lockout_threshold_days: 30
lockout_message: 'This account has been locked. Please contact an administrator.'
extensions:
- DNADesign\IdleLock\Extensions\MemberLockoutExtension

SilverStripe\Security\Group:
extensions:
- DNADesign\IdleLock\Extensions\GroupLockoutThresholdExtension
- DNADesign\IdleLock\Extensions\GroupLockoutExtension

SilverStripe\Security\Member:
lockout_threshold_days: 30 # Default lockout threshold
SilverStripe\SiteConfig\SiteConfig:
extensions:
- DNADesign\IdleLock\Extensions\MemberLockoutExtension
- DNADesign\IdleLock\Extensions\SiteConfigLockoutExtension
72 changes: 72 additions & 0 deletions src/Extensions/GroupLockoutExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace DNADesign\IdleLock\Extensions;

use SilverStripe\Forms\FieldList;
use SilverStripe\Security\Member;
use SilverStripe\ORM\DataExtension;
use SilverStripe\Core\Config\Config;
use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Security\Group;
use SilverStripe\Security\Permission;

class GroupLockoutExtension extends DataExtension
{
private static $db = [
'LockoutEnabled' => 'Boolean',
'LockoutExempt' => 'Boolean',
'LockoutThresholdDays' => 'Int',
];

public function updateCMSFields(FieldList $fields)
{
$fields->removeByName([
'LockoutEnabled',
'LockoutExempt',
'LockoutThresholdDays'
]);

if (Permission::check('ADMIN')) {
// If configured, groups may exempt members from the idle-lockout feature
if (Config::inst()->get(Group::class, 'lockout_exempt')) {
$fields->insertAfter(
'Description',
CheckboxField::create('LockoutExempt', 'Lockout exempt')
->setDescription('Membership of this group exempts members from the idle-lockout feature'),
);
}

}
// Enable lockout for this group
if (!$this->owner->LockoutExempt) {
$fields->insertAfter(
'Description',
CheckboxField::create('LockoutEnabled', 'Enable lockout')
->setDescription('Activates the idle-lockout feature for members of this group'),
);
}

// Set the lockout threshold
if ($this->owner->LockoutEnabled) {
$fields->insertAfter(
'LockoutEnabled',
NumericField::create('LockoutThresholdDays', 'Lockout threshold in days')
->setDescription(sprintf(
'Inactive time in days. Members of this group will be locked out if they
do not log in for this amount of time.<br>Set to 0 to use the default of
%s days.', Config::inst()->get(Member::class, 'lockout_threshold_days')))
->setAttribute('max', 999),
);
}
}

public function onBeforeWrite()
{
parent::onBeforeWrite();

if ($this->owner->LockoutExempt) {
$this->owner->LockoutEnabled = false;
}
}
}
32 changes: 0 additions & 32 deletions src/Extensions/GroupLockoutThresholdExtension.php

This file was deleted.

124 changes: 81 additions & 43 deletions src/Extensions/MemberLockoutExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,108 @@

namespace DNADesign\IdleLock\Extensions;

use SilverStripe\Core\Config\Config;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Security\Member;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Core\Config\Config;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Security\Permission;
use SilverStripe\Security\LoginAttempt;
use SilverStripe\Security\Member;
use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\ORM\FieldType\DBDatetime;

class MemberLockoutExtension extends DataExtension
{
private static $db = [
'Locked' => 'Boolean',
'LockoutExempt' => 'Boolean',
];

public function updateCMSFields(FieldList $fields)
{
$defaultLockoutThreshold = Config::inst()->get(Member::class, 'lockout_threshold_days');
$fields->replaceField('Locked', CheckboxField::create('Locked')->setDescription(sprintf('When checked, this user cannot log in. Set either manually here, or by idle timeout (default %s days or specified per security group).', $defaultLockoutThreshold)));
$fields->removeByName(['Locked', 'LockoutExempt']);

if (Permission::check('ADMIN')) {
if (!$this->owner->LockoutExempt) {
$defaultLockoutThreshold = Config::inst()->get(Member::class, 'lockout_threshold_days');
$fields->addFieldToTab('Root.Main', CheckboxField::create('Locked')->setDescription(sprintf('When checked, this user cannot log in. Set either manually here, or by idle timeout (default %s days or specified per security group).', $defaultLockoutThreshold)), 'FirstName');
}

// If configured, users may be exempted from the idle-lockout feature
if (Config::inst()->get(Member::class, 'lockout_exempt')) {
$fields->addFieldToTab('Root.Main', CheckboxField::create('LockoutExempt')->setDescription('When checked, this user cannot be locked out by the idle-lock feature'), 'FirstName');
}
}

}

public function onBeforeWrite()
{
parent::onBeforeWrite();

if ($this->owner->LockoutExempt) {
$this->owner->Locked = false;
}
}

/**
* Prevent login if the Member is locked out
* Prevent login if the Member is locked out, allow a custom message
*
* @param $result
* @return void
*/
public function canLogIn(&$result)
{
if ($this->owner->Locked) {
$result->addError('Your account has been locked due to inactivity. If this is not correct, please contact the administrator.');
$config = SiteConfig::current_site_config();
$defaultLockoutMessage = Config::inst()->get(Member::class, 'lockout_message');
$errorMessage = $config->LockoutMessage ?: $defaultLockoutMessage;
$result->addError($errorMessage);
}
}

/**
* List the groups this Member is a member of
*
* @return string
* Return whether the user should be locked out
* Depending on whether they haven't logged in for a certain amount of time.
*/
public function getGroupNames()
public function shouldBeLockedOut() : bool
{
$groups = $this->owner->Groups()->sort('Title ASC');
// False if exempt, or a member of an exempt group
if ($this->owner->LockoutExempt || $this->owner->Groups()->filter('LockoutExempt', true)->first()) {
return false;
}

$groupNames = $groups->map('ID', 'Title')->toArray();
$groupNamesString = implode(' | ', $groupNames);
$lockoutGroups = $this->owner->Groups()->filter([
'LockoutEnabled' => true
]);

return $groupNamesString;

if ($lockoutGroups->count() > 0) {
$lockAfterDate = $this->owner->getLockAfterDate($lockoutGroups);
$lastAccessedDate = $this->owner->getLastLogin();

// For accounts for new users who haven't logged in yet, use the created date
if (is_null($lastAccessedDate)) {
$lastAccessedDate = $this->owner->dbObject('Created');
}

return $lockAfterDate > $lastAccessedDate;
}

// If not a member of any lockout groups, return false
return false;
}

/**
* Return the Date after which a user should be locked out
*/
public function getLockIfInactiveAfter() : DBDateTime
public function getLockAfterDate($lockoutGroups) : DBDateTime
{
$defaultLockoutThreshold = Config::inst()->get(Member::class, 'lockout_threshold_days');

$groups = $this->owner->Groups();
$lowestThreshold = $groups->filter(
[
'LockoutThresholdDays:GreaterThan' => 0,
'LockoutThresholdDays:LessThan' => $defaultLockoutThreshold,
]
)->min('LockoutThresholdDays') ?: $defaultLockoutThreshold;
if ($lockoutGroups->count() > 0) {
$lowestThreshold = $lockoutGroups->min('LockoutThresholdDays');
} else {
$lowestThreshold = Config::inst()->get(Member::class, 'lockout_threshold_days');
}

return DBDateTime::now()->modify("-{$lowestThreshold} days");
}
Expand Down Expand Up @@ -106,23 +146,6 @@ public function getLastLogin(): ?DBDateTime
return null;
}

/**
* Return whether the user should be locked out
* Depending on whether they haven't logged in for a certain amount of time.
*/
public function shouldBeLockedOut() : bool
{
$lockIfInactiveAfter = $this->owner->getLockIfInactiveAfter();
$lastAccessed = $this->owner->getLastLogin();

// For accounts for new users who haven't logged in yet, use the created date
if (is_null($lastAccessed)) {
$lastAccessed = $this->owner->dbObject('Created');
}

return $lockIfInactiveAfter > $lastAccessed;
}

/**
* Lock the user if their last login was more than the lockout threshold ago
*
Expand All @@ -135,4 +158,19 @@ public function doLockOutAfterIdle() : bool

return true;
}

/**
* List the groups this Member is a member of
*
* @return string
*/
public function getGroupNames()
{
$groups = $this->owner->Groups()->sort('Title ASC');

$groupNames = $groups->map('ID', 'Title')->toArray();
$groupNamesString = implode(' | ', $groupNames);

return $groupNamesString;
}
}
30 changes: 30 additions & 0 deletions src/Extensions/SiteConfigLockoutExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace DNADesign\IdleLock\Extensions;

use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
use SilverStripe\Security\Member;
use SilverStripe\ORM\DataExtension;
use SilverStripe\Core\Config\Config;

/**
* Allow a CMS override for the login screen message displayed to locked users
*/
class SiteConfigLockoutExtension extends DataExtension
{
private static $db = [
'LockoutMessage' => 'Varchar(255)',
];

public function updateCMSFields(FieldList $fields)
{
$fields->addFieldToTab('Root.Access',
TextField::create('LockoutMessage')
->setDescription(sprintf(
'Override the login screen message displayed to locked users.<br><strong>Default:</strong> %s',
Config::inst()->get(Member::class, 'lockout_message')
))
);
}
}

0 comments on commit 475f164

Please sign in to comment.