Merged in composer_update (pull request #21)

composer update and Sentry support
This commit is contained in:
Alexey Eschenko 2019-01-18 17:34:36 +00:00
commit 3af1d6b2ac
19 changed files with 1226 additions and 699 deletions

View file

@ -16,3 +16,7 @@ APP_SECRET=xxx
APP_DATABASE_URL=postgres://$PGUSER:$PGPASSWORD@127.0.0.1:5436/test?application_name=magnetico_web APP_DATABASE_URL=postgres://$PGUSER:$PGPASSWORD@127.0.0.1:5436/test?application_name=magnetico_web
MAGNETICOD_DATABASE_URL=sqlite:///%kernel.project_dir%/tests/database/database.sqlite3 MAGNETICOD_DATABASE_URL=sqlite:///%kernel.project_dir%/tests/database/database.sqlite3
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
###> sentry/sentry-symfony ###
SENTRY_DSN=
###< sentry/sentry-symfony ###

View file

@ -16,11 +16,11 @@
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"sensio/framework-extra-bundle": "^5.1", "sensio/framework-extra-bundle": "^5.1",
"sentry/sentry-symfony": "^2.2",
"symfony/console": "^4.1", "symfony/console": "^4.1",
"symfony/flex": "^1.0", "symfony/flex": "^1.0",
"symfony/form": "^4.1", "symfony/form": "^4.1",
"symfony/framework-bundle": "^4.1", "symfony/framework-bundle": "^4.1",
"symfony/lts": "^4@dev",
"symfony/monolog-bundle": "^3.3", "symfony/monolog-bundle": "^3.3",
"symfony/orm-pack": "^1.0", "symfony/orm-pack": "^1.0",
"symfony/security-bundle": "^4.1", "symfony/security-bundle": "^4.1",

1650
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -12,4 +12,5 @@ return [
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Sentry\SentryBundle\SentryBundle::class => ['all' => true],
]; ];

View file

@ -0,0 +1,2 @@
sentry:
dsn: '%env(SENTRY_DSN)%'

View file

@ -6,8 +6,6 @@ security:
class: App\Entity\User class: App\Entity\User
property: username property: username
manager_name: default manager_name: default
api_token_provider:
id: App\Security\ApiTokenUserProvider
encoders: encoders:
App\Entity\User: App\Entity\User:
algorithm: 'argon2i' algorithm: 'argon2i'
@ -22,9 +20,9 @@ security:
pattern: ^/api/ pattern: ^/api/
anonymous: ~ anonymous: ~
stateless: true stateless: true
simple_preauth: guard:
authenticator: App\Security\ApiTokenAuthenticator authenticators:
provider: api_token_provider - App\Security\ApiTokenAuthenticator
main: main:
pattern: ^/ pattern: ^/
anonymous: ~ anonymous: ~

View file

@ -0,0 +1,8 @@
sentry:
options:
curl_method: async
# skip_capture: # To skip certain exceptions, specify a list below
# - 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException'
# - 'Symfony\Component\HttpKernel\Exception\BadRequestHttpException'
# - 'Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException'

View file

@ -43,4 +43,4 @@ services:
# Torrent searcher # Torrent searcher
App\Search\TorrentSearcher: App\Search\TorrentSearcher:
arguments: arguments:
$metadataFactory: '@doctrine.orm.magneticod_entity_manager.metadata_factory' $em: '@doctrine.orm.magneticod_entity_manager'

View file

@ -3,10 +3,10 @@
namespace App\Api\V1\Controller; namespace App\Api\V1\Controller;
use App\Api\V1\DTO\ApiResponse; use App\Api\V1\DTO\ApiResponse;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\{JsonResponse, Response}; use Symfony\Component\HttpFoundation\{JsonResponse, Response};
abstract class AbstractApiController extends Controller abstract class AbstractApiController extends AbstractController
{ {
protected const DEFAULT_SERIALIZER_GROUPS = ['api']; protected const DEFAULT_SERIALIZER_GROUPS = ['api'];

View file

@ -45,16 +45,19 @@ class SecurityController extends AbstractApiController
public function logout(TokenStorageInterface $tokenStorage, ApiTokenRepository $apiTokenRepo, EntityManagerInterface $em): JsonResponse public function logout(TokenStorageInterface $tokenStorage, ApiTokenRepository $apiTokenRepo, EntityManagerInterface $em): JsonResponse
{ {
$token = $tokenStorage->getToken(); if (null === $token = $tokenStorage->getToken()) {
return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Can\'t retrieve user token.');
}
if (!$token instanceof AuthenticatedApiToken) { if (!$token instanceof AuthenticatedApiToken) {
return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Invalid session token type retrieved.'); return $this->createJsonResponse(null, [], JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Invalid session token type retrieved.');
}
if (null === $apiTokenKey = $token->getTokenKey()) {
return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Can\'t retrieve token key from session.');
} }
if (null === $apiToken = $apiTokenRepo->findOneBy(['key' => $apiTokenKey])) { if (null === $tokenKey = $token->getTokenKey()) {
return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Can\'t retrieve token key from the session.');
}
if (null === $apiToken = $apiTokenRepo->findOneBy(['key' => $tokenKey])) {
return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'API token with such key not found in the database.'); return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'API token with such key not found in the database.');
} }

View file

@ -4,10 +4,10 @@ namespace App\Controller;
use App\Entity\User; use App\Entity\User;
use App\Repository\InviteRepository; use App\Repository\InviteRepository;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
class AccountController extends Controller class AccountController extends AbstractController
{ {
public function invites(InviteRepository $inviteRepo): Response public function invites(InviteRepository $inviteRepo): Response
{ {

View file

@ -3,12 +3,12 @@
namespace App\Controller; namespace App\Controller;
use App\Form\LoginType; use App\Form\LoginType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
class MainController extends Controller class MainController extends AbstractController
{ {
public function index(): Response public function index(): Response
{ {

View file

@ -3,13 +3,13 @@
namespace App\Controller; namespace App\Controller;
use App\Form\LoginType; use App\Form\LoginType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\{FormError, FormInterface}; use Symfony\Component\Form\{FormError, FormInterface};
use Symfony\Component\HttpFoundation\{Request, Response}; use Symfony\Component\HttpFoundation\{Request, Response};
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends Controller class SecurityController extends AbstractController
{ {
public function login(Request $request, AuthenticationUtils $authenticationUtils): Response public function login(Request $request, AuthenticationUtils $authenticationUtils): Response
{ {

View file

@ -6,10 +6,10 @@ use App\Magnetico\Entity\Torrent;
use App\Search\TorrentSearcher; use App\Search\TorrentSearcher;
use Pagerfanta\Adapter\DoctrineORMAdapter; use Pagerfanta\Adapter\DoctrineORMAdapter;
use Pagerfanta\Pagerfanta; use Pagerfanta\Pagerfanta;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\{Request, Response}; use Symfony\Component\HttpFoundation\{Request, Response};
class TorrentController extends Controller class TorrentController extends AbstractController
{ {
private const PER_PAGE = 20; private const PER_PAGE = 20;

View file

@ -7,12 +7,12 @@ use App\FormRequest\CreateUserRequest;
use App\Repository\InviteRepository; use App\Repository\InviteRepository;
use App\User\{InviteManager, UserManager}; use App\User\{InviteManager, UserManager};
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\{Request, Response}; use Symfony\Component\HttpFoundation\{Request, Response};
class UserController extends Controller class UserController extends AbstractController
{ {
public function register( public function register(
string $inviteCode, string $inviteCode,

View file

@ -4,8 +4,7 @@ namespace App\Search;
use App\Magnetico\Entity\Torrent; use App\Magnetico\Entity\Torrent;
use App\Magnetico\Repository\TorrentRepository; use App\Magnetico\Repository\TorrentRepository;
use Doctrine\Common\Persistence\Mapping\ClassMetadataFactory; use Doctrine\ORM\{EntityManagerInterface, QueryBuilder};
use Doctrine\ORM\QueryBuilder;
class TorrentSearcher class TorrentSearcher
{ {
@ -14,13 +13,13 @@ class TorrentSearcher
/** @var TorrentRepository */ /** @var TorrentRepository */
private $torrentRepo; private $torrentRepo;
/** @var ClassMetadataFactory */ /** @var EntityManagerInterface */
private $metadataFactory; private $em;
public function __construct(TorrentRepository $torrentRepo, ClassMetadataFactory $metadataFactory) public function __construct(TorrentRepository $torrentRepo, EntityManagerInterface $em)
{ {
$this->torrentRepo = $torrentRepo; $this->torrentRepo = $torrentRepo;
$this->metadataFactory = $metadataFactory; $this->em = $em;
} }
public function createSearchQueryBuilder(string $query, string $orderBy = null, string $order = 'asc'): QueryBuilder public function createSearchQueryBuilder(string $query, string $orderBy = null, string $order = 'asc'): QueryBuilder
@ -65,7 +64,7 @@ class TorrentSearcher
{ {
return ( return (
!\in_array($orderBy, self::ORDER_DISABLED_FIELDS, true) !\in_array($orderBy, self::ORDER_DISABLED_FIELDS, true)
&& $this->metadataFactory->getMetadataFor(Torrent::class)->hasField($orderBy) && $this->em->getClassMetadata(Torrent::class)->hasField($orderBy)
); );
} }

View file

@ -3,86 +3,111 @@
namespace App\Security; namespace App\Security;
use App\Api\V1\DTO\ApiResponse; use App\Api\V1\DTO\ApiResponse;
use App\Entity\User;
use App\Repository\ApiTokenRepository;
use App\Security\Token\AuthenticatedApiToken; use App\Security\Token\AuthenticatedApiToken;
use Symfony\Component\HttpFoundation\{JsonResponse, Request}; use Symfony\Component\HttpFoundation\{JsonResponse, Request, RequestStack, Response};
use Symfony\Component\Security\Core\Authentication\Token\{PreAuthenticatedToken, TokenInterface}; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\{AuthenticationException, BadCredentialsException, CustomUserMessageAuthenticationException}; use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\{UserInterface, UserProviderInterface};
use Symfony\Component\Security\Http\Authentication\{AuthenticationFailureHandlerInterface, SimplePreAuthenticatorInterface}; use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\SerializerInterface;
class ApiTokenAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{ {
public const TOKEN_HEADER = 'api-token'; public const TOKEN_HEADER = 'api-token';
/** @var ApiTokenRepository */
private $tokenRepo;
/** @var SerializerInterface */ /** @var SerializerInterface */
private $serializer; private $serializer;
public function __construct(SerializerInterface $serializer) /** @var RequestStack */
private $requestStack;
public function __construct(SerializerInterface $serializer, ApiTokenRepository $tokenRepo, RequestStack $requestStack)
{ {
$this->serializer = $serializer; $this->serializer = $serializer;
$this->tokenRepo = $tokenRepo;
// Crutch for Guard simplified auth to retrieve 'api-token' header in the createAuthenticatedToken()
$this->requestStack = $requestStack;
} }
/** Takes request data and creates token which will be ready to auth check */ public function supports(Request $request): bool
public function createToken(Request $request, $providerKey)
{ {
if (!($tokenKey = $request->headers->get(self::TOKEN_HEADER))) { return $request->headers->has(self::TOKEN_HEADER);
// Throwing exception here will break anonymous authentication for login method }
//throw new BadCredentialsException(sprintf('\'%s\' is invalid or not defined', self::TOKEN_HEADER));
public function getCredentials(Request $request)
{
return [
'token' => $request->headers->get(self::TOKEN_HEADER),
];
}
public function getUser($credentials, UserProviderInterface $userProvider): ?User
{
if (null === $token = $credentials['token']) {
return null; return null;
} }
return new PreAuthenticatedToken( return $this->tokenRepo->findUserByTokenKey($token);
'anon.',
$tokenKey,
$providerKey
);
} }
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) public function start(Request $request, AuthenticationException $authException = null)
{ {
if (!$userProvider instanceof ApiTokenUserProvider) { $message = sprintf('You need to use \'%s\' in your request: %s', self::TOKEN_HEADER, $authException ? $authException->getMessage() : '');
throw new \InvalidArgumentException(sprintf(
'The user provider for providerKey = \'%s\' must be an instance of %s, %s given.',
$providerKey,
ApiTokenUserProvider::class,
get_class($userProvider)
));
}
$apiTokenKey = $token->getCredentials(); $json = $this->serializer->serialize(
new ApiResponse(null, JsonResponse::HTTP_UNAUTHORIZED, $message),
$user = $userProvider->loadUserByUsername($apiTokenKey); 'json',
['groups' => ['api']]
if (!$user) {
throw new CustomUserMessageAuthenticationException(sprintf(
'API token \'%s\' does not exist.', $apiTokenKey
));
}
return new AuthenticatedApiToken(
$user,
$apiTokenKey,
$providerKey,
$user->getRoles()
); );
return new JsonResponse($json, Response::HTTP_UNAUTHORIZED,[], true);
} }
public function checkCredentials($credentials, UserInterface $user)
{
// No credentials check needed in case of token auth
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// No response object needed in token auth
return null;
}
public function supportsRememberMe()
{
// Remember me functionality don't needed in token auth
return false;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): JsonResponse public function onAuthenticationFailure(Request $request, AuthenticationException $exception): JsonResponse
{ {
// @todo Decouple with App\Api\V1\DTO // @todo Decouple with App\Api\V1\DTO
$json = $this->serializer->serialize( $json = $this->serializer->serialize(
new ApiResponse(null, JsonResponse::HTTP_UNAUTHORIZED, $exception->getMessage()), new ApiResponse(null, JsonResponse::HTTP_UNAUTHORIZED, $exception->getMessage()),
'json', 'json',
['groups' => ['api_v1']] ['groups' => ['api']]
); );
return new JsonResponse($json, JsonResponse::HTTP_UNAUTHORIZED,[], true); return new JsonResponse($json, JsonResponse::HTTP_UNAUTHORIZED,[], true);
} }
public function createAuthenticatedToken(UserInterface $user, $providerKey)
public function supportsToken(TokenInterface $token, $providerKey)
{ {
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey; $tokenKey = $this->requestStack->getCurrentRequest()->headers->get(self::TOKEN_HEADER);
return new AuthenticatedApiToken(
$user,
$tokenKey,
$providerKey,
$user->getRoles()
);
} }
} }

View file

@ -1,39 +0,0 @@
<?php
namespace App\Security;
use App\Entity\User;
use App\Repository\ApiTokenRepository;
use Symfony\Component\Security\Core\Exception\{UnsupportedUserException, UsernameNotFoundException};
use Symfony\Component\Security\Core\User\{UserInterface, UserProviderInterface};
class ApiTokenUserProvider implements UserProviderInterface
{
/** @var ApiTokenRepository */
private $userRepo;
public function __construct(ApiTokenRepository $userRepo)
{
$this->userRepo = $userRepo;
}
public function loadUserByUsername($username): User
{
if (null === $user = $this->userRepo->findUserByTokenKey($username)) {
throw new UsernameNotFoundException(sprintf('Token \'%s\' is not found.', $username));
}
return $user;
}
public function refreshUser(UserInterface $user)
{
throw new UnsupportedUserException();
}
public function supportsClass($class): bool
{
return User::class === $class;
}
}

View file

@ -68,6 +68,9 @@
"jdorn/sql-formatter": { "jdorn/sql-formatter": {
"version": "v1.2.17" "version": "v1.2.17"
}, },
"jean85/pretty-package-versions": {
"version": "1.2"
},
"monolog/monolog": { "monolog/monolog": {
"version": "1.23.0" "version": "1.23.0"
}, },
@ -110,6 +113,18 @@
"ref": "aaddfdf43cdecd4cf91f992052d76c2cadc04543" "ref": "aaddfdf43cdecd4cf91f992052d76c2cadc04543"
} }
}, },
"sentry/sentry": {
"version": "1.10.0"
},
"sentry/sentry-symfony": {
"version": "1.0",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "master",
"version": "1.0",
"ref": "fa1a2dfc020798cd7076b5419596e72dca07047a"
}
},
"symfony/cache": { "symfony/cache": {
"version": "v4.1.0" "version": "v4.1.0"
}, },
@ -125,6 +140,9 @@
"ref": "e3868d2f4a5104f19f844fe551099a00c6562527" "ref": "e3868d2f4a5104f19f844fe551099a00c6562527"
} }
}, },
"symfony/contracts": {
"version": "v1.0.2"
},
"symfony/debug": { "symfony/debug": {
"version": "v4.1.0" "version": "v4.1.0"
}, },
@ -179,9 +197,6 @@
"symfony/intl": { "symfony/intl": {
"version": "v4.1.0" "version": "v4.1.0"
}, },
"symfony/lts": {
"version": "4-dev"
},
"symfony/monolog-bridge": { "symfony/monolog-bridge": {
"version": "v4.1.0" "version": "v4.1.0"
}, },
@ -227,9 +242,6 @@
"ref": "cda8b550123383d25827705d05a42acf6819fe4e" "ref": "cda8b550123383d25827705d05a42acf6819fe4e"
} }
}, },
"symfony/security": {
"version": "v4.1.0"
},
"symfony/security-bundle": { "symfony/security-bundle": {
"version": "3.3", "version": "3.3",
"recipe": { "recipe": {
@ -239,12 +251,27 @@
"ref": "f8a63faa0d9521526499c0a8f403c9964ecb0527" "ref": "f8a63faa0d9521526499c0a8f403c9964ecb0527"
} }
}, },
"symfony/security-core": {
"version": "v4.2.2"
},
"symfony/security-csrf": {
"version": "v4.2.2"
},
"symfony/security-guard": {
"version": "v4.2.2"
},
"symfony/security-http": {
"version": "v4.2.2"
},
"symfony/serializer": { "symfony/serializer": {
"version": "v4.1.0" "version": "v4.1.0"
}, },
"symfony/serializer-pack": { "symfony/serializer-pack": {
"version": "v1.0.1" "version": "v1.0.1"
}, },
"symfony/stopwatch": {
"version": "v4.2.2"
},
"symfony/translation": { "symfony/translation": {
"version": "3.3", "version": "3.3",
"recipe": { "recipe": {
@ -272,6 +299,9 @@
"symfony/var-dumper": { "symfony/var-dumper": {
"version": "v4.1.0" "version": "v4.1.0"
}, },
"symfony/var-exporter": {
"version": "v4.2.2"
},
"symfony/web-profiler-bundle": { "symfony/web-profiler-bundle": {
"version": "3.3", "version": "3.3",
"recipe": { "recipe": {