From f2ab7346aaf955f8180b063f191d615c778d8466 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Fri, 2 Aug 2019 03:29:20 +0300 Subject: [PATCH] Fixed almost everything, but all functional tests are broken at the last minute :( --- api/codeception.dist.yml | 1 + .../OAuth2/Storage/ScopeStorage.php | 2 +- api/components/Tokens/Component.php | 9 +- api/components/Tokens/TokensFactory.php | 3 +- api/components/User/Component.php | 46 +--- api/components/User/JwtIdentity.php | 15 +- .../controllers/DefaultController.php | 2 +- api/modules/accounts/models/AccountInfo.php | 2 +- .../controllers/AccountsController.php | 2 +- .../controllers/AuthorizationController.php | 2 +- .../oauth/controllers/ClientsController.php | 2 +- .../oauth/controllers/IdentityController.php | 2 +- api/modules/oauth/models/OauthProcess.php | 2 +- api/modules/session/models/JoinForm.php | 2 +- {common => api}/rbac/.generated/.gitignore | 0 api/rbac/Manager.php | 38 +++ {common => api}/rbac/Permissions.php | 4 +- {common => api}/rbac/Roles.php | 4 +- {common => api}/rbac/rules/AccountOwner.php | 11 +- .../rbac/rules/OauthClientOwner.php | 15 +- .../functional/_steps/SessionServerSteps.php | 2 +- api/tests/functional/accounts/BanCest.php | 2 +- api/tests/functional/accounts/PardonCest.php | 2 +- api/tests/functional/oauth/AuthCodeCest.php | 2 +- .../functional/oauth/RefreshTokenCest.php | 2 +- .../functional/sessionserver/JoinCest.php | 2 +- .../sessionserver/JoinLegacyCest.php | 2 +- .../unit/components/Tokens/ComponentTest.php | 10 +- .../unit/components/User/ComponentTest.php | 98 +++---- .../unit/components/User/JwtIdentityTest.php | 99 ++++--- .../models/authentication/LogoutFormTest.php | 16 +- .../authentication/RefreshTokenFormTest.php | 52 ++-- .../models/ChangePasswordFormTest.php | 21 +- .../models/EnableTwoFactorAuthFormTest.php | 12 +- .../unit/rbac/rules/AccountOwnerTest.php | 43 +-- .../unit/rbac/rules/OauthClientOwnerTest.php | 39 ++- .../PasswordRequiredValidatorTest.php | 2 +- api/validators/PasswordRequiredValidator.php | 2 +- common/codeception.dist.yml | 1 + common/config/config.php | 2 +- common/rbac/Manager.php | 34 --- composer.json | 2 +- composer.lock | 244 +++++++++++++++--- console/codeception.dist.yml | 1 + console/controllers/RbacController.php | 25 +- 45 files changed, 504 insertions(+), 377 deletions(-) rename {common => api}/rbac/.generated/.gitignore (100%) create mode 100644 api/rbac/Manager.php rename {common => api}/rbac/Permissions.php (97%) rename {common => api}/rbac/Roles.php (65%) rename {common => api}/rbac/rules/AccountOwner.php (89%) rename {common => api}/rbac/rules/OauthClientOwner.php (81%) rename {common => api}/tests/unit/rbac/rules/AccountOwnerTest.php (61%) rename {common => api}/tests/unit/rbac/rules/OauthClientOwnerTest.php (55%) delete mode 100644 common/rbac/Manager.php diff --git a/api/codeception.dist.yml b/api/codeception.dist.yml index 4f785ce..ff9f984 100644 --- a/api/codeception.dist.yml +++ b/api/codeception.dist.yml @@ -22,4 +22,5 @@ coverage: - tests/* - codeception.dist.yml - codeception.yml + - index.php c3url: 'http://localhost/api/web/index.php' diff --git a/api/components/OAuth2/Storage/ScopeStorage.php b/api/components/OAuth2/Storage/ScopeStorage.php index 85be563..6defc0c 100644 --- a/api/components/OAuth2/Storage/ScopeStorage.php +++ b/api/components/OAuth2/Storage/ScopeStorage.php @@ -3,8 +3,8 @@ namespace api\components\OAuth2\Storage; use api\components\OAuth2\Entities\ClientEntity; use api\components\OAuth2\Entities\ScopeEntity; +use api\rbac\Permissions as P; use Assert\Assert; -use common\rbac\Permissions as P; use League\OAuth2\Server\Storage\AbstractStorage; use League\OAuth2\Server\Storage\ScopeInterface; diff --git a/api/components/Tokens/Component.php b/api/components/Tokens/Component.php index a16cea4..8872e7c 100644 --- a/api/components/Tokens/Component.php +++ b/api/components/Tokens/Component.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace api\components\Tokens; +use Carbon\Carbon; use Exception; use Lcobucci\JWT\Builder; use Lcobucci\JWT\Parser; @@ -11,8 +12,6 @@ use yii\base\Component as BaseComponent; class Component extends BaseComponent { - private const EXPIRATION_TIMEOUT = 3600; // 1h - private const PREFERRED_ALGORITHM = 'ES256'; /** @@ -41,10 +40,10 @@ class Component extends BaseComponent { private $algorithmManager; public function create(array $payloads = [], array $headers = []): Token { - $time = time(); + $now = Carbon::now(); $builder = (new Builder()) - ->issuedAt($time) - ->expiresAt($time + self::EXPIRATION_TIMEOUT); + ->issuedAt($now->getTimestamp()) + ->expiresAt($now->addHour()->getTimestamp()); foreach ($payloads as $claim => $value) { $builder->withClaim($claim, $value); } diff --git a/api/components/Tokens/TokensFactory.php b/api/components/Tokens/TokensFactory.php index e92446a..00fee41 100644 --- a/api/components/Tokens/TokensFactory.php +++ b/api/components/Tokens/TokensFactory.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace api\components\Tokens; +use Carbon\Carbon; use common\models\Account; use common\models\AccountSession; use Lcobucci\JWT\Token; @@ -20,7 +21,7 @@ class TokensFactory { if ($session === null) { // If we don't remember a session, the token should live longer // so that the session doesn't end while working with the account - $payloads['exp'] = time() + 60 * 60 * 24 * 7; // 7d + $payloads['exp'] = Carbon::now()->addDays(7)->getTimestamp(); } else { $payloads['jti'] = $session->id; } diff --git a/api/components/User/Component.php b/api/components/User/Component.php index 955a857..bf00f9c 100644 --- a/api/components/User/Component.php +++ b/api/components/User/Component.php @@ -5,10 +5,6 @@ namespace api\components\User; use common\models\Account; use common\models\AccountSession; -use Exception; -use InvalidArgumentException; -use Yii; -use yii\web\UnauthorizedHttpException; use yii\web\User as YiiUserComponent; /** @@ -38,29 +34,11 @@ class Component extends YiiUserComponent { */ public $identityClass = IdentityFactory::class; - public function findIdentityByAccessToken($accessToken): ?IdentityInterface { - if (empty($accessToken)) { - return null; - } - - try { - return IdentityFactory::findIdentityByAccessToken($accessToken); - } catch (UnauthorizedHttpException $e) { - // TODO: if this exception is catched there, how it forms "Token expired" exception? - // Do nothing. It's okay to catch this. - } catch (Exception $e) { - Yii::error($e); - } - - return null; - } - /** * The method searches AccountSession model, which one has been used to create current JWT token. * null will be returned in case when any of the following situations occurred: * - The user isn't authorized - * - There is no header with a token - * - Token validation isn't passed and some exception has been thrown + * - The user isn't authorized via JWT token * - No session key found in the token. This is possible if the user chose not to remember me * or just some old tokens, without the support of saving the used session * @@ -71,18 +49,13 @@ class Component extends YiiUserComponent { return null; } - $bearer = $this->getBearerToken(); - if ($bearer === null) { + /** @var IdentityInterface $identity */ + $identity = $this->getIdentity(); + if (!$identity instanceof JwtIdentity) { return null; } - try { - $token = Yii::$app->tokens->parse($bearer); - } catch (InvalidArgumentException $e) { - return null; - } - - $sessionId = $token->getClaim('jti', false); + $sessionId = $identity->getToken()->getClaim('jti', false); if ($sessionId === false) { return null; } @@ -111,13 +84,4 @@ class Component extends YiiUserComponent { } } - private function getBearerToken(): ?string { - $authHeader = Yii::$app->request->getHeaders()->get('Authorization'); - if ($authHeader === null || !preg_match('/^Bearer\s+(.*?)$/', $authHeader, $matches)) { - return null; - } - - return $matches[1]; - } - } diff --git a/api/components/User/JwtIdentity.php b/api/components/User/JwtIdentity.php index 479351f..e327e32 100644 --- a/api/components/User/JwtIdentity.php +++ b/api/components/User/JwtIdentity.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace api\components\User; use api\components\Tokens\TokensFactory; +use Carbon\Carbon; use common\models\Account; use Exception; use Lcobucci\JWT\Token; @@ -36,22 +37,27 @@ class JwtIdentity implements IdentityInterface { throw new UnauthorizedHttpException('Incorrect token'); } - if ($token->isExpired()) { + $now = Carbon::now(); + if ($token->isExpired($now)) { throw new UnauthorizedHttpException('Token expired'); } - if (!$token->validate(new ValidationData())) { + if (!$token->validate(new ValidationData($now->getTimestamp()))) { throw new UnauthorizedHttpException('Incorrect token'); } $sub = $token->getClaim('sub', false); - if ($sub !== false && strpos($sub, TokensFactory::SUB_ACCOUNT_PREFIX) !== 0) { + if ($sub !== false && strpos((string)$sub, TokensFactory::SUB_ACCOUNT_PREFIX) !== 0) { throw new UnauthorizedHttpException('Incorrect token'); } return new self($token); } + public function getToken(): Token { + return $this->token; + } + public function getAccount(): ?Account { $subject = $this->token->getClaim('sub', false); if ($subject === false) { @@ -77,6 +83,7 @@ class JwtIdentity implements IdentityInterface { return (string)$this->token; } + // @codeCoverageIgnoreStart public function getAuthKey() { throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth'); } @@ -89,4 +96,6 @@ class JwtIdentity implements IdentityInterface { throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth'); } + // @codeCoverageIgnoreEnd + } diff --git a/api/modules/accounts/controllers/DefaultController.php b/api/modules/accounts/controllers/DefaultController.php index b9ac258..911d434 100644 --- a/api/modules/accounts/controllers/DefaultController.php +++ b/api/modules/accounts/controllers/DefaultController.php @@ -5,8 +5,8 @@ use api\controllers\Controller; use api\modules\accounts\actions; use api\modules\accounts\models\AccountInfo; use api\modules\accounts\models\TwoFactorAuthInfo; +use api\rbac\Permissions as P; use common\models\Account; -use common\rbac\Permissions as P; use Yii; use yii\filters\AccessControl; use yii\helpers\ArrayHelper; diff --git a/api/modules/accounts/models/AccountInfo.php b/api/modules/accounts/models/AccountInfo.php index 87b486a..0086cf9 100644 --- a/api/modules/accounts/models/AccountInfo.php +++ b/api/modules/accounts/models/AccountInfo.php @@ -2,8 +2,8 @@ namespace api\modules\accounts\models; use api\models\base\BaseAccountForm; +use api\rbac\Permissions as P; use common\models\Account; -use common\rbac\Permissions as P; use yii\di\Instance; use yii\web\User; diff --git a/api/modules/internal/controllers/AccountsController.php b/api/modules/internal/controllers/AccountsController.php index 92bf23c..abea541 100644 --- a/api/modules/internal/controllers/AccountsController.php +++ b/api/modules/internal/controllers/AccountsController.php @@ -2,8 +2,8 @@ namespace api\modules\internal\controllers; use api\controllers\Controller; +use api\rbac\Permissions as P; use common\models\Account; -use common\rbac\Permissions as P; use yii\filters\AccessControl; use yii\helpers\ArrayHelper; use yii\web\BadRequestHttpException; diff --git a/api/modules/oauth/controllers/AuthorizationController.php b/api/modules/oauth/controllers/AuthorizationController.php index 03d073f..51b1ae4 100644 --- a/api/modules/oauth/controllers/AuthorizationController.php +++ b/api/modules/oauth/controllers/AuthorizationController.php @@ -3,7 +3,7 @@ namespace api\modules\oauth\controllers; use api\controllers\Controller; use api\modules\oauth\models\OauthProcess; -use common\rbac\Permissions as P; +use api\rbac\Permissions as P; use Yii; use yii\filters\AccessControl; use yii\helpers\ArrayHelper; diff --git a/api/modules/oauth/controllers/ClientsController.php b/api/modules/oauth/controllers/ClientsController.php index 485c1c5..9439376 100644 --- a/api/modules/oauth/controllers/ClientsController.php +++ b/api/modules/oauth/controllers/ClientsController.php @@ -7,9 +7,9 @@ use api\modules\oauth\exceptions\UnsupportedOauthClientType; use api\modules\oauth\models\OauthClientForm; use api\modules\oauth\models\OauthClientFormFactory; use api\modules\oauth\models\OauthClientTypeForm; +use api\rbac\Permissions as P; use common\models\Account; use common\models\OauthClient; -use common\rbac\Permissions as P; use Yii; use yii\filters\AccessControl; use yii\helpers\ArrayHelper; diff --git a/api/modules/oauth/controllers/IdentityController.php b/api/modules/oauth/controllers/IdentityController.php index 5f0f952..034e8c5 100644 --- a/api/modules/oauth/controllers/IdentityController.php +++ b/api/modules/oauth/controllers/IdentityController.php @@ -3,7 +3,7 @@ namespace api\modules\oauth\controllers; use api\controllers\Controller; use api\modules\oauth\models\IdentityInfo; -use common\rbac\Permissions as P; +use api\rbac\Permissions as P; use Yii; use yii\filters\AccessControl; use yii\helpers\ArrayHelper; diff --git a/api/modules/oauth/models/OauthProcess.php b/api/modules/oauth/models/OauthProcess.php index d282c95..ee0ebd6 100644 --- a/api/modules/oauth/models/OauthProcess.php +++ b/api/modules/oauth/models/OauthProcess.php @@ -5,9 +5,9 @@ use api\components\OAuth2\Exception\AcceptRequiredException; use api\components\OAuth2\Exception\AccessDeniedException; use api\components\OAuth2\Grants\AuthCodeGrant; use api\components\OAuth2\Grants\AuthorizeParams; +use api\rbac\Permissions as P; use common\models\Account; use common\models\OauthClient; -use common\rbac\Permissions as P; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\InvalidGrantException; use League\OAuth2\Server\Exception\OAuthException; diff --git a/api/modules/session/models/JoinForm.php b/api/modules/session/models/JoinForm.php index 0361ae5..bc4e2ad 100644 --- a/api/modules/session/models/JoinForm.php +++ b/api/modules/session/models/JoinForm.php @@ -6,10 +6,10 @@ use api\modules\session\exceptions\IllegalArgumentException; use api\modules\session\models\protocols\JoinInterface; use api\modules\session\Module as Session; use api\modules\session\validators\RequiredValidator; +use api\rbac\Permissions as P; use common\helpers\StringHelper; use common\models\Account; use common\models\MinecraftAccessKey; -use common\rbac\Permissions as P; use Ramsey\Uuid\Uuid; use Yii; use yii\base\ErrorException; diff --git a/common/rbac/.generated/.gitignore b/api/rbac/.generated/.gitignore similarity index 100% rename from common/rbac/.generated/.gitignore rename to api/rbac/.generated/.gitignore diff --git a/api/rbac/Manager.php b/api/rbac/Manager.php new file mode 100644 index 0000000..9829158 --- /dev/null +++ b/api/rbac/Manager.php @@ -0,0 +1,38 @@ +user->getIdentity(); + if ($identity === null) { + return []; + } + + /** @noinspection NullPointerExceptionInspection */ + $rawPermissions = $identity->getAssignedPermissions(); + $result = []; + foreach ($rawPermissions as $name) { + $result[$name] = new Assignment(['roleName' => $name]); + } + + return $result; + } + +} diff --git a/common/rbac/Permissions.php b/api/rbac/Permissions.php similarity index 97% rename from common/rbac/Permissions.php rename to api/rbac/Permissions.php index 1b0b710..914905c 100644 --- a/common/rbac/Permissions.php +++ b/api/rbac/Permissions.php @@ -1,5 +1,7 @@ user->findIdentityByAccessToken($accessToken); + $identity = Yii::$app->user->getIdentity(); if ($identity === null) { return false; } diff --git a/common/rbac/rules/OauthClientOwner.php b/api/rbac/rules/OauthClientOwner.php similarity index 81% rename from common/rbac/rules/OauthClientOwner.php rename to api/rbac/rules/OauthClientOwner.php index b03636a..942dc13 100644 --- a/common/rbac/rules/OauthClientOwner.php +++ b/api/rbac/rules/OauthClientOwner.php @@ -1,10 +1,11 @@ execute($accessToken, $item, ['accountId' => $accountId]); } - $clientId = $params['clientId'] ?? null; - if ($clientId === null) { - return false; - } - + Assert::keyExists($params, 'clientId'); /** @var OauthClient|null $client */ - $client = OauthClient::findOne($clientId); + $client = OauthClient::findOne(['id' => $params['clientId']]); if ($client === null) { return true; } - $identity = Yii::$app->user->findIdentityByAccessToken($accessToken); + $identity = Yii::$app->user->getIdentity(); if ($identity === null) { return false; } diff --git a/api/tests/functional/_steps/SessionServerSteps.php b/api/tests/functional/_steps/SessionServerSteps.php index f33e2c8..00e8071 100644 --- a/api/tests/functional/_steps/SessionServerSteps.php +++ b/api/tests/functional/_steps/SessionServerSteps.php @@ -1,9 +1,9 @@ component = Yii::$app->tokens; - } - public function testCreate() { // Run without any arguments $token = $this->component->create(); @@ -80,6 +75,11 @@ class ComponentTest extends TestCase { ]; } + protected function _setUp() { + parent::_setUp(); + $this->component = Yii::$app->tokens; + } + private function assertValidParsedToken(Token $token, string $expectedAlg) { $this->assertSame($expectedAlg, $token->getHeader('alg')); $this->assertSame(1564527476, $token->getClaim('iat')); diff --git a/api/tests/unit/components/User/ComponentTest.php b/api/tests/unit/components/User/ComponentTest.php index 4232c39..1d9ac6e 100644 --- a/api/tests/unit/components/User/ComponentTest.php +++ b/api/tests/unit/components/User/ComponentTest.php @@ -4,17 +4,16 @@ declare(strict_types=1); namespace codeception\api\unit\components\User; use api\components\User\Component; -use api\components\User\IdentityFactory; +use api\components\User\JwtIdentity; +use api\components\User\Oauth2Identity; use api\tests\unit\TestCase; use common\models\Account; use common\models\AccountSession; use common\tests\fixtures\AccountFixture; use common\tests\fixtures\AccountSessionFixture; use common\tests\fixtures\MinecraftAccessKeyFixture; -use Emarref\Jwt\Claim; -use Emarref\Jwt\Jwt; -use Yii; -use yii\web\Request; +use Lcobucci\JWT\Claim\Basic; +use Lcobucci\JWT\Token; class ComponentTest extends TestCase { @@ -36,53 +35,37 @@ class ComponentTest extends TestCase { ]; } - // TODO: move test to refresh token form - public function testRenewJwtAuthenticationToken() { - $userIP = '192.168.0.1'; - $this->mockRequest($userIP); - /** @var AccountSession $session */ - $session = $this->tester->grabFixture('sessions', 'admin'); - $result = $this->component->renewJwtAuthenticationToken($session); - $this->assertSame($session, $result->getSession()); - $this->assertSame($session->account_id, $result->getAccount()->id); - $session->refresh(); // reload data from db - $this->assertEqualsWithDelta(time(), $session->last_refreshed_at, 3); - $this->assertSame($userIP, $session->getReadableIp()); - $payloads = (new Jwt())->deserialize($result->getJwt())->getPayload(); - /** @noinspection NullPointerExceptionInspection */ - $this->assertEqualsWithDelta(time(), $payloads->findClaimByName(Claim\IssuedAt::NAME)->getValue(), 3); - /** @noinspection NullPointerExceptionInspection */ - $this->assertEqualsWithDelta(time() + 3600, $payloads->findClaimByName('exp')->getValue(), 3); - /** @noinspection NullPointerExceptionInspection */ - $this->assertSame('ely|1', $payloads->findClaimByName('sub')->getValue()); - /** @noinspection NullPointerExceptionInspection */ - $this->assertSame('accounts_web_user', $payloads->findClaimByName('ely-scopes')->getValue()); - /** @noinspection NullPointerExceptionInspection */ - $this->assertSame($session->id, $payloads->findClaimByName('jti')->getValue(), 'session has not changed'); - } - public function testGetActiveSession() { - /** @var Account $account */ - $account = $this->tester->grabFixture('accounts', 'admin'); - /** @var AccountSession $session */ - $session = $this->tester->grabFixture('sessions', 'admin'); - $token = $this->component->createJwtAuthenticationToken($account, $session); - $jwt = $this->component->serializeToken($token); + // User is guest + $component = new Component(); + $this->assertNull($component->getActiveSession()); - /** @var Component|\PHPUnit\Framework\MockObject\MockObject $component */ - $component = $this->getMockBuilder(Component::class) - ->setMethods(['getIsGuest']) - ->getMock(); + // Identity is a Oauth2Identity + $component->setIdentity(mock(Oauth2Identity::class)); + $this->assertNull($component->getActiveSession()); - $component - ->method('getIsGuest') - ->willReturn(false); + // Identity is correct, but have no jti claim + /** @var JwtIdentity|\Mockery\MockInterface $identity */ + $identity = mock(JwtIdentity::class); + $identity->shouldReceive('getToken')->andReturn(new Token()); + $component->setIdentity($identity); + $this->assertNull($component->getActiveSession()); - $this->mockAuthorizationHeader($jwt); + // Identity is correct and has jti claim, but there is no associated session + /** @var JwtIdentity|\Mockery\MockInterface $identity */ + $identity = mock(JwtIdentity::class); + $identity->shouldReceive('getToken')->andReturn(new Token([], ['jti' => new Basic('jti', 999999)])); + $component->setIdentity($identity); + $this->assertNull($component->getActiveSession()); - $foundSession = $component->getActiveSession(); - $this->assertInstanceOf(AccountSession::class, $foundSession); - $this->assertSame($session->id, $foundSession->id); + // Identity is correct, has jti claim and associated session exists + /** @var JwtIdentity|\Mockery\MockInterface $identity */ + $identity = mock(JwtIdentity::class); + $identity->shouldReceive('getToken')->andReturn(new Token([], ['jti' => new Basic('jti', 1)])); + $component->setIdentity($identity); + $session = $component->getActiveSession(); + $this->assertNotNull($session); + $this->assertSame(1, $session->id); } public function testTerminateSessions() { @@ -95,7 +78,6 @@ class ComponentTest extends TestCase { /** @var Account $account */ $account = $this->tester->grabFixture('accounts', 'admin'); - $component->createJwtAuthenticationToken($account); // Dry run: no sessions should be removed $component->terminateSessions($account, Component::KEEP_MINECRAFT_SESSIONS | Component::KEEP_SITE_SESSIONS); @@ -119,24 +101,4 @@ class ComponentTest extends TestCase { $this->assertEmpty($account->getMinecraftAccessKeys()->all()); } - private function mockRequest($userIP = '127.0.0.1') { - /** @var Request|\Mockery\MockInterface $request */ - $request = mock(Request::class . '[getHostInfo,getUserIP]')->makePartial(); - $request->shouldReceive('getHostInfo')->andReturn('http://localhost'); - $request->shouldReceive('getUserIP')->andReturn($userIP); - - Yii::$app->set('request', $request); - } - - /** - * @param string $bearerToken - */ - private function mockAuthorizationHeader($bearerToken = null) { - if ($bearerToken !== null) { - $bearerToken = 'Bearer ' . $bearerToken; - } - - Yii::$app->request->headers->set('Authorization', $bearerToken); - } - } diff --git a/api/tests/unit/components/User/JwtIdentityTest.php b/api/tests/unit/components/User/JwtIdentityTest.php index 828eaf4..ef8c082 100644 --- a/api/tests/unit/components/User/JwtIdentityTest.php +++ b/api/tests/unit/components/User/JwtIdentityTest.php @@ -5,9 +5,9 @@ namespace codeception\api\unit\components\User; use api\components\User\JwtIdentity; use api\tests\unit\TestCase; +use Carbon\Carbon; use common\tests\fixtures\AccountFixture; -use Emarref\Jwt\Claim\Expiration as ExpirationClaim; -use Yii; +use yii\web\UnauthorizedHttpException; class JwtIdentityTest extends TestCase { @@ -18,40 +18,77 @@ class JwtIdentityTest extends TestCase { } public function testFindIdentityByAccessToken() { - $token = $this->generateToken(); + $token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ.4Oidvuo4spvUf9hkpHR72eeqZUh2Zbxh_L8Od3vcgTj--0iOrcOEp6zwmEW6vF7BTHtjz2b3mXce61bqsCjXjQ'; + /** @var JwtIdentity $identity */ $identity = JwtIdentity::findIdentityByAccessToken($token); $this->assertSame($token, $identity->getId()); - $this->assertSame($this->tester->grabFixture('accounts', 'admin')['id'], $identity->getAccount()->id); - } - - /** - * @expectedException \yii\web\UnauthorizedHttpException - * @expectedExceptionMessage Token expired - */ - public function testFindIdentityByAccessTokenWithExpiredToken() { - $expiredToken = $this->generateToken(time() - 3600); - JwtIdentity::findIdentityByAccessToken($expiredToken); - } - - /** - * @expectedException \yii\web\UnauthorizedHttpException - * @expectedExceptionMessage Incorrect token - */ - public function testFindIdentityByAccessTokenWithEmptyToken() { - JwtIdentity::findIdentityByAccessToken(''); - } - - private function generateToken(int $expiresAt = null): string { - /** @var \api\components\User\Component $component */ - $component = Yii::$app->user; + $this->assertSame($token, (string)$identity->getToken()); /** @var \common\models\Account $account */ $account = $this->tester->grabFixture('accounts', 'admin'); - $token = $component->createJwtAuthenticationToken($account); - if ($expiresAt !== null) { - $token->addClaim(new ExpirationClaim($expiresAt)); - } + $this->assertSame($account->id, $identity->getAccount()->id); + } - return $component->serializeToken($token); + /** + * @dataProvider getFindIdentityByAccessTokenInvalidCases + */ + public function testFindIdentityByAccessTokenInvalidCases(string $token, string $expectedExceptionMessage) { + $this->expectException(UnauthorizedHttpException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + JwtIdentity::findIdentityByAccessToken($token); + } + + public function getFindIdentityByAccessTokenInvalidCases() { + yield 'expired token' => [ + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MDMzNDIsImV4cCI6MTU2NDYwNjk0Miwic3ViIjoiZWx5fDEifQ.36cDWyiXRArv-lgK_S5dyC5m_Ddytwkb78tMrxcPcbWEpoeg2VtwPC7zr6NI0cd0CuLw6InC2hZ9Ey95SSOsHw', + 'Token expired', + ]; + yield 'iat from future' => [ + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTc3NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ._6hj6XUSmSLibgT9ZE1Pokf4oI9r-d6tEc1z2J-fBlr1710Qiso5yNcXqb3Z_xy7Qtemyq8jOlOZA8DvmkVBrg', + 'Incorrect token', + ]; + yield 'invalid signature' => [ + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ.yth31f2PyhUkYSfBlizzUXWIgOvxxk8gNP-js0z8g1OT5rig40FPTIkgsZRctAwAAlj6QoIWW7-hxLTcSb2vmw', + 'Incorrect token', + ]; + yield 'invalid sub' => [ + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoxMjM0fQ.yigP5nWFdX0ktbuZC_Unb9bWxpAVd7Nv8Fb1Vsa0t5WkVA88VbhPi2P-CenbDOy8ngwoGV9m3c3upMs2V3gqvw', + 'Incorrect token', + ]; + yield 'empty token' => ['', 'Incorrect token']; + } + + public function testGetAccount() { + // Token with sub claim + $identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ.4Oidvuo4spvUf9hkpHR72eeqZUh2Zbxh_L8Od3vcgTj--0iOrcOEp6zwmEW6vF7BTHtjz2b3mXce61bqsCjXjQ'); + $this->assertSame(1, $identity->getAccount()->id); + + // Sub presented, but account not exists + $identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDk5OTk5In0.1pAnhkR-_ZqzjLBR-PNIMJUXRSUK3aYixrFNKZg2ynPNPiDvzh8U-iBTT6XRfMP5nvfXZucRpoPVoiXtx40CUQ'); + $this->assertNull($identity->getAccount()); + + // Token without sub claim + $identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Mn0.QxmYgSflZOQmhzYRr8bowU767yu4yKgTVaho0MPuyCmUfZO_0O0SQASMKVILf-wlT0ODTTG7vD753a2MTAmPmw'); + $this->assertNull($identity->getAccount()); + } + + public function testGetAssignedPermissions() { + // Token with ely-scopes claim + $identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoicGVybTEscGVybTIscGVybTMiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ.MO6T92EOFcZSPIdK8VBUG0qyV-pdayzOPQmpWLPwpl1933E9ann9GdV49piX1IfLHeCHVGThm5_v7AJgyZ5Oaw'); + $this->assertSame(['perm1', 'perm2', 'perm3'], $identity->getAssignedPermissions()); + + // Token without sub claim + $identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ.jsjv2dDetSxu4xivlHoTeDUhqsl-cxSI6SktufJhwR9wqDgQCVIONiqQCUzTzyTwyAz4Ztvel4lKjMCstdJOEw'); + $this->assertSame([], $identity->getAssignedPermissions()); + } + + protected function _before() { + parent::_before(); + Carbon::setTestNow(Carbon::create(2019, 8, 1, 1, 2, 22, 'Europe/Minsk')); + } + + protected function _after() { + parent::_after(); + Carbon::setTestNow(); } } diff --git a/api/tests/unit/models/authentication/LogoutFormTest.php b/api/tests/unit/models/authentication/LogoutFormTest.php index e5426f9..9608b52 100644 --- a/api/tests/unit/models/authentication/LogoutFormTest.php +++ b/api/tests/unit/models/authentication/LogoutFormTest.php @@ -1,8 +1,9 @@ specify('No actions if active session is not exists', function() { $userComp = $this ->getMockBuilder(Component::class) - ->setConstructorArgs([$this->getComponentArgs()]) ->setMethods(['getActiveSession']) ->getMock(); $userComp @@ -42,7 +42,6 @@ class LogoutFormTest extends TestCase { $userComp = $this ->getMockBuilder(Component::class) - ->setConstructorArgs([$this->getComponentArgs()]) ->setMethods(['getActiveSession']) ->getMock(); $userComp @@ -57,15 +56,4 @@ class LogoutFormTest extends TestCase { }); } - private function getComponentArgs() { - return [ - 'identityClass' => IdentityFactory::class, - 'enableSession' => false, - 'loginUrl' => null, - 'secret' => 'secret', - 'publicKeyPath' => 'data/certs/public.crt', - 'privateKeyPath' => 'data/certs/private.key', - ]; - } - } diff --git a/api/tests/unit/models/authentication/RefreshTokenFormTest.php b/api/tests/unit/models/authentication/RefreshTokenFormTest.php index 9a89703..4d26c96 100644 --- a/api/tests/unit/models/authentication/RefreshTokenFormTest.php +++ b/api/tests/unit/models/authentication/RefreshTokenFormTest.php @@ -8,6 +8,8 @@ use api\tests\unit\TestCase; use Codeception\Specify; use common\models\AccountSession; use common\tests\fixtures\AccountSessionFixture; +use Yii; +use yii\web\Request; class RefreshTokenFormTest extends TestCase { use Specify; @@ -18,34 +20,36 @@ class RefreshTokenFormTest extends TestCase { ]; } - public function testValidateRefreshToken() { - $this->specify('error.refresh_token_not_exist if passed token not exists', function() { - /** @var RefreshTokenForm $model */ - $model = new class extends RefreshTokenForm { - public function getSession() { - return null; - } - }; - $model->validateRefreshToken(); - $this->assertSame(['error.refresh_token_not_exist'], $model->getErrors('refresh_token')); - }); + public function testRenew() { + /** @var Request|\Mockery\MockInterface $request */ + $request = mock(Request::class . '[getUserIP]')->makePartial(); + $request->shouldReceive('getUserIP')->andReturn('10.1.2.3'); + Yii::$app->set('request', $request); - $this->specify('no errors if token exists', function() { - /** @var RefreshTokenForm $model */ - $model = new class extends RefreshTokenForm { - public function getSession() { - return new AccountSession(); - } - }; - $model->validateRefreshToken(); - $this->assertEmpty($model->getErrors('refresh_token')); - }); + $model = new RefreshTokenForm(); + $model->refresh_token = 'SOutIr6Seeaii3uqMVy3Wan8sKFVFrNz'; + $result = $model->renew(); + $this->assertNotNull($result); + $this->assertSame('SOutIr6Seeaii3uqMVy3Wan8sKFVFrNz', $result->getRefreshToken()); + + $token = $result->getToken(); + $this->assertSame('ely|1', $token->getClaim('sub')); + $this->assertSame('accounts_web_user', $token->getClaim('ely-scopes')); + $this->assertEqualsWithDelta(time(), $token->getClaim('iat'), 5); + $this->assertEqualsWithDelta(time() + 3600, $token->getClaim('exp'), 5); + $this->assertSame(1, $token->getClaim('jti')); + + /** @var AccountSession $session */ + $session = AccountSession::findOne(['refresh_token' => 'SOutIr6Seeaii3uqMVy3Wan8sKFVFrNz']); + $this->assertEqualsWithDelta(time(), $session->last_refreshed_at, 5); + $this->assertSame('10.1.2.3', $session->getReadableIp()); } - public function testRenew() { + public function testRenewWithInvalidRefreshToken() { $model = new RefreshTokenForm(); - $model->refresh_token = $this->tester->grabFixture('sessions', 'admin')['refresh_token']; - $this->assertNotNull($model->renew()); + $model->refresh_token = 'unknown refresh token'; + $this->assertNull($model->renew()); + $this->assertSame(['error.refresh_token_not_exist'], $model->getErrors('refresh_token')); } } diff --git a/api/tests/unit/modules/accounts/models/ChangePasswordFormTest.php b/api/tests/unit/modules/accounts/models/ChangePasswordFormTest.php index 803bea3..67dd574 100644 --- a/api/tests/unit/modules/accounts/models/ChangePasswordFormTest.php +++ b/api/tests/unit/modules/accounts/models/ChangePasswordFormTest.php @@ -1,8 +1,9 @@ IdentityFactory::class, - 'enableSession' => false, - 'loginUrl' => null, - 'secret' => 'secret', - 'publicKeyPath' => 'data/certs/public.crt', - 'privateKeyPath' => 'data/certs/private.key', - ]]); + $component = mock(Component::class . '[terminateSessions]'); $component->shouldNotReceive('terminateSessions'); Yii::$app->set('user', $component); @@ -118,14 +112,7 @@ class ChangePasswordFormTest extends TestCase { $account->setPassword('password_0'); /** @var Component|\Mockery\MockInterface $component */ - $component = mock(Component::class . '[terminateSessions]', [[ - 'identityClass' => IdentityFactory::class, - 'enableSession' => false, - 'loginUrl' => null, - 'secret' => 'secret', - 'publicKeyPath' => 'data/certs/public.crt', - 'privateKeyPath' => 'data/certs/private.key', - ]]); + $component = mock(Component::class . '[terminateSessions]'); $component->shouldReceive('terminateSessions')->once()->withArgs([$account, Component::KEEP_CURRENT_SESSION]); Yii::$app->set('user', $component); diff --git a/api/tests/unit/modules/accounts/models/EnableTwoFactorAuthFormTest.php b/api/tests/unit/modules/accounts/models/EnableTwoFactorAuthFormTest.php index 09647a4..322c908 100644 --- a/api/tests/unit/modules/accounts/models/EnableTwoFactorAuthFormTest.php +++ b/api/tests/unit/modules/accounts/models/EnableTwoFactorAuthFormTest.php @@ -1,8 +1,9 @@ otp_secret = 'mock secret'; /** @var Component|\Mockery\MockInterface $component */ - $component = mock(Component::class . '[terminateSessions]', [[ - 'identityClass' => IdentityFactory::class, - 'enableSession' => false, - 'loginUrl' => null, - 'secret' => 'secret', - 'publicKeyPath' => 'data/certs/public.crt', - 'privateKeyPath' => 'data/certs/private.key', - ]]); + $component = mock(Component::class . '[terminateSessions]'); $component->shouldReceive('terminateSessions')->withArgs([$account, Component::KEEP_CURRENT_SESSION]); Yii::$app->set('user', $component); diff --git a/common/tests/unit/rbac/rules/AccountOwnerTest.php b/api/tests/unit/rbac/rules/AccountOwnerTest.php similarity index 61% rename from common/tests/unit/rbac/rules/AccountOwnerTest.php rename to api/tests/unit/rbac/rules/AccountOwnerTest.php index dbf1c4d..065c9bb 100644 --- a/common/tests/unit/rbac/rules/AccountOwnerTest.php +++ b/api/tests/unit/rbac/rules/AccountOwnerTest.php @@ -1,12 +1,11 @@ makePartial(); - $component->shouldReceive('findIdentityByAccessToken')->andReturn(null); - - Yii::$app->set('user', $component); - - $this->assertFalse((new AccountOwner())->execute('some token', new Item(), ['accountId' => 123])); - } - public function testExecute() { $rule = new AccountOwner(); $item = new Item(); + // Identity is null + $this->assertFalse($rule->execute('some token', $item, ['accountId' => 123])); + + // Identity presented, but have no account + /** @var IdentityInterface|\Mockery\MockInterface $identity */ + $identity = mock(IdentityInterface::class); + $identity->shouldReceive('getAccount')->andReturn(null); + Yii::$app->user->setIdentity($identity); + + $this->assertFalse($rule->execute('some token', $item, ['accountId' => 123])); + + // Identity has an account $account = new Account(); $account->id = 1; $account->status = Account::STATUS_ACTIVE; $account->rules_agreement_version = LATEST_RULES_VERSION; + /** @var IdentityInterface|\Mockery\MockInterface $identity */ $identity = mock(IdentityInterface::class); $identity->shouldReceive('getAccount')->andReturn($account); - $component = mock(Component::class . '[findIdentityByAccessToken]'); - $component->makePartial(); - $component->shouldReceive('findIdentityByAccessToken')->withArgs(['token'])->andReturn($identity); + Yii::$app->user->setIdentity($identity); - Yii::$app->set('user', $component); - - $this->assertFalse($rule->execute('token', $item, [])); $this->assertFalse($rule->execute('token', $item, ['accountId' => 2])); $this->assertFalse($rule->execute('token', $item, ['accountId' => '2'])); $this->assertTrue($rule->execute('token', $item, ['accountId' => 1])); @@ -56,4 +53,12 @@ class AccountOwnerTest extends TestCase { $this->assertFalse($rule->execute('token', $item, ['accountId' => 1, 'optionalRules' => true])); } + /** + * @expectedException \InvalidArgumentException + */ + public function testExecuteWithoutAccountId() { + $rule = new AccountOwner(); + $this->assertFalse($rule->execute('token', new Item(), [])); + } + } diff --git a/common/tests/unit/rbac/rules/OauthClientOwnerTest.php b/api/tests/unit/rbac/rules/OauthClientOwnerTest.php similarity index 55% rename from common/tests/unit/rbac/rules/OauthClientOwnerTest.php rename to api/tests/unit/rbac/rules/OauthClientOwnerTest.php index 31520fb..c699415 100644 --- a/common/tests/unit/rbac/rules/OauthClientOwnerTest.php +++ b/api/tests/unit/rbac/rules/OauthClientOwnerTest.php @@ -1,13 +1,12 @@ assertTrue($rule->execute('some token', $item, ['clientId' => 'not exists client id'])); + + // Client exists, but identity is null + $this->assertFalse($rule->execute('some token', $item, ['clientId' => 'ely'])); + + // Client exists, identity presented, but have no account + /** @var IdentityInterface|\Mockery\MockInterface $identity */ + $identity = mock(IdentityInterface::class); + $identity->shouldReceive('getAccount')->andReturn(null); + Yii::$app->user->setIdentity($identity); + + $this->assertFalse($rule->execute('some token', $item, ['clientId' => 'ely'])); + + // Identity has an account $account = new Account(); $account->id = 1; $account->status = Account::STATUS_ACTIVE; @@ -34,15 +48,8 @@ class OauthClientOwnerTest extends TestCase { /** @var IdentityInterface|\Mockery\MockInterface $identity */ $identity = mock(IdentityInterface::class); $identity->shouldReceive('getAccount')->andReturn($account); + Yii::$app->user->setIdentity($identity); - /** @var Component|\Mockery\MockInterface $component */ - $component = mock(Component::class . '[findIdentityByAccessToken]'); - $component->makePartial(); - $component->shouldReceive('findIdentityByAccessToken')->withArgs(['token'])->andReturn($identity); - - Yii::$app->set('user', $component); - - $this->assertFalse($rule->execute('token', $item, [])); $this->assertTrue($rule->execute('token', $item, ['clientId' => 'admin-oauth-client'])); $this->assertTrue($rule->execute('token', $item, ['clientId' => 'not-exists-client'])); $account->id = 2; @@ -52,4 +59,12 @@ class OauthClientOwnerTest extends TestCase { $this->assertFalse($rule->execute('token', $item, ['accountId' => 1])); } + /** + * @expectedException \InvalidArgumentException + */ + public function testExecuteWithoutClientId() { + $rule = new OauthClientOwner(); + $this->assertFalse($rule->execute('token', new Item(), [])); + } + } diff --git a/api/tests/unit/validators/PasswordRequiredValidatorTest.php b/api/tests/unit/validators/PasswordRequiredValidatorTest.php index f60aed6..1c26e1d 100644 --- a/api/tests/unit/validators/PasswordRequiredValidatorTest.php +++ b/api/tests/unit/validators/PasswordRequiredValidatorTest.php @@ -1,11 +1,11 @@ api\components\OAuth2\Component::class, ], 'authManager' => [ - 'class' => common\rbac\Manager::class, + 'class' => \api\rbac\Manager::class, 'itemFile' => '@common/rbac/.generated/items.php', 'ruleFile' => '@common/rbac/.generated/rules.php', ], diff --git a/common/rbac/Manager.php b/common/rbac/Manager.php deleted file mode 100644 index 0565240..0000000 --- a/common/rbac/Manager.php +++ /dev/null @@ -1,34 +0,0 @@ -user->findIdentityByAccessToken($accessToken); - if ($identity === null) { - return []; - } - - /** @noinspection NullPointerExceptionInspection */ - $permissions = $identity->getAssignedPermissions(); - if (empty($permissions)) { - return []; - } - - return array_flip($permissions); - } - -} diff --git a/composer.json b/composer.json index 9a0d0e1..d58d470 100644 --- a/composer.json +++ b/composer.json @@ -16,12 +16,12 @@ "domnikl/statsd": "^2.6", "ely/mojang-api": "^0.2.0", "ely/yii2-tempmail-validator": "^2.0", - "emarref/jwt": "~1.0.3", "goaop/framework": "^2.2.0", "guzzlehttp/guzzle": "^6.0.0", "lcobucci/jwt": "^3.3", "league/oauth2-server": "^4.1", "mito/yii2-sentry": "^1.0", + "nesbot/carbon": "^2.22", "paragonie/constant_time_encoding": "^2.0", "ramsey/uuid": "^3.5", "spomky-labs/otphp": "^9.0.2", diff --git a/composer.lock b/composer.lock index 6271d21..0b2b56d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2dfa204a51a82cd7c7d6a5b7d1ccbc0c", + "content-hash": "2c49fce9e25e3bc27dc3ae43ac0c079b", "packages": [ { "name": "bacon/bacon-qr-code", @@ -701,48 +701,6 @@ ], "time": "2017-09-30T22:51:45+00:00" }, - { - "name": "emarref/jwt", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/emarref/jwt.git", - "reference": "79f563750ff90dabd4fa677c4b4e5ec9ed52d9b4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/emarref/jwt/zipball/79f563750ff90dabd4fa677c4b4e5ec9ed52d9b4", - "reference": "79f563750ff90dabd4fa677c4b4e5ec9ed52d9b4", - "shasum": "" - }, - "require": { - "php": ">=5.4" - }, - "require-dev": { - "phpunit/phpunit": "*" - }, - "suggest": { - "ext-openssl": "Enables more token encryption options" - }, - "type": "library", - "autoload": { - "psr-4": { - "Emarref\\Jwt\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Malcolm Fell", - "email": "emarref@gmail.com" - } - ], - "description": "A JWT implementation", - "time": "2016-09-05T20:33:06+00:00" - }, { "name": "ezyang/htmlpurifier", "version": "v4.10.0", @@ -1369,6 +1327,73 @@ ], "time": "2017-11-28T16:52:35+00:00" }, + { + "name": "nesbot/carbon", + "version": "2.22.0", + "source": { + "type": "git", + "url": "https://github.com/briannesbitt/Carbon.git", + "reference": "1a0e48b5f656065ba3c265b058b25d36c2162a5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/1a0e48b5f656065ba3c265b058b25d36c2162a5e", + "reference": "1a0e48b5f656065ba3c265b058b25d36c2162a5e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.1.8 || ^8.0", + "symfony/translation": "^3.4 || ^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.14 || ^3.0", + "kylekatarnls/multi-tester": "^1.1", + "phpmd/phpmd": "dev-php-7.1-compatibility", + "phpstan/phpstan": "^0.11", + "phpunit/phpunit": "^7.5 || ^8.0", + "squizlabs/php_codesniffer": "^3.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "http://nesbot.com" + }, + { + "name": "kylekatarnls", + "homepage": "http://github.com/kylekatarnls" + } + ], + "description": "A simple API extension for DateTime.", + "homepage": "http://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "time": "2019-07-28T09:02:12+00:00" + }, { "name": "nikic/php-parser", "version": "v4.2.1", @@ -1990,6 +2015,139 @@ "homepage": "https://symfony.com", "time": "2019-04-10T16:20:36+00:00" }, + { + "name": "symfony/translation", + "version": "v4.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "4e3e39cc485304f807622bdc64938e4633396406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/4e3e39cc485304f807622bdc64938e4633396406", + "reference": "4e3e39cc485304f807622bdc64938e4633396406", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^1.1.2" + }, + "conflict": { + "symfony/config": "<3.4", + "symfony/dependency-injection": "<3.4", + "symfony/yaml": "<3.4" + }, + "provide": { + "symfony/translation-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.4|~4.0", + "symfony/console": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/finder": "~2.8|~3.0|~4.0", + "symfony/http-kernel": "~3.4|~4.0", + "symfony/intl": "~3.4|~4.0", + "symfony/service-contracts": "^1.1.2", + "symfony/var-dumper": "~3.4|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "psr/log-implementation": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Translation Component", + "homepage": "https://symfony.com", + "time": "2019-07-18T10:34:59+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v1.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "cb4b18ad7b92a26e83b65dde940fab78339e6f3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/cb4b18ad7b92a26e83b65dde940fab78339e6f3c", + "reference": "cb4b18ad7b92a26e83b65dde940fab78339e6f3c", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "suggest": { + "symfony/translation-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2019-06-13T11:15:36+00:00" + }, { "name": "webmozart/assert", "version": "1.4.0", diff --git a/console/codeception.dist.yml b/console/codeception.dist.yml index 673cb42..9aed054 100644 --- a/console/codeception.dist.yml +++ b/console/codeception.dist.yml @@ -18,6 +18,7 @@ coverage: - config/* - runtime/* - migrations/* + - tests/* - views/* - codeception.dist.yml - codeception.yml diff --git a/console/controllers/RbacController.php b/console/controllers/RbacController.php index 66f9a7c..2d0b8a0 100644 --- a/console/controllers/RbacController.php +++ b/console/controllers/RbacController.php @@ -1,13 +1,14 @@ getAuthManager(); $role = $authManager->createRole($name); - if (!$authManager->add($role)) { - throw new ErrorException('Cannot save role in authManager'); - } + Assert::true($authManager->add($role), 'Cannot save role in authManager'); return $role; } @@ -96,9 +95,7 @@ class RbacController extends Controller { $permission = $authManager->createPermission($name); if ($ruleClassName !== null) { $rule = new $ruleClassName(); - if (!$rule instanceof Rule) { - throw new InvalidArgumentException('ruleClassName must be rule class name'); - } + Assert::isInstanceOf($rule, Rule::class, 'ruleClassName must be rule class name'); $ruleFromAuthManager = $authManager->getRule($rule->name); if ($ruleFromAuthManager === null) { @@ -108,9 +105,7 @@ class RbacController extends Controller { $permission->ruleName = $rule->name; } - if (!$authManager->add($permission)) { - throw new ErrorException('Cannot save permission in authManager'); - } + Assert::true($authManager->add($permission), 'Cannot save permission in authManager'); return $permission; }