diff --git a/.gitignore b/.gitignore index f0a8876..0032e3c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ grumphp.yml phpspec.yml /build/ .php_cs.cache -.envrc \ No newline at end of file diff --git a/LICENSE b/LICENSE index e8c58a0..498cf7a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2020-2022, European Union. +Copyright (c) 2019-2022, European Union. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/composer.json b/composer.json index cd83bb5..ea4c939 100644 --- a/composer.json +++ b/composer.json @@ -13,23 +13,23 @@ "sso" ], "require": { - "php": ">= 7.4", + "php": ">= 8.1", "ext-json": "*", "ext-simplexml": "*", - "ecphp/cas-bundle": "dev-master", - "ecphp/ecas": "^2", - "symfony/framework-bundle": "^5.4 || ^6" + "ecphp/cas-bundle": "dev-refactor/cas-lib-v2", + "ecphp/ecas": "^3", + "symfony/framework-bundle": "^6.1" }, "require-dev": { "ext-pcov": "*", "ecphp/php-conventions": "^1", "friends-of-phpspec/phpspec-code-coverage": "^6", - "infection/infection": "^0.24.0", - "infection/phpspec-adapter": "^0.1.2", + "infection/infection": "^0.26.0", + "infection/phpspec-adapter": "^0.2", "nyholm/psr7": "^1.5", "phpspec/phpspec": "^7", - "symfony/http-client": "^5.4 || ^6", - "symfony/security-core": "^5.4 || ^6" + "symfony/http-client": "^6.1", + "symfony/security-core": "^6.1" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/grumphp.yml.dist b/grumphp.yml.dist index 235ed71..bd5b31a 100644 --- a/grumphp.yml.dist +++ b/grumphp.yml.dist @@ -2,13 +2,15 @@ imports: - { resource: vendor/ecphp/php-conventions/config/php73/grumphp.yml } parameters: - tasks.license.date_from: 2020 extra_tasks: phpspec: verbose: true + clover_coverage: + clover_file: build/logs/clover.xml + level: 50 infection: threads: 10 test_framework: phpspec configuration: infection.json.dist - min_msi: 10 - min_covered_msi: 10 + min_msi: 50 + min_covered_msi: 90 diff --git a/infection.json.dist b/infection.json.dist index 3fe5fe5..78ed0b7 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -1,17 +1,19 @@ { - "timeout": 10, + "timeout": 30, "source": { "directories": [ "src" ] }, "logs": { + "github": true, "text": "build/infection.log", "summary": "build/summary.log", "debug": "build/debug.log", "perMutator": "build/per-mutator.md", - "badge": { - "branch": "master" + "html": "build/report.html", + "stryker": { + "report": "master" } }, "testFramework":"phpspec" diff --git a/phpspec.yml.dist b/phpspec.yml.dist index 9de3cd0..85449bb 100644 --- a/phpspec.yml.dist +++ b/phpspec.yml.dist @@ -5,7 +5,12 @@ extensions: - clover - php - text + - html output: html: build/coverage clover: build/logs/clover.xml php: build/coverage.php + whitelist: + - src + blacklist: + - src/Resources diff --git a/spec/EcPhp/EuLoginBundle/Security/Core/User/EuLoginUserProviderSpec.php b/spec/EcPhp/EuLoginBundle/Security/Core/User/EuLoginUserProviderSpec.php index 8ac2071..8af60e5 100644 --- a/spec/EcPhp/EuLoginBundle/Security/Core/User/EuLoginUserProviderSpec.php +++ b/spec/EcPhp/EuLoginBundle/Security/Core/User/EuLoginUserProviderSpec.php @@ -11,14 +11,22 @@ namespace spec\EcPhp\EuLoginBundle\Security\Core\User; +use EcPhp\CasBundle\Cas\SymfonyCasResponseBuilder; use EcPhp\CasBundle\Security\Core\User\CasUserProvider; -use EcPhp\CasLib\Introspection\Introspector; -use EcPhp\Ecas\Introspection\EcasIntrospector; +use EcPhp\CasLib\Response\CasResponseBuilder; +use EcPhp\CasLib\Response\Factory\AuthenticationFailureFactory; +use EcPhp\CasLib\Response\Factory\ProxyFactory; +use EcPhp\CasLib\Response\Factory\ProxyFailureFactory; +use EcPhp\CasLib\Response\Factory\ServiceValidateFactory as FactoryServiceValidateFactory; +use EcPhp\Ecas\Response\Factory\ServiceValidateFactory; use EcPhp\EuLoginBundle\Security\Core\User\EuLoginUser; use EcPhp\EuLoginBundle\Security\Core\User\EuLoginUserInterface; use EcPhp\EuLoginBundle\Security\Core\User\EuLoginUserProvider; -use Nyholm\Psr7\Response; +use loophp\psr17\Psr17; +use Nyholm\Psr7\Factory\Psr17Factory; use PhpSpec\ObjectBehavior; +use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\User\InMemoryUser; @@ -38,7 +46,7 @@ public function it_can_check_if_the_user_class_is_supported() public function it_can_load_a_user_from_a_response(): void { // TestBody1 - $response = new Response(200, ['content-type' => 'application/xml'], $this->getTestBody1()); + $response = new Response($this->getTestBody1(), 200, ['content-type' => 'application/xml']); $user = $this->loadUserByResponse($response); @@ -143,7 +151,7 @@ public function it_can_load_a_user_from_a_response(): void ]); // TestBody2 - $response = new Response(200, ['content-type' => 'application/xml'], $this->getTestBody2()); + $response = new Response($this->getTestBody2(), 200, ['content-type' => 'application/xml']); $user = $this->loadUserByResponse($response); @@ -242,11 +250,11 @@ public function it_can_refresh_a_user(EuLoginUserInterface $user) ->shouldReturn($user); } - public function it_cannot_load_a_user_by_username() + public function it_cannot_load_a_user_by_identifier() { $this ->shouldThrow(UnsupportedUserException::class) - ->during('loadUserByUsername', ['foo']); + ->during('loadUserByIdentifier', ['foo']); } public function it_is_initializable() @@ -256,8 +264,29 @@ public function it_is_initializable() public function let() { + $psr17Factory = new Psr17Factory(); + + $psr17 = new Psr17($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); + + $casResponseBuilder = new CasResponseBuilder( + new AuthenticationFailureFactory(), + new ProxyFactory(), + new ProxyFailureFactory(), + new ServiceValidateFactory(new FactoryServiceValidateFactory(), $psr17) + ); + + $psrHttpFactory = new PsrHttpFactory( + $psr17Factory, + $psr17Factory, + $psr17Factory, + $psr17Factory + ); + $this - ->beConstructedWith(new CasUserProvider(new EcasIntrospector(new Introspector()))); + ->beConstructedWith( + new CasUserProvider($casResponseBuilder, $psrHttpFactory), + new SymfonyCasResponseBuilder($casResponseBuilder, $psrHttpFactory) + ); } private function getTestBody1() diff --git a/spec/EcPhp/EuLoginBundle/Security/Core/User/EuLoginUserSpec.php b/spec/EcPhp/EuLoginBundle/Security/Core/User/EuLoginUserSpec.php index 78810dc..e23250d 100644 --- a/spec/EcPhp/EuLoginBundle/Security/Core/User/EuLoginUserSpec.php +++ b/spec/EcPhp/EuLoginBundle/Security/Core/User/EuLoginUserSpec.php @@ -11,39 +11,50 @@ namespace spec\EcPhp\EuLoginBundle\Security\Core\User; +use EcPhp\CasBundle\Cas\SymfonyCasResponseBuilder; use EcPhp\CasBundle\Security\Core\User\CasUser; -use EcPhp\CasLib\Introspection\Introspector; +use EcPhp\CasLib\Response\CasResponseBuilder; +use EcPhp\CasLib\Response\Factory\AuthenticationFailureFactory; +use EcPhp\CasLib\Response\Factory\ProxyFactory; +use EcPhp\CasLib\Response\Factory\ProxyFailureFactory; +use EcPhp\CasLib\Response\Factory\ServiceValidateFactory; use EcPhp\EuLoginBundle\Security\Core\User\EuLoginUser; -use Nyholm\Psr7\Response; +use Nyholm\Psr7\Factory\Psr17Factory; use PhpSpec\ObjectBehavior; +use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Exception\UnsupportedUserException; class EuLoginUserSpec extends ObjectBehavior { public function it_can_get_groups_when_no_groups_are_available() { - $body = <<<'EOF' - - - username - bar - - foo - - - - - - rex - snoopy - - - - - - EOF; - - $response = new Response(200, ['Content-Type' => 'application/xml'], $body); - $data = (new Introspector())->parse($response)['serviceResponse']['authenticationSuccess']; + $data = [ + 'user' => 'username', + 'foo' => 'bar', + 'proxies' => [ + 'proxy' => 'foo', + ], + 'attributes' => [ + 'groups' => [ + '@value' => '', + '@attributes' => [ + 'number' => '0', + ], + ], + 'extendedAttributes' => [ + 'extendedAttribute' => [ + 'attributeValue' => [ + 0 => 'rex', + 1 => 'snoopy', + ], + '@attributes' => [ + 'name' => 'http://stork.eu/motherInLawDogName', + ], + ], + ], + ], + ]; $casUser = new CasUser($data); @@ -53,6 +64,9 @@ public function it_can_get_groups_when_no_groups_are_available() $this ->getGroups() ->shouldReturn([]); + $this + ->getUserIdentifier() + ->shouldReturn('username'); } public function it_can_get_specific_attribute() @@ -175,18 +189,10 @@ public function it_is_initializable() { $this->shouldHaveType(EuLoginUser::class); - $this - ->getPassword() - ->shouldBeNull(); - $this ->getPgt() ->shouldReturn('proxyGrantingTicket'); - $this - ->getSalt() - ->shouldBeNull(); - $this ->getUserIdentifier() ->shouldReturn('username'); @@ -196,8 +202,8 @@ public function it_is_initializable() ->shouldReturn('bar'); $this - ->eraseCredentials() - ->shouldBeNull(); + ->shouldThrow(UnsupportedUserException::class) + ->during('eraseCredentials'); } public function let() @@ -295,10 +301,32 @@ public function let() EOF; - $response = new Response(200, ['Content-Type' => 'application/xml'], $body); - $data = (new Introspector())->parse($response)['serviceResponse']['authenticationSuccess']; + $response = new Response($body, 200, ['Content-Type' => 'application/xml']); + + $psr17Factory = new Psr17Factory(); + + $casResponseBuilder = new CasResponseBuilder( + new AuthenticationFailureFactory(), + new ProxyFactory(), + new ProxyFailureFactory(), + new ServiceValidateFactory() + ); + + $psrHttpFactory = new PsrHttpFactory( + $psr17Factory, + $psr17Factory, + $psr17Factory, + $psr17Factory + ); + + $symfonyCasResponseBuilder = new SymfonyCasResponseBuilder( + $casResponseBuilder, + $psrHttpFactory + ); + + $responseArray = $symfonyCasResponseBuilder->fromResponse($response)->toArray(); - $this->beConstructedWith(new CasUser($data)); + $this->beConstructedWith(new CasUser($responseArray['serviceResponse']['authenticationSuccess'])); } private function getAttributesData(): array diff --git a/src/Cas/SymfonyECas.php b/src/Cas/SymfonyECas.php new file mode 100644 index 0000000..37cfc1e --- /dev/null +++ b/src/Cas/SymfonyECas.php @@ -0,0 +1,151 @@ +cas = $cas; + $this->httpMessageFactory = $httpMessageFactory; + } + + public function authenticate( + ServerRequestInterface|Request $request, + array $parameters = [] + ): array { + return $this + ->cas + ->authenticate( + $this->updateRequest($request), + $parameters + ); + } + + public function handleProxyCallback( + ServerRequestInterface|Request $request, + array $parameters = [] + ): ResponseInterface { + return $this + ->cas + ->handleProxyCallback( + $this->updateRequest($request), + $parameters + ); + } + + public function login( + ServerRequestInterface|Request $request, + array $parameters = [] + ): ResponseInterface { + return $this + ->cas + ->login( + $this->updateRequest($request), + $parameters + ); + } + + public function logout( + ServerRequestInterface|Request $request, + array $parameters = [] + ): ResponseInterface { + return $this + ->cas + ->logout( + $this->updateRequest($request), + $parameters + ); + } + + public function process( + ServerRequestInterface|Request $request, + RequestHandlerInterface $handler + ): ResponseInterface { + return $this + ->cas + ->process( + $this->updateRequest($request), + $handler + ); + } + + public function requestProxyTicket( + ServerRequestInterface|Request $request, + array $parameters = [] + ): ResponseInterface { + return $this + ->cas + ->requestProxyTicket( + $this->updateRequest($request), + $parameters + ); + } + + public function requestServiceValidate( + ServerRequestInterface|Request $request, + array $parameters = [] + ): ResponseInterface { + return $this + ->cas + ->requestServiceValidate( + $this->updateRequest($request), + $parameters + ); + } + + public function requestTicketValidation( + ServerRequestInterface|Request $request, + array $parameters = [] + ): ResponseInterface { + return $this + ->cas + ->requestTicketValidation( + $this->updateRequest($request), + $parameters + ); + } + + public function supportAuthentication( + ServerRequestInterface|Request $request, + array $parameters = [] + ): bool { + return $this + ->cas + ->supportAuthentication( + $this->updateRequest($request), + $parameters + ); + } + + private function updateRequest( + ServerRequestInterface|Request $request + ): ServerRequestInterface { + return $request instanceof Request + ? $this->httpMessageFactory->createRequest($request) + : $request; + } +} diff --git a/src/DependencyInjection/EuLoginExtension.php b/src/DependencyInjection/EuLoginExtension.php index 11aa6e6..f564974 100644 --- a/src/DependencyInjection/EuLoginExtension.php +++ b/src/DependencyInjection/EuLoginExtension.php @@ -21,7 +21,10 @@ final class EuLoginExtension extends Extension public function load(array $configs, ContainerBuilder $container): void { // Load EU Login services. - $loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader = new PhpFileLoader( + $container, + new FileLocator(__DIR__ . '/../Resources/config') + ); $loader->load('services.php'); } } diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index d78d3fb..de08621 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -11,10 +11,15 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use EcPhp\Ecas\Ecas; +use EcPhp\CasBundle\Cas\SymfonyCasInterface; +use EcPhp\CasBundle\Security\Core\User\CasUserProviderInterface; +use EcPhp\CasLib\Contract\Configuration\PropertiesInterface; +use EcPhp\CasLib\Contract\Response\Factory\ServiceValidateFactory as FactoryServiceValidateFactory; use EcPhp\Ecas\EcasProperties; -use EcPhp\Ecas\Introspection\EcasIntrospector; +use EcPhp\Ecas\Response\Factory\ServiceValidateFactory; +use EcPhp\EuLoginBundle\Cas\SymfonyECas; use EcPhp\EuLoginBundle\Security\Core\User\EuLoginUserProvider; +use Symfony\Component\Security\Core\User\UserProviderInterface; return static function (ContainerConfigurator $container) { $services = $container->services(); @@ -25,21 +30,27 @@ ->autowire(true); $services - ->set('ecas.introspector', EcasIntrospector::class) - ->decorate('cas.introspector') - ->arg('$introspector', service('ecas.introspector.inner')); + ->set(EuLoginUserProvider::class) + ->decorate(CasUserProviderInterface::class) + ->arg('$casUserProvider', service('.inner')); $services - ->set('eulogin.userprovider', EuLoginUserProvider::class) - ->arg('$casUserProvider', service('cas.userprovider')); + ->set(EuLoginUserProvider::class) + ->decorate(UserProviderInterface::class) + ->arg('$casUserProvider', service('.inner')); $services - ->set('ecas.configuration', EcasProperties::class) - ->decorate('cas.configuration') - ->arg('$casProperties', service('ecas.configuration.inner')); + ->set(EcasProperties::class) + ->decorate(PropertiesInterface::class) + ->arg('$casProperties', service('.inner')); $services - ->set('ecas', Ecas::class) - ->decorate('cas') - ->arg('$cas', service('ecas.inner')); + ->set(SymfonyECas::class) + ->decorate(SymfonyCasInterface::class) + ->arg('$cas', service('.inner')); + + $services + ->set(ServiceValidateFactory::class) + ->decorate(FactoryServiceValidateFactory::class) + ->arg('$serviceValidateFactory', service('.inner')); }; diff --git a/src/Security/Core/User/EuLoginUser.php b/src/Security/Core/User/EuLoginUser.php index 66fcd41..7a4e672 100644 --- a/src/Security/Core/User/EuLoginUser.php +++ b/src/Security/Core/User/EuLoginUser.php @@ -27,10 +27,9 @@ public function __construct(CasUserInterface $user) public function eraseCredentials(): void { - // null } - public function get(string $key, $default = null) + public function get(string $key, mixed $default = null): mixed { return $this->user->get($key, $default); } @@ -40,7 +39,7 @@ public function getAssuranceLevel(): ?string return $this->user->getAttribute('assuranceLevel'); } - public function getAttribute(string $key, $default = null) + public function getAttribute(string $key, mixed $default = null): mixed { return $this->user->getAttribute($key, $default); } @@ -182,11 +181,6 @@ public function getOrgId(): ?string return $this->user->getAttribute('orgId'); } - public function getPassword() - { - return null; - } - public function getPgt(): ?string { return $this->user->getPgt(); @@ -204,11 +198,6 @@ public function getRoles(): array return array_merge($this->getGroups(), $default); } - public function getSalt() - { - return null; - } - public function getSso(): ?string { return $this->user->getAttribute('sso'); @@ -266,22 +255,6 @@ public function getUserManager(): ?string return $this->user->getAttribute('userManager'); } - /** - * @deprecated since Symfony 5.3, use getUserIdentifier() instead - */ - public function getUsername() - { - trigger_deprecation( - 'ecphp/eu-login-bundle', - '2.3.8', - 'The method "%s::getUsername()" is deprecated, use %s::getUserIdentifier() instead.', - EuLoginUser::class, - EuLoginUser::class - ); - - return $this->getUserIdentifier(); - } - public function isEqualTo(UserInterface $user): bool { return $this->user->isEqualTo($user); diff --git a/src/Security/Core/User/EuLoginUserProvider.php b/src/Security/Core/User/EuLoginUserProvider.php index b13bd91..a528bd7 100644 --- a/src/Security/Core/User/EuLoginUserProvider.php +++ b/src/Security/Core/User/EuLoginUserProvider.php @@ -21,10 +21,7 @@ final class EuLoginUserProvider implements CasUserProviderInterface { - /** - * @var CasUserProviderInterface - */ - private $casUserProvider; + private CasUserProviderInterface $casUserProvider; public function __construct(CasUserProviderInterface $casUserProvider) { @@ -33,7 +30,7 @@ public function __construct(CasUserProviderInterface $casUserProvider) public function loadUserByIdentifier(string $identifier): UserInterface { - throw new UnsupportedUserException('Unsupported operation.'); + return $this->casUserProvider->loadUserByIdentifier($identifier); } public function loadUserByResponse(ResponseInterface $response): CasUserInterface @@ -41,18 +38,13 @@ public function loadUserByResponse(ResponseInterface $response): CasUserInterfac return new EuLoginUser($this->casUserProvider->loadUserByResponse($response)); } - public function loadUserByUsername(string $username) - { - throw new UnsupportedUserException(sprintf('Username "%s" does not exist.', $username)); - } - public function refreshUser(UserInterface $user) { if (!$user instanceof EuLoginUserInterface) { throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); } - return $user; + return $this->casUserProvider->refreshUser($user); } public function supportsClass(string $class)