diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index df240a5..7dac620 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -171,9 +171,15 @@ Docker: - docker push $WEB_LATEST_IMAGE_NAME - docker push $DB_VERSIONED_IMAGE_NAME - docker push $DB_LATEST_IMAGE_NAME - only: - - master - - tags + rules: + - if: '$CI_COMMIT_TAG' + when: on_success + - if: '$CI_COMMIT_BRANCH == "master"' + when: on_success + - if: '$CI_COMMIT_MESSAGE =~ /\[deploy.*\]/' + when: on_success + # Default: + - when: never ########## # Deploy # diff --git a/api/components/Tokens/TokensFactory.php b/api/components/Tokens/TokensFactory.php index 3e5c3da..12e1213 100644 --- a/api/components/Tokens/TokensFactory.php +++ b/api/components/Tokens/TokensFactory.php @@ -54,7 +54,7 @@ class TokensFactory extends Component { public function createForMinecraftAccount(Account $account, string $clientToken): Token { return Yii::$app->tokens->create([ - 'scope' => $this->prepareScopes([P::MINECRAFT_SERVER_SESSION]), + 'scope' => $this->prepareScopes([P::OBTAIN_OWN_ACCOUNT_INFO, P::MINECRAFT_SERVER_SESSION]), 'ely-client-token' => new EncryptedValue($clientToken), 'sub' => $this->buildSub($account->id), 'exp' => Carbon::now()->addDays(2)->getTimestamp(), diff --git a/api/config/routes.php b/api/config/routes.php index ffb4222..9eccb63 100644 --- a/api/config/routes.php +++ b/api/config/routes.php @@ -46,6 +46,7 @@ return [ '/mojang/profiles/' => 'mojang/api/uuid-by-username', '/mojang/profiles//names' => 'mojang/api/usernames-by-uuid', 'POST /mojang/profiles' => 'mojang/api/uuids-by-usernames', + 'GET /mojang/services/minecraft/profile' => 'mojang/services/profile', // authlib-injector '/authlib-injector/authserver/' => 'authserver/authentication/', diff --git a/api/modules/mojang/behaviors/ServiceErrorConverterBehavior.php b/api/modules/mojang/behaviors/ServiceErrorConverterBehavior.php new file mode 100644 index 0000000..ad20134 --- /dev/null +++ b/api/modules/mojang/behaviors/ServiceErrorConverterBehavior.php @@ -0,0 +1,54 @@ + Closure::fromCallable([$this, 'beforeSend']), + ]; + } + + private function beforeSend(Event $event): void { + /** @var Response $response */ + $response = $event->sender; + $data = $response->data; + if ($data === null || !isset($data['status'])) { + return; + } + + $request = Yii::$app->request; + $type = $data['type']; + switch ($type) { + case UnauthorizedHttpException::class: + $response->data = [ + 'path' => '/' . $request->getPathInfo(), + 'errorType' => 'UnauthorizedOperationException', + 'error' => 'UnauthorizedOperationException', + 'errorMessage' => 'Unauthorized', + 'developerMessage' => 'Unauthorized', + ]; + break; + case NotFoundHttpException::class: + $response->data = [ + 'path' => '/' . $request->getPathInfo(), + 'errorType' => 'NOT_FOUND', + 'error' => 'NOT_FOUND', + 'errorMessage' => 'The server has not found anything matching the request URI', + 'developerMessage' => 'The server has not found anything matching the request URI', + ]; + break; + } + } + +} diff --git a/api/modules/mojang/controllers/ServicesController.php b/api/modules/mojang/controllers/ServicesController.php new file mode 100644 index 0000000..a6e8b3e --- /dev/null +++ b/api/modules/mojang/controllers/ServicesController.php @@ -0,0 +1,96 @@ + [ + 'class' => AccessControl::class, + 'rules' => [ + [ + 'allow' => true, + 'actions' => ['profile'], + 'roles' => [Permissions::OBTAIN_ACCOUNT_INFO], + 'roleParams' => function(): array { + $account = Yii::$app->user->identity->getAccount(); + + return [ + 'accountId' => $account ? $account->id : -1, + ]; + }, + ], + ], + ], + 'verbs' => [ + 'class' => VerbFilter::class, + 'actions' => [ + 'profile' => ['GET'], + ], + ], + ]); + } + + public function init(): void { + parent::init(); + $this->response->attachBehavior('errorFormatter', ServiceErrorConverterBehavior::class); + } + + public function actionProfile(SkinsSystemApi $skinsSystemApi): array { + $account = Yii::$app->user->identity->getAccount(); + if ($account === null) { + throw new NotFoundHttpException(); + } + + try { + $textures = $skinsSystemApi->textures($account->username); + } catch (Exception $e) { + Yii::warning('Cannot get textures from skinsystem.ely.by. Exception message is ' . $e->getMessage()); + $textures = []; + } + + $response = [ + 'id' => str_replace('-', '', $account->uuid), + 'name' => $account->username, + 'skins' => [], + 'capes' => [], + ]; + + if (isset($textures['SKIN'])) { + $response['skins'][] = [ + 'id' => v3(Uuid::NAMESPACE_URL, $textures['SKIN']['url']), + 'state' => 'ACTIVE', + 'url' => $textures['SKIN']['url'], + 'variant' => isset($textures['SKIN']['metadata']['model']) ? 'SLIM' : 'CLASSIC', + 'alias' => '', + ]; + } + + if (isset($textures['CAPE'])) { + $response['capes'][] = [ + 'id' => v3(Uuid::NAMESPACE_URL, $textures['CAPE']['url']), + 'state' => 'ACTIVE', + 'url' => $textures['CAPE']['url'], + 'alias' => '', + ]; + } + + return $response; + } + +} diff --git a/api/tests/functional/mojang/ProfileCest.php b/api/tests/functional/mojang/ProfileCest.php new file mode 100644 index 0000000..c677566 --- /dev/null +++ b/api/tests/functional/mojang/ProfileCest.php @@ -0,0 +1,58 @@ +amAuthenticated(); + $I->sendGet('/api/mojang/services/minecraft/profile'); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseContainsJson([ + 'id' => 'df936908b2e1544d96f82977ec213022', + 'name' => 'Admin', + 'skins' => [ + [ + 'id' => '1794a784-2d87-32f0-b233-0b2fd5682444', + 'state' => 'ACTIVE', + 'url' => 'http://localhost/skin.png', + 'variant' => 'CLASSIC', + 'alias' => '', + ], + ], + 'capes' => [], + ]); + } + + public function getProfileAsServiceAccount(OauthSteps $I): void { + $accessToken = $I->getAccessTokenByClientCredentialsGrant(['internal_account_info']); + $I->amBearerAuthenticated($accessToken); + + $I->sendGet('/api/mojang/services/minecraft/profile'); + $I->canSeeResponseCodeIs(404); + $I->canSeeResponseContainsJson([ + 'path' => '/mojang/services/minecraft/profile', + 'errorType' => 'NOT_FOUND', + 'error' => 'NOT_FOUND', + 'errorMessage' => 'The server has not found anything matching the request URI', + 'developerMessage' => 'The server has not found anything matching the request URI', + ]); + } + + public function getProfileWithoutAuthentication(FunctionalTester $I): void { + $I->sendGet('/api/mojang/services/minecraft/profile'); + $I->canSeeResponseCodeIs(401); + $I->canSeeResponseContainsJson([ + 'path' => '/mojang/services/minecraft/profile', + 'errorType' => 'UnauthorizedOperationException', + 'error' => 'UnauthorizedOperationException', + 'errorMessage' => 'Unauthorized', + 'developerMessage' => 'Unauthorized', + ]); + } + +} diff --git a/api/tests/unit/components/Tokens/TokensFactoryTest.php b/api/tests/unit/components/Tokens/TokensFactoryTest.php index 8932613..253c7d3 100644 --- a/api/tests/unit/components/Tokens/TokensFactoryTest.php +++ b/api/tests/unit/components/Tokens/TokensFactoryTest.php @@ -93,7 +93,7 @@ class TokensFactoryTest extends TestCase { $token = $factory->createForMinecraftAccount($account, $clientToken); $this->assertEqualsWithDelta(time(), $token->getClaim('iat'), 5); $this->assertEqualsWithDelta(time() + 60 * 60 * 24 * 2, $token->getClaim('exp'), 5); - $this->assertSame('minecraft_server_session', $token->getClaim('scope')); + $this->assertSame('obtain_own_account_info minecraft_server_session', $token->getClaim('scope')); $this->assertNotSame('e44fae79-f80e-4975-952e-47e8a9ed9472', $token->getClaim('ely-client-token')); $this->assertSame('ely|1', $token->getClaim('sub')); } diff --git a/common/models/Account.php b/common/models/Account.php index 31ab1e6..74493c3 100644 --- a/common/models/Account.php +++ b/common/models/Account.php @@ -20,7 +20,7 @@ use const common\LATEST_RULES_VERSION; /** * Fields: * @property int $id - * @property string $uuid + * @property string $uuid UUID with dashes * @property string $username * @property string $email * @property string $password_hash