Completed
Pull Request — master (#7007)
by Simon
08:19
created

testLegacyPasswordHashMigrationUponLogin()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 15
nc 1
nop 0
dl 0
loc 25
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security\Tests;
4
5
use SilverStripe\Core\Injector\Injector;
6
use SilverStripe\ORM\DataModel;
7
use SilverStripe\ORM\FieldType\DBDatetime;
8
use SilverStripe\Security\Authenticator;
9
use SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator;
10
use SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm;
11
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
12
use SilverStripe\Security\Security;
13
use SilverStripe\Security\Member;
14
use SilverStripe\Security\MemberAuthenticator\MemberLoginForm;
15
use SilverStripe\Security\IdentityStore;
16
use SilverStripe\Core\Config\Config;
17
use SilverStripe\Dev\SapphireTest;
18
use SilverStripe\Control\HTTPRequest;
19
20
class MemberAuthenticatorTest extends SapphireTest
21
{
22
23
    protected $usesDatabase = true;
24
25
    protected $defaultUsername = null;
26
    protected $defaultPassword = null;
27
28
    protected function setUp()
29
    {
30
        parent::setUp();
31
32
        $this->defaultUsername = Security::default_admin_username();
33
        $this->defaultPassword = Security::default_admin_password();
34
        Security::clear_default_admin();
35
        Security::setDefaultAdmin('admin', 'password');
36
    }
37
38
    protected function tearDown()
39
    {
40
        Security::setDefaultAdmin($this->defaultUsername, $this->defaultPassword);
41
        parent::tearDown();
42
    }
43
44
    public function testCustomIdentifierField()
45
    {
46
47
        $origField = Member::config()->unique_identifier_field;
0 ignored issues
show
Documentation introduced by
The property unique_identifier_field does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
48
        Member::config()->unique_identifier_field = 'Username';
0 ignored issues
show
Documentation introduced by
The property unique_identifier_field does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
49
50
        $label=singleton(Member::class)->fieldLabel(Member::config()->unique_identifier_field);
51
52
        $this->assertEquals($label, 'Username');
53
54
        Member::config()->unique_identifier_field = $origField;
0 ignored issues
show
Documentation introduced by
The property unique_identifier_field does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
55
    }
56
57
    public function testGenerateLoginForm()
58
    {
59
        $authenticator = new MemberAuthenticator();
60
61
        $controller = new Security();
62
63
        // Create basic login form
64
        $frontendResponse = $authenticator
65
            ->getLoginHandler($controller->link())
66
            ->handleRequest(new HTTPRequest('get', '/'), DataModel::inst());
67
68
        $this->assertTrue(is_array($frontendResponse));
69
        $this->assertTrue(isset($frontendResponse['Form']));
70
        $this->assertTrue($frontendResponse['Form'] instanceof MemberLoginForm);
71
    }
72
73
    public function testGenerateCMSLoginForm()
74
    {
75
        /** @var CMSMemberAuthenticator $authenticator */
76
        $authenticator = new CMSMemberAuthenticator();
77
78
        // Supports cms login form
79
        $this->assertGreaterThan(0, ($authenticator->supportedServices() & Authenticator::CMS_LOGIN));
80
        $cmsHandler = $authenticator->getLoginHandler('/');
81
        $cmsForm = $cmsHandler->loginForm();
82
        $this->assertTrue($cmsForm instanceof CMSMemberLoginForm);
83
    }
84
85
86
    /**
87
     * Test that a member can be authenticated via their temp id
88
     */
89
    public function testAuthenticateByTempID()
90
    {
91
        $authenticator = new CMSMemberAuthenticator();
92
93
        $member = new Member();
94
        $member->Email = '[email protected]';
95
        $member->PasswordEncryption = "sha1";
96
        $member->Password = "mypassword";
97
        $member->write();
98
99
        // If the user has never logged in, then the tempid should be empty
100
        $tempID = $member->TempIDHash;
101
        $this->assertEmpty($tempID);
102
103
        // If the user logs in then they have a temp id
104
        Injector::inst()->get(IdentityStore::class)->logIn($member, true);
105
        $tempID = $member->TempIDHash;
106
        $this->assertNotEmpty($tempID);
107
108
        // Test correct login
109
        $result = $authenticator->authenticate(
110
            array(
111
            'tempid' => $tempID,
112
            'Password' => 'mypassword'
113
            ),
114
            $message
115
        );
116
117
        $this->assertNotEmpty($result);
118
        $this->assertEquals($result->ID, $member->ID);
119
        $this->assertTrue($message->isValid());
120
121
        // Test incorrect login
122
        $result = $authenticator->authenticate(
123
            array(
124
            'tempid' => $tempID,
125
            'Password' => 'notmypassword'
126
            ),
127
            $message
128
        );
129
130
        $this->assertEmpty($result);
131
        $messages = $message->getMessages();
132
        $this->assertEquals(
133
            _t('SilverStripe\\Security\\Member.ERRORWRONGCRED', 'The provided details don\'t seem to be correct. Please try again.'),
134
            $messages[0]['message']
135
        );
136
    }
137
138
    /**
139
     * Test that the default admin can be authenticated
140
     */
141
    public function testDefaultAdmin()
142
    {
143
        $authenticator = new MemberAuthenticator();
144
145
        // Test correct login
146
        $result = $authenticator->authenticate(
147
            array(
148
            'Email' => 'admin',
149
            'Password' => 'password'
150
            ),
151
            $message
152
        );
153
        $this->assertNotEmpty($result);
154
        $this->assertEquals($result->Email, Security::default_admin_username());
155
        $this->assertTrue($message->isValid());
156
157
        // Test incorrect login
158
        $result = $authenticator->authenticate(
159
            array(
160
            'Email' => 'admin',
161
            'Password' => 'notmypassword'
162
            ),
163
            $message
164
        );
165
        $messages = $message->getMessages();
166
        $this->assertEmpty($result);
167
        $this->assertEquals(
168
            'The provided details don\'t seem to be correct. Please try again.',
169
            $messages[0]['message']
170
        );
171
    }
172
173
    public function testDefaultAdminLockOut()
174
    {
175
        $authenticator = new MemberAuthenticator();
176
177
        Config::inst()->update(Member::class, 'lock_out_after_incorrect_logins', 1);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Config\Coll...nfigCollectionInterface as the method update() does only exist in the following implementations of said interface: SilverStripe\Config\Coll...\MemoryConfigCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
178
        Config::inst()->update(Member::class, 'lock_out_delay_mins', 10);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Config\Coll...nfigCollectionInterface as the method update() does only exist in the following implementations of said interface: SilverStripe\Config\Coll...\MemoryConfigCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
179
        DBDatetime::set_mock_now('2016-04-18 00:00:00');
180
181
        // Test correct login
182
        $authenticator->authenticate(
183
            [
184
                'Email' => 'admin',
185
                'Password' => 'wrongpassword'
186
            ]
187
        );
188
189
        $this->assertFalse(Member::default_admin()->canLogin()->isValid());
190
        $this->assertEquals('2016-04-18 00:10:00', Member::default_admin()->LockedOutUntil);
191
    }
192
}
193