Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUG Only allow the owner of a LoginSession to view/delete it #62

67 changes: 67 additions & 0 deletions doc/en/permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Managing permissions for sessions

By default, users can only see and invalidate their own sessions. Not even an administrator can see other users' sessions. This is meant to protect the privacy of all users.

Specific projects may wish to allow a some users to view and manage the sessions of other users.

## A note about privacy
maxime-rainville marked this conversation as resolved.
Show resolved Hide resolved

Viewing a user's session details can allow you to determine their approximate location at specific times. Before allowing some of your users to manages other users' sessions, it's a good idea to have a conversation with them about the privacy implications.

You should also consider the relevant privacy legislation in the jurisdiction you operate in.

## Customising the permissions for `LoginSession`

`SilverStripe\SessionManager\Model\LoginSession` is the object that tracks the users' sessions. By altering the permission logic on this object, you can allow some users to manage other users' sessions. The two permissions you'll most likely want to change are `canView` and `canDelete`. You can customise `canEdit` and `canCreate` as well, but the use case for doing so is less clear.

### Creating an extension for `LoginSession`

The first step is to create a `DataExtension` that grant some users the ability to hooks into `LoginSession`'s `canView` and `canDelete` methods. This example aligns the permissions on the `LoginSession` to the permission on the Member who owns the `LoginSession`.

Alternatively, you could call `Permission::check()` to validate if the member has a pre-defined CMS permission. If you need even more granular permissions, you can implement a [PermissionProvider](https://docs.silverstripe.org/en/4/developer_guides/security/permissions/#permissionprovider) to define your own custom permissions.

```php
<?php
namespace My\App;

use SilverStripe\ORM\DataExtension;
maxime-rainville marked this conversation as resolved.
Show resolved Hide resolved
use SilverStripe\Security\Member;

class LoginSessionExtension extends DataExtension
{
/**
* @param Member $member
*/
public function canView($member)
{
if ($this->getOwner()->Member()->canView($member)) {
// If you can view a Member, you can also view their sessions.
// This does not allow you to terminate their session.
return true;
};
}

/**
* @param Member $member
*/
public function canDelete($member)
{
if ($this->getOwner()->Member()->canEdit($member)) {
// If you can edit a Member, you can also log them out of a session.
// This action is aligned to canDelete, because logging a user out is
// equivalent to deleting the LoginSession.
return true;
};
}
}
```

### Applying the extension to `LoginSession`

Add this bit of code to your project's YML configuration te enable your extension.

```yml
SilverStripe\SessionManager\Model\LoginSession:
extensions:
- My\App\LoginSessionExtension
```
3 changes: 1 addition & 2 deletions lang/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ en:
SilverStripe\SessionManager\Extensions\MemberExtension:
DEVICES: 'Authenticated devices'
LEARN_MORE: 'Learn more'
SESSION_MANAGER_PERMISSION_DESCRIPTION: 'Ability to view and purge active login sessions for other members. Requires the "Access to ''Security'' section" permission.'
SESSION_MANAGER_PERMISSION_LABEL: 'View/purge login sessions for other members'
SilverStripe\SessionManager\Forms\GridFieldRevokeLoginSessionAction:
DeletePermissionsFailure: 'No delete permissions'
SilverStripe\SessionManager\Job\GarbageCollectionJob:
TITLE: 'Session manager garbage collection'
SilverStripe\SessionManager\Model\LoginSession:
BROWSER_ON_OS: '{browser} on {os}.'
PLURALNAME: 'Login Sessions'
PLURALS:
one: 'A Login Session'
Expand Down
126 changes: 33 additions & 93 deletions src/Extension/MemberExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@

namespace SilverStripe\SessionManager\Extensions;

use SilverStripe\Control\Session;
use SilverStripe\Core\Extension;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig_Base;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Security\Permission;
use SilverStripe\Security\PermissionProvider;
use SilverStripe\Security\Security;
use SilverStripe\ORM\DataExtension;
use SilverStripe\Security\Member;
use SilverStripe\SessionManager\FormField\SessionManagerField;
use SilverStripe\SessionManager\Forms\GridFieldRevokeLoginSessionAction;
use SilverStripe\SessionManager\Model\LoginSession;

class MemberExtension extends Extension implements PermissionProvider
/**
* Augment `Member` to allow relationship to the LoginSession DataObject
* @method LoginSession[]|DataList LoginSessions()
* @method Member getOwner()
*/
class MemberExtension extends DataExtension
maxime-rainville marked this conversation as resolved.
Show resolved Hide resolved
{
public const SESSION_MANAGER_ADMINISTER_SESSIONS = 'SESSION_MANAGER_ADMINISTER_SESSIONS';
/**
* URL to the user help abot managing session
* @var string
* @config
*/
private static $session_login_help_url =
'https://userhelp.silverstripe.org/en/4/optional_features/managing_devices';
maxime-rainville marked this conversation as resolved.
Show resolved Hide resolved

/**
* @var array
Expand All @@ -31,95 +35,31 @@ class MemberExtension extends Extension implements PermissionProvider
*/
public function updateCMSFields(FieldList $fields)
{
$fields->removeByName(['LoginSessions', 'SessionManagerField']);
$fields->removeByName('LoginSessions');
maxime-rainville marked this conversation as resolved.
Show resolved Hide resolved

if (!$this->owner->exists() || !$this->currentUserCanViewSessions()) {
return $fields;
$member = $this->getOwner();
if (!$member->exists()) {
// We're creating a new member
return;
}

$session = $member->LoginSessions()->first();
if (!$session || !$session->canView()) {
// We're assuming that the first session permission are representative of the other sessions
maxime-rainville marked this conversation as resolved.
Show resolved Hide resolved
return;
}

$helpUrl = $member::config()->get('session_login_help_url');

$fields->addFieldToTab(
'Root.Main',
$sessionManagerField = SessionManagerField::create(
'SessionManagerField',
SessionManagerField::create(
'LoginSessions',
_t(__CLASS__ . '.DEVICES', 'Authenticated devices'),
$this->owner->ID,
_t(__CLASS__ . '.LEARN_MORE', 'Learn more'),
'http://userhelp.silverstripe.org/TODO'
)
);

if (!$this->currentUserCanEditSessions()) {
$sessionManagerField->setReadonly(true);
}

return $fields;
}

/**
* @return int
*/
private function getSessionLifetime(): int
{
if ($lifetime = Session::config()->get('timeout')) {
return $lifetime;
}

return LoginSession::config()->get('default_session_lifetime');
}

/**
* Determines whether the logged in user has sufficient permission to see the SessionManager config for this Member.
*
* @return bool
*/
public function currentUserCanViewSessions(): bool
{
return (Permission::check(self::SESSION_MANAGER_ADMINISTER_SESSIONS)
|| $this->currentUserCanEditSessions());
}

/**
* Determines whether the logged in user has sufficient permission
* to modify the SessionManager config for this Member.
* Note that this is different from being able to _reset_ the config (which administrators can do).
*
* @return bool
*/
public function currentUserCanEditSessions(): bool
{
return (Security::getCurrentUser() && Security::getCurrentUser()->ID === $this->owner->ID);
}

/**
* Provides the SessionManager view/reset permission for selection in the permission list in the CMS.
*
* @return array
*/
public function providePermissions(): array
{
$label = _t(
__CLASS__ . '.SESSION_MANAGER_PERMISSION_LABEL',
'View/purge login sessions for other members'
$helpUrl ? _t(__CLASS__ . '.LEARN_MORE', 'Learn more') : '',
$helpUrl ?: ''
)->setReadonly(!$session->canDelete())
maxime-rainville marked this conversation as resolved.
Show resolved Hide resolved
);

$category = _t(
'SilverStripe\\Security\\Permission.PERMISSIONS_CATEGORY',
'Roles and access permissions'
);

$description = _t(
__CLASS__ . '.SESSION_MANAGER_PERMISSION_DESCRIPTION',
'Ability to view and purge active login sessions for other members.'
. ' Requires the "Access to \'Security\' section" permission.'
);

return [
self::SESSION_MANAGER_ADMINISTER_SESSIONS => [
'name' => $label,
'category' => $category,
'help' => $description,
'sort' => 200,
],
];
}
}
5 changes: 4 additions & 1 deletion src/FormField/SessionManagerField.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
namespace SilverStripe\SessionManager\FormField;

use SilverStripe\Forms\FormField;
use SilverStripe\SessionManager\Control\LoginSessionController;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Member;
use SilverStripe\SessionManager\Control\LoginSessionController;
use SilverStripe\SessionManager\Model\LoginSession;
use SilverStripe\View\ViewableData;

Expand Down Expand Up @@ -123,6 +123,9 @@ protected function getLoginSessions(Member $member)
$loginSessions = [];
/** @var LoginSession $loginSession */
foreach (LoginSession::getCurrentSessions($member) as $loginSession) {
if (!$loginSession->canView()) {
continue;
}
$loginSessions[] = [
'ID' => $loginSession->ID,
'IPAddress' => $loginSession->IPAddress,
Expand Down
Loading