Skip to content

Commit

Permalink
Use enum for different possible statuses
Browse files Browse the repository at this point in the history
  • Loading branch information
tvdijen committed May 9, 2024
1 parent 6b0567e commit b9f9a10
Show file tree
Hide file tree
Showing 10 changed files with 53 additions and 39 deletions.
14 changes: 8 additions & 6 deletions src/Auth/Source/RateLimitUserPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
UsernameLimiter,
UserPassLimiter,
};
use SimpleSAML\Module\ratelimit\PreAuthStatusEnum;
use SimpleSAML\Store\{StoreFactory, StoreInterface};

use function get_class;
Expand All @@ -42,7 +43,10 @@ class RateLimitUserPass extends UserPassBase
*/
private array $rateLimiters = [];

/** @psalm-suppress MissingClassConstType */
/**
* @var array
* psalm-suppress MissingClassConstType
*/
private const DEFAULT_CONFIG = [

Check failure on line 50 in src/Auth/Source/RateLimitUserPass.php

View workflow job for this annotation

GitHub Actions / Quality control

MissingClassConstType

src/Auth/Source/RateLimitUserPass.php:50:19: MissingClassConstType: Class constant "SimpleSAML\Module\ratelimit\Auth\Source\RateLimitUserPass::DEFAULT_CONFIG" should have a declared type. (see https://psalm.dev/359)
0 => [
'device',
Expand Down Expand Up @@ -207,16 +211,14 @@ public function allowLoginAttempt(string $username, string $password): bool
continue;
}
switch ($result) {
case 'allow':
case PreAuthStatusEnum::ALLOW:
Logger::debug(sprintf('User \'%s\' login attempt allowed by %s', $username, get_class($limiter)));
return true;
case 'block':
case PreAuthStatusEnum::BLOCK:
Logger::stats(sprintf('User \'%s\' login attempt blocked by %s', $username, get_class($limiter)));
return false;
case 'continue':
case PreAuthStatusEnum::CONTINUE:
continue 2;
default:
Logger::warning("Unrecognized ratelimit allow() value '$result'");
}
}

Expand Down
9 changes: 5 additions & 4 deletions src/Limiters/DeviceCookieLimiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace SimpleSAML\Module\ratelimit\Limiters;

use SimpleSAML\{Configuration, Logger};
use SimpleSAML\Module\ratelimit\PreAuthStatusEnum;
use SimpleSAML\Utils\HTTP;

use function sprintf;
Expand All @@ -31,18 +32,18 @@ public function getRateLimitKey(string $username, string $password): string
return "device-" . $this->checkForDeviceCookie();
}

public function allow(string $username, string $password): string
public function allow(string $username, string $password): PreAuthStatusEnum
{
if (!$this->hasDeviceCookieSet()) {
return UserPassBaseLimiter::PREAUTH_CONTINUE;
return PreAuthStatusEnum::CONTINUE;
}
$key = $this->getRateLimitKey($username, $password);
/** @var array|null $ret */
$ret = $this->getStore()->get('array', "ratelimit-$key");
if ($ret === null) {
return UserPassBaseLimiter::PREAUTH_CONTINUE;
return PreAuthStatusEnum::CONTINUE;
}
return $ret['user'] === $username ? UserPassBaseLimiter::PREAUTH_ALLOW : UserPassBaseLimiter::PREAUTH_CONTINUE;
return $ret['user'] === $username ? PreAuthStatusEnum::ALLOW : PreAuthStatusEnum::CONTINUE;
}

public function postFailure(string $username, string $password): int
Expand Down
5 changes: 3 additions & 2 deletions src/Limiters/IpLimiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace SimpleSAML\Module\ratelimit\Limiters;

use SimpleSAML\{Configuration, Logger};
use SimpleSAML\Module\ratelimit\PreAuthStatusEnum;
use Symfony\Component\HttpFoundation\{IpUtils, Request};

/**
Expand Down Expand Up @@ -32,10 +33,10 @@ public function __construct(Configuration $config)
$this->clientIpAddress = $ip;
}

public function allow(string $username, string $password): string
public function allow(string $username, string $password): PreAuthStatusEnum
{
if ($this->isIpWhiteListed($this->clientIpAddress)) {
return UserPassBaseLimiter::PREAUTH_CONTINUE;
return PreAuthStatusEnum::CONTINUE;
}
return parent::allow($username, $password);
}
Expand Down
18 changes: 5 additions & 13 deletions src/Limiters/UserPassBaseLimiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use SimpleSAML\Assert\Assert;
use SimpleSAML\Configuration;
use SimpleSAML\Module\ratelimit\PreAuthStatusEnum;
use SimpleSAML\Store\{StoreFactory, StoreInterface};
use SimpleSAML\Utils\Time;

Expand All @@ -14,15 +15,6 @@

abstract class UserPassBaseLimiter implements UserPassLimiter
{
/** @psalm-suppress MissingClassConstType */
protected const PREAUTH_ALLOW = 'allow';

/** @psalm-suppress MissingClassConstType */
protected const PREAUTH_BLOCK = 'block';

/** @psalm-suppress MissingClassConstType */
protected const PREAUTH_CONTINUE = 'continue';

/**
* @var int $limit The limit of attempts
*/
Expand Down Expand Up @@ -54,16 +46,16 @@ public function __construct(Configuration $config, string $defaultWindow = 'PT5M
* Called prior to verifying the credentials to determine if the attempt is allowed.
* @param string $username The username to check
* @param string $password The password to check
* @return string allow|block|continue
* @return \SimpleSAML\Module\ratelimit\PreAuthStatusEnum
*/
public function allow(string $username, string $password): string
public function allow(string $username, string $password): PreAuthStatusEnum
{
$key = $this->getRateLimitKey($username, $password);
$count = $this->getCurrentCount($key);
if ($count >= $this->limit) {
return UserPassBaseLimiter::PREAUTH_BLOCK;
return PreAuthStatusEnum::BLOCK;
}
return UserPassBaseLimiter::PREAUTH_CONTINUE;
return PreAuthStatusEnum::CONTINUE;
}

/**
Expand Down
6 changes: 4 additions & 2 deletions src/Limiters/UserPassLimiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace SimpleSAML\Module\ratelimit\Limiters;

use SimpleSAML\Module\ratelimit\PreAuthStatusEnum;

/**
* Allow limiting of user password authentications
* @package SimpleSAML\Module\ratelimit
Expand All @@ -14,9 +16,9 @@ interface UserPassLimiter
* Called prior to verifying the credentials to determine if the attempt is allowed.
* @param string $username The username to check
* @param string $password The password to check
* @return string allow|block|continue
* @return \SimpleSAML\Module\ratelimit\PreAuthStatusEnum
*/
public function allow(string $username, string $password): string;
public function allow(string $username, string $password): PreAuthStatusEnum;

/**
* Called after a successful authentication
Expand Down
12 changes: 12 additions & 0 deletions src/PreAuthStatusEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\Module\ratelimit;

enum PreAuthStatusEnum
{
case ALLOW;
case BLOCK;
case CONTINUE;
}
7 changes: 4 additions & 3 deletions tests/Utils/BaseLimitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPUnit\Framework\TestCase;
use SimpleSAML\Configuration;
use SimpleSAML\Module\ratelimit\Limiters\UserPassBaseLimiter;
use SimpleSAML\Module\ratelimit\PreAuthStatusEnum;
use SimpleSAML\Store\StoreFactory;
use SimpleSAML\TestUtils\InMemoryStore;

Expand Down Expand Up @@ -65,20 +66,20 @@ public function testAllowAndFailure(): void
$password = 'Beer';
for ($i = 1; $i <= 3; $i++) {
// First 3 attempts should not be blocked
$this->assertEquals('continue', $limiter->allow($username, $password), "Attempt $i");
$this->assertEquals(PreAuthStatusEnum::CONTINUE, $limiter->allow($username, $password), "Attempt $i");
$this->assertEquals($i, $limiter->postFailure($username, $password));
$this->assertEquals($i, $this->getStoreValueFor($limiter->getRateLimitKey($username, $password)));
}
// After 3 failed attempts it should be blocked
$this->assertEquals('block', $limiter->allow($username, $password));
$this->assertEquals(PreAuthStatusEnum::BLOCK, $limiter->allow($username, $password));

// Sleep until the next window, and counter should be reset
usleep(4020000);
$this->assertNull(
$this->getStoreValueFor($limiter->getRateLimitKey($username, $password)),
'Value not expected in store'
);
$this->assertEquals('continue', $limiter->allow($username, $password));
$this->assertEquals(PreAuthStatusEnum::CONTINUE, $limiter->allow($username, $password));
}

/**
Expand Down
13 changes: 7 additions & 6 deletions tests/src/Limiters/DeviceCookieLimiterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPUnit\Framework\TestCase;
use SimpleSAML\{Configuration, Utils};
use SimpleSAML\Module\ratelimit\Limiters\DeviceCookieLimiter;
use SimpleSAML\Module\ratelimit\PreAuthStatusEnum;
use SimpleSAML\TestUtils\InMemoryStore;

#[CoversClass(DeviceCookieLimiter::class)]
Expand Down Expand Up @@ -93,27 +94,27 @@ public function testDeviceCookieAllowsAuth(): void
$limiter = $this->getLimiter([]);

//expect: auth with out device cookie is continue
$this->assertEquals('continue', $limiter->allow('me', 'a'));
$this->assertEquals(PreAuthStatusEnum::CONTINUE, $limiter->allow('me', 'a'));

//given: an existing cookie in the stroe
$limiter->postSuccess('me', 'pass');
$deviceCookie = $this->getDeviceCookieFromMock();
$_COOKIE['deviceCookie'] = $deviceCookie;

$this->assertEquals(
'allow',
PreAuthStatusEnum::ALLOW,
$limiter->allow('me', 'a'),
'User matches device cookie, so is allowed'
);

$this->assertEquals(
'continue',
PreAuthStatusEnum::CONTINUE,
$limiter->allow('typoe', 'a'),
'User does not match device cookie, so go to other rules'
);
$_COOKIE['deviceCookie'] = 'not-in-store';
$this->assertEquals(
'continue',
PreAuthStatusEnum::CONTINUE,
$limiter->allow('me', 'a'),
'Device cookie not in store means go to other rules'
);
Expand Down Expand Up @@ -145,7 +146,7 @@ public function testFailedAttemptsRemoveCookie(): void
// Expect: failure for use increments count
$this->assertEquals(1, $limiter->postFailure('u', 'p'));
//sanity check that allow method still returns allow
$this->assertEquals('allow', $limiter->allow('u', 'p'));
$this->assertEquals(PreAuthStatusEnum::ALLOW, $limiter->allow('u', 'p'));

$key = $limiter->getRateLimitKey('u', 'p');
/** @var array{count: int, user: string}|null $result */
Expand All @@ -157,7 +158,7 @@ public function testFailedAttemptsRemoveCookie(): void
$this->assertEquals(2, $limiter->postFailure('u', 'p'));
$this->assertNull($store->get('array', 'ratelimit-' . $key));
//sanity check that allow method not returns continue
$this->assertEquals('continue', $limiter->allow('u', 'p'));
$this->assertEquals(PreAuthStatusEnum::CONTINUE, $limiter->allow('u', 'p'));
}

private function getDeviceCookieFromMock(): ?string
Expand Down
5 changes: 3 additions & 2 deletions tests/src/Limiters/ExceptionThrowingLimiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@

use Exception;
use SimpleSAML\Module\ratelimit\Limiters\UserPassLimiter;
use SimpleSAML\Module\ratelimit\PreAuthStatusEnum;

class ExceptionThrowingLimiter implements UserPassLimiter
{
/**
* Called prior to verifying the credentials to determine if the attempt is allowed.
* @param string $username The username to check
* @param string $password The password to check
* @return string allow|block|continue
* @return \SimpleSAML\Module\ratelimit\PreAuthStatusEnum
* @throws \Exception always thrown
*/
public function allow(string $username, string $password): string
public function allow(string $username, string $password): PreAuthStatusEnum
{
throw new Exception('Boom!');
}
Expand Down
3 changes: 2 additions & 1 deletion tests/src/Limiters/IpLimiterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPUnit\Framework\Attributes\{CoversClass, DataProvider};
use SimpleSAML\Configuration;
use SimpleSAML\Module\ratelimit\Limiters\{IpLimiter, UserPassBaseLimiter};
use SimpleSAML\Module\ratelimit\PreAuthStatusEnum;
use SimpleSAML\Test\Module\ratelimit\Utils\BaseLimitTest;

#[CoversClass(IpLimiter::class)]
Expand Down Expand Up @@ -41,7 +42,7 @@ public function testIpWhitelist(string $ip, bool $ignoreExpected): void

$limiter = $this->getLimiter($config);
$this->assertEquals($ignoreExpected ? 0 : 1, $limiter->postFailure('u', 'p'));
$this->assertEquals($ignoreExpected ? 'continue' : 'block', $limiter->allow('u', 'p'));
$this->assertEquals($ignoreExpected ? PreAuthStatusEnum::CONTINUE : PreAuthStatusEnum::BLOCK, $limiter->allow('u', 'p'));
}

public static function ipWhitelistProvider(): array
Expand Down

0 comments on commit b9f9a10

Please sign in to comment.