diff --git a/config/routes/sulu_admin.yaml b/config/routes/sulu_admin.yaml index d0ccf7e9b8d..0b19447d89f 100644 --- a/config/routes/sulu_admin.yaml +++ b/config/routes/sulu_admin.yaml @@ -113,9 +113,3 @@ sulu_audience_targeting_api: type: rest resource: "@SuluAudienceTargetingBundle/Resources/config/routing_api.yml" prefix: /admin/api - -sulu_admin_single_sign_on: - path: /openid - controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction - defaults: - route: sulu_admin diff --git a/src/Sulu/Bundle/AdminBundle/Controller/AdminController.php b/src/Sulu/Bundle/AdminBundle/Controller/AdminController.php index 44981eff2e0..987ab30e481 100644 --- a/src/Sulu/Bundle/AdminBundle/Controller/AdminController.php +++ b/src/Sulu/Bundle/AdminBundle/Controller/AdminController.php @@ -1,7 +1,5 @@ $this->urlGenerator->generate('sulu_admin.translation'), 'generateUrl' => $this->urlGenerator->generate('sulu_page.post_resourcelocator', ['action' => 'generate']), 'routing' => $this->urlGenerator->generate('fos_js_routing_js'), - 'single_sign_on' => $this->hasSingleSignOnProvider, + 'has_single_sign_on' => $this->hasSingleSignOnProvider, ]; try { @@ -271,7 +269,7 @@ public function indexAction() 'password_info_translation_key' => $this->passwordInfoTranslationKey, 'sulu_version' => $this->suluVersion, 'app_version' => $this->appVersion, - 'single_sign_on' => $this->hasSingleSignOnProvider, + 'has_single_sign_on' => $this->hasSingleSignOnProvider, ] )); } diff --git a/src/Sulu/Bundle/AdminBundle/Resources/js/components/Input/Input.js b/src/Sulu/Bundle/AdminBundle/Resources/js/components/Input/Input.js index c551189d02b..d0cb09a08d0 100644 --- a/src/Sulu/Bundle/AdminBundle/Resources/js/components/Input/Input.js +++ b/src/Sulu/Bundle/AdminBundle/Resources/js/components/Input/Input.js @@ -14,12 +14,12 @@ const LOADER_SIZE = 20; export default class Input extends React.PureComponent> { static defaultProps = { alignment: 'left', + autoFocus: false, collapsed: false, disabled: false, skin: 'default', type: 'text', valid: true, - autoFocus: false, }; setInputRef = (ref: ?ElementRef<'input'>) => { @@ -58,6 +58,7 @@ export default class Input extends React.PureComponent extends React.PureComponent extends React.PureComponent extends React.PureComponent diff --git a/src/Sulu/Bundle/AdminBundle/Resources/js/components/Input/tests/Input.test.js b/src/Sulu/Bundle/AdminBundle/Resources/js/components/Input/tests/Input.test.js index 94bf0cd3b30..a002fa18770 100644 --- a/src/Sulu/Bundle/AdminBundle/Resources/js/components/Input/tests/Input.test.js +++ b/src/Sulu/Bundle/AdminBundle/Resources/js/components/Input/tests/Input.test.js @@ -15,6 +15,12 @@ test('Input should render', () => { expect(container).toMatchSnapshot(); }); +test('Input should render with autoFocus', () => { + const onChange = jest.fn(); + const {container} = render(); + expect(container).toMatchSnapshot(); +}); + test('Input should render with autocomplete off', () => { const onChange = jest.fn(); const {container} = render(); diff --git a/src/Sulu/Bundle/AdminBundle/Resources/js/components/Input/tests/__snapshots__/Input.test.js.snap b/src/Sulu/Bundle/AdminBundle/Resources/js/components/Input/tests/__snapshots__/Input.test.js.snap index e2a6b25962c..8373601ee53 100644 --- a/src/Sulu/Bundle/AdminBundle/Resources/js/components/Input/tests/__snapshots__/Input.test.js.snap +++ b/src/Sulu/Bundle/AdminBundle/Resources/js/components/Input/tests/__snapshots__/Input.test.js.snap @@ -168,6 +168,19 @@ exports[`Input should render with a segment counter 1`] = ` `; +exports[`Input should render with autoFocus 1`] = ` +
+
+ +
+
+`; + exports[`Input should render with autocomplete off 1`] = `
= {| alignment?: 'left' | 'center' | 'right', autocomplete?: string, + autoFocus?: boolean, collapsed?: boolean, disabled: boolean, headline?: boolean, diff --git a/src/Sulu/Bundle/AdminBundle/Resources/js/containers/Login/Login.js b/src/Sulu/Bundle/AdminBundle/Resources/js/containers/Login/Login.js index 157987fffbf..cf980fdee18 100644 --- a/src/Sulu/Bundle/AdminBundle/Resources/js/containers/Login/Login.js +++ b/src/Sulu/Bundle/AdminBundle/Resources/js/containers/Login/Login.js @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import {action, computed, observable} from 'mobx'; +import {action, autorun, computed, observable} from 'mobx'; import {observer} from 'mobx-react'; import Icon from '../../components/Icon/index'; import {translate} from '../../utils/index'; @@ -25,11 +25,27 @@ type Props = {| @observer class Login extends React.Component { + redirectDisposer: () => void; + static defaultProps = { backLink: '/', initialized: false, }; + constructor(props: Props) { + super(props); + + this.redirectDisposer = autorun(() => { + if (userStore.redirectUrl !== '') { + window.location.href = userStore.redirectUrl; + } + }); + } + + componentWillUnmount() { + this.redirectDisposer(); + } + @observable visibleForm: FormTypes = this.props.router.attributes.forgotPasswordToken ? 'reset-password' : 'login'; @computed get loginFormVisible(): boolean { @@ -70,7 +86,7 @@ class Login extends React.Component { handleLoginFormSubmit = (data: LoginFormData) => { userStore.login(data).then(() => { - if (userStore.hasJsonLogin) { + if (userStore.loginMethod === 'json_login') { return; } @@ -139,8 +155,9 @@ class Login extends React.Component { diff --git a/src/Sulu/Bundle/AdminBundle/Resources/js/containers/Login/LoginForm.js b/src/Sulu/Bundle/AdminBundle/Resources/js/containers/Login/LoginForm.js index e2c076ffc63..d7a01df3981 100644 --- a/src/Sulu/Bundle/AdminBundle/Resources/js/containers/Login/LoginForm.js +++ b/src/Sulu/Bundle/AdminBundle/Resources/js/containers/Login/LoginForm.js @@ -14,8 +14,7 @@ import type {LoginFormData} from './types'; type Props = {| error: boolean, loading: boolean, - hasSingleSignOn: boolean, - hasOnlyPassword: boolean, + mode: string, onChangeForm: () => void, onSubmit: (data: LoginFormData) => void, |}; @@ -25,8 +24,7 @@ class LoginForm extends React.Component { static defaultProps = { error: false, loading: false, - hasSingleSignOn: false, - hasOnlyPassword: false, + mode: 'username_password', }; @observable inputRef: ?ElementRef<*>; @@ -35,7 +33,9 @@ class LoginForm extends React.Component { @observable password: ?string; @computed get submitButtonDisabled(): boolean { - return !(this.user && this.password) && !((this.user || this.password) && this.props.hasSingleSignOn); + return !(this.user && this.password) + && !((this.user || this.password) + && this.props.mode !== 'username_password'); } @action setInputRef = (ref: ?ElementRef<*>) => { @@ -59,12 +59,12 @@ class LoginForm extends React.Component { @action handleSubmit = (event: SyntheticEvent) => { event.preventDefault(); - if (this.user && this.props.hasSingleSignOn) { + if (this.user && this.props.mode !== 'username_password') { const {onSubmit} = this.props; onSubmit({ username: this.user, - password: this.password, + password: this.password ?? '', }); if (this.user && this.password) { @@ -104,13 +104,14 @@ class LoginForm extends React.Component {
- {(!this.props.hasOnlyPassword) && ( + {(this.props.mode !== 'password_only') && ( )} - {(!this.props.hasSingleSignOn || (this.props.hasSingleSignOn && this.props.hasOnlyPassword)) && ( + {(this.props.mode !== 'username_only') && ( )} -
- - -
+
+ + +
diff --git a/src/Sulu/Bundle/AdminBundle/Resources/js/containers/Login/tests/Login.test.js b/src/Sulu/Bundle/AdminBundle/Resources/js/containers/Login/tests/Login.test.js index fcc0e7ec7a3..7cc0c6731b0 100644 --- a/src/Sulu/Bundle/AdminBundle/Resources/js/containers/Login/tests/Login.test.js +++ b/src/Sulu/Bundle/AdminBundle/Resources/js/containers/Login/tests/Login.test.js @@ -27,8 +27,9 @@ const mockUserStoreTwoFactorError = jest.fn(); const mockUserStoreSetResetSuccess = jest.fn(); const mockUserStoreLoading = jest.fn().mockReturnValue(false); const mockUserStoreForgotPasswordSuccess = jest.fn().mockReturnValue(false); -const mockUserStoreHasJsonLogin = jest.fn().mockReturnValue(false); -const mockUserStoreHasSingleSignOn = jest.fn().mockReturnValue(false); +const mockUserStoreLoginMethod = jest.fn().mockReturnValue(false); +const mockUserStoreHasSingleSignOn = jest.fn(); +const mockUserStoreRedirectUrl = jest.fn().mockReturnValue(''); jest.mock('../../../stores/userStore', () => { return new class { @@ -68,12 +69,16 @@ jest.mock('../../../stores/userStore', () => { return mockUserStoreSetResetSuccess(value); } + get loginMethod() { + return mockUserStoreLoginMethod(); + } + hasSingleSignOn() { return mockUserStoreHasSingleSignOn(); } - get hasJsonLogin() { - return mockUserStoreHasJsonLogin(); + redirectUrl() { + return mockUserStoreRedirectUrl(); } get loading() { @@ -260,22 +265,24 @@ test('Should not call the submit handler of the reset password view with not mat test('Should render the Login with only username/email', () => { const router = new Router(); - mockUserStoreHasSingleSignOn.mockReturnValueOnce(true); + mockUserStoreHasSingleSignOn.mockReturnValue(true); + mockUserStoreLoginMethod.mockReturnValueOnce(''); const loginForm = mount( ); - expect(loginForm.render()).toMatchSnapshot() + expect(loginForm.render()).toMatchSnapshot(); }); test('Should render the Login with only password', () => { const router = new Router(); - mockUserStoreHasJsonLogin.mockReturnValue(true); + mockUserStoreHasSingleSignOn.mockReturnValue(true); + mockUserStoreLoginMethod.mockReturnValue('json_login'); const loginForm = mount( ); - expect(loginForm.render()).toMatchSnapshot() + expect(loginForm.render()).toMatchSnapshot(); }); diff --git a/src/Sulu/Bundle/AdminBundle/Resources/js/stores/userStore/userStore.js b/src/Sulu/Bundle/AdminBundle/Resources/js/stores/userStore/userStore.js index 4cb56407d26..44b1553d50d 100644 --- a/src/Sulu/Bundle/AdminBundle/Resources/js/stores/userStore/userStore.js +++ b/src/Sulu/Bundle/AdminBundle/Resources/js/stores/userStore/userStore.js @@ -19,10 +19,11 @@ class UserStore { @observable loggedIn: boolean = false; @observable loading: boolean = false; @observable loginError: boolean = false; - @observable hasJsonLogin: boolean = false; + @observable loginMethod: string = ''; @observable forgotPasswordSuccess: boolean = false; @observable twoFactorMethods: Array = []; @observable twoFactorError: boolean = false; + @observable redirectUrl: string = ''; @action clear() { this.persistentSettings = new Map(); @@ -31,10 +32,11 @@ class UserStore { this.user = undefined; this.contact = undefined; this.loginError = false; - this.hasJsonLogin = false; + this.loginMethod = ''; this.forgotPasswordSuccess = false; this.twoFactorMethods = []; this.twoFactorError = false; + this.redirectUrl = ''; } @computed get systemLocale() { @@ -53,8 +55,8 @@ class UserStore { this.loginError = loginError; } - @action setHasJsonLogin(hasJsonLogin: boolean) { - this.hasJsonLogin = hasJsonLogin; + @action setLoginMethod(loginMethod: string) { + this.loginMethod = loginMethod; } @action setForgotPasswordSuccess(forgotPasswordSuccess: boolean) { @@ -69,6 +71,10 @@ class UserStore { this.twoFactorError = twoFactorError; } + @action setRedirectUrl(redirectUrl: string) { + this.redirectUrl = redirectUrl; + } + @computed get contentLocale(): string { const contentLocale = this.persistentSettings.get(CONTENT_LOCALE_SETTING_KEY); @@ -113,13 +119,13 @@ class UserStore { this.setTwoFactorMethods([]); if (data.method === 'redirect' && data.url) { - window.location.href = data.url; + this.setRedirectUrl(data.url); - return + return; } if (data.method === 'json_login') { - this.setHasJsonLogin(true); + this.setLoginMethod(data.method); this.setLoading(false); return; @@ -149,7 +155,7 @@ class UserStore { this.clear(); } - if (this.hasJsonLogin) { + if (this.loginMethod === 'json_login') { this.clear(); } @@ -170,7 +176,7 @@ class UserStore { return Promise.reject(error); } - if (this.hasJsonLogin) { + if (this.loginMethod === 'json_login') { this.clear(); } @@ -199,9 +205,9 @@ class UserStore { return Requester.post(Config.endpoints.forgotPasswordReset, data) .then((data) => { if (data.method === 'redirect' && data.url) { - window.location.href = data.url; + this.setRedirectUrl(data.url); - return + return; } this.setLoading(false); @@ -269,7 +275,7 @@ class UserStore { } hasSingleSignOn() { - return Config.endpoints.single_sign_on; + return Config.endpoints.has_single_sign_on; } } diff --git a/src/Sulu/Bundle/AdminBundle/Resources/views/Admin/main.html.twig b/src/Sulu/Bundle/AdminBundle/Resources/views/Admin/main.html.twig index a3dee096fe7..dae76f00500 100644 --- a/src/Sulu/Bundle/AdminBundle/Resources/views/Admin/main.html.twig +++ b/src/Sulu/Bundle/AdminBundle/Resources/views/Admin/main.html.twig @@ -21,7 +21,7 @@ appVersion: app_version, passwordPattern: password_pattern, passwordInfoTranslationKey: password_info_translation_key, - SingleSignOn: single_sign_on + hasSingleSignOn: has_single_sign_on, } -%} {% block application -%} diff --git a/src/Sulu/Bundle/AdminBundle/Tests/Functional/Controller/AdminControllerTest.php b/src/Sulu/Bundle/AdminBundle/Tests/Functional/Controller/AdminControllerTest.php index 2fcfaca7917..44b8904308b 100644 --- a/src/Sulu/Bundle/AdminBundle/Tests/Functional/Controller/AdminControllerTest.php +++ b/src/Sulu/Bundle/AdminBundle/Tests/Functional/Controller/AdminControllerTest.php @@ -127,11 +127,13 @@ public function testTemplateConfig(): void 'translations' => '/admin/translations', 'generateUrl' => '/admin/api/resourcelocators?action=generate', 'routing' => '/admin/js/routing', + 'has_single_sign_on' => false, ], 'suluVersion' => '_._._', 'appVersion' => null, 'passwordPattern' => null, 'passwordInfoTranslationKey' => null, + 'hasSingleSignOn' => false, ], $config); } diff --git a/src/Sulu/Bundle/SecurityBundle/DependencyInjection/SuluSecurityExtension.php b/src/Sulu/Bundle/SecurityBundle/DependencyInjection/SuluSecurityExtension.php index 862c7363037..7ca2771459a 100644 --- a/src/Sulu/Bundle/SecurityBundle/DependencyInjection/SuluSecurityExtension.php +++ b/src/Sulu/Bundle/SecurityBundle/DependencyInjection/SuluSecurityExtension.php @@ -1,7 +1,5 @@ + %sulu_core.translations% diff --git a/src/Sulu/Bundle/SecurityBundle/SingleSignOn/Adapter/OpenId/OpenIdSingleSignOnAdapter.php b/src/Sulu/Bundle/SecurityBundle/SingleSignOn/Adapter/OpenId/OpenIdSingleSignOnAdapter.php index b033555fa1c..01e9c0c2430 100644 --- a/src/Sulu/Bundle/SecurityBundle/SingleSignOn/Adapter/OpenId/OpenIdSingleSignOnAdapter.php +++ b/src/Sulu/Bundle/SecurityBundle/SingleSignOn/Adapter/OpenId/OpenIdSingleSignOnAdapter.php @@ -1,7 +1,5 @@ $translations + */ public function __construct( private HttpClientInterface $httpClient, - private readonly UserRepositoryInterface $userRepository, - private readonly EntityManagerInterface $entityManager, - private readonly ContactRepositoryInterface $contactRepository, - private readonly RoleRepositoryInterface $roleRepository, - private readonly UrlGeneratorInterface $urlGenerator, + private UserRepositoryInterface $userRepository, + private EntityManagerInterface $entityManager, + private ContactRepositoryInterface $contactRepository, + private RoleRepositoryInterface $roleRepository, + private UrlGeneratorInterface $urlGenerator, private string $endpoint, private string $clientId, #[\SensitiveParameter] private string $clientSecret, private string $userRole, + private array $translations, ) { } @@ -167,15 +169,15 @@ public function createOrUpdateUser(string $token): UserBadge $accessToken = $data['access_token']; /** @var array{ - * sub?: string, - * name?: string, - * given_name?: string, - * family_name?: string, - * picture?: string, - * email?: string, - * email_verified?: bool, - * locale?: string, - * hd?: string, + * sub?: string, + * name?: string, + * given_name?: string, + * family_name?: string, + * picture?: string, + * email?: string, + * email_verified?: bool, + * locale?: string, + * hd?: string, * } $attributes */ $attributes = $this->httpClient->request('GET', $userinfoEndpoint, [ @@ -270,7 +272,8 @@ private function createOrUpdateAdminUser(string $email, array $attributes): void $contact = $user->getContact(); $contact->setFirstName($attributes['given_name'] ?? ''); $contact->setLastName($attributes['family_name'] ?? ''); - $user->setLocale((isset($attributes['locale']) && 'de' === $attributes['locale']) ? 'de' : 'en'); + $locale = (isset($attributes['locale']) && \in_array($attributes['locale'], $this->translations, true)) ? $attributes['locale'] : 'en'; + $user->setLocale($locale); $this->entityManager->flush(); } diff --git a/src/Sulu/Bundle/SecurityBundle/SingleSignOn/Adapter/OpenId/OpenIdSingleSignOnAdapterFactory.php b/src/Sulu/Bundle/SecurityBundle/SingleSignOn/Adapter/OpenId/OpenIdSingleSignOnAdapterFactory.php index 6352d3b3276..c5b83e15f1e 100644 --- a/src/Sulu/Bundle/SecurityBundle/SingleSignOn/Adapter/OpenId/OpenIdSingleSignOnAdapterFactory.php +++ b/src/Sulu/Bundle/SecurityBundle/SingleSignOn/Adapter/OpenId/OpenIdSingleSignOnAdapterFactory.php @@ -27,13 +27,17 @@ */ class OpenIdSingleSignOnAdapterFactory implements SingleSignOnAdapterFactoryInterface { + /** + * @param array $translations + */ public function __construct( private HttpClientInterface $httpClient, - private readonly UserRepositoryInterface $userRepository, - private readonly EntityManagerInterface $entityManager, - private readonly ContactRepositoryInterface $contactRepository, - private readonly RoleRepositoryInterface $roleRepository, - private readonly UrlGeneratorInterface $urlGenerator, + private UserRepositoryInterface $userRepository, + private EntityManagerInterface $entityManager, + private ContactRepositoryInterface $contactRepository, + private RoleRepositoryInterface $roleRepository, + private UrlGeneratorInterface $urlGenerator, + private array $translations ) { } @@ -58,6 +62,7 @@ public function createAdapter(#[\SensitiveParameter] array $dsn, string $userRol $clientId, $clientSecret, $userRole, + $this->translations, ); } diff --git a/src/Sulu/Bundle/SecurityBundle/SingleSignOn/SingleSignOnAdapterProvider.php b/src/Sulu/Bundle/SecurityBundle/SingleSignOn/SingleSignOnAdapterProvider.php index 3ec374744e0..ea92eed6e37 100644 --- a/src/Sulu/Bundle/SecurityBundle/SingleSignOn/SingleSignOnAdapterProvider.php +++ b/src/Sulu/Bundle/SecurityBundle/SingleSignOn/SingleSignOnAdapterProvider.php @@ -23,7 +23,7 @@ class SingleSignOnAdapterProvider { public function __construct( - private readonly ContainerInterface $adapters, + private ContainerInterface $adapters, ) { } diff --git a/src/Sulu/Bundle/SecurityBundle/SingleSignOn/SingleSignOnLoginRequestSubscriber.php b/src/Sulu/Bundle/SecurityBundle/SingleSignOn/SingleSignOnLoginRequestSubscriber.php index c73a26d60d7..5f956584d98 100644 --- a/src/Sulu/Bundle/SecurityBundle/SingleSignOn/SingleSignOnLoginRequestSubscriber.php +++ b/src/Sulu/Bundle/SecurityBundle/SingleSignOn/SingleSignOnLoginRequestSubscriber.php @@ -31,9 +31,9 @@ class SingleSignOnLoginRequestSubscriber implements EventSubscriberInterface { public function __construct( - private readonly SingleSignOnAdapterProvider $singleSignOnAdapterProvider, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly UserRepository $userRepository, + private SingleSignOnAdapterProvider $singleSignOnAdapterProvider, + private UrlGeneratorInterface $urlGenerator, + private UserRepository $userRepository, ) { } @@ -73,9 +73,9 @@ public function onKernelRequest(RequestEvent $event): void // Todo: Change this, userRepository should not return an error if there was no user found. try { - /** @var User $user */ + /** @var ?User $user */ $user = $this->userRepository->findUserByIdentifier($identifier); - $email = $user->getEmail() ?? $user->getUsername(); + $email = $user ? ($user->getEmail() ?? $user->getUsername()) : $identifier; } catch (NoResultException $e) { $email = $identifier; } diff --git a/src/Sulu/Bundle/SecurityBundle/SingleSignOn/SingleSignOnTokenExtractor.php b/src/Sulu/Bundle/SecurityBundle/SingleSignOn/SingleSignOnTokenExtractor.php index bc658ae6645..97f72965623 100644 --- a/src/Sulu/Bundle/SecurityBundle/SingleSignOn/SingleSignOnTokenExtractor.php +++ b/src/Sulu/Bundle/SecurityBundle/SingleSignOn/SingleSignOnTokenExtractor.php @@ -1,7 +1,5 @@ + */ + private $userRepository; + + /** + * @var ObjectProphecy + */ + private $entityManager; + + /** + * @var ObjectProphecy + */ + private $contactRepository; + + /** + * @var ObjectProphecy + */ + private $roleRepository; + + /** + * @var ObjectProphecy + */ + private $urlGenerator; + + private OpenIdSingleSignOnAdapter $adapter; + + protected function setUp(): void + { + $this->httpClient = new MockHttpClient(); + $this->userRepository = $this->prophesize(UserRepositoryInterface::class); + $this->entityManager = $this->prophesize(EntityManagerInterface::class); + $this->contactRepository = $this->prophesize(ContactRepositoryInterface::class); + $this->roleRepository = $this->prophesize(RoleRepositoryInterface::class); + $this->urlGenerator = $this->prophesize(UrlGeneratorInterface::class); + + $this->adapter = new OpenIdSingleSignOnAdapter( + $this->httpClient, + $this->userRepository->reveal(), + $this->entityManager->reveal(), + $this->contactRepository->reveal(), + $this->roleRepository->reveal(), + $this->urlGenerator->reveal(), + 'https://example.com/endpoint', + 'clientId', + 'clientSecret', + 'userRole', + ['de', 'en'], + ); + } + + public function testGenerateLoginUrl(): void + { + $session = new Session(new MockArraySessionStorage()); + /** @var string $responseContent */ + $responseContent = \json_encode([ + 'authorization_endpoint' => 'https://example.com/authorize', + ]); + + $response = new MockResponse($responseContent, [ + 'http_code' => 200, + 'response_headers' => ['Content-Type' => 'application/json'], + ]); + + $this->httpClient->setResponseFactory([$response]); + $request = new Request(); + $request->setSession($session); + $redirectUrl = 'https://example.com/redirect'; + $domain = 'example.com'; + + $loginUrl = $this->adapter->generateLoginUrl($request, $redirectUrl, $domain); + + $this->assertStringStartsWith('https://example.com/authorize', $loginUrl); + + /** @var array{ + * domain: string, + * state: string, + * } $openIdAttributes + */ + $openIdAttributes = $session->get(OpenIdSingleSignOnAdapter::OPEN_ID_ATTRIBUTES); + $this->assertSame($domain, $openIdAttributes['domain']); + $this->assertIsString($openIdAttributes['state']); + } + + public function testIsAuthorizationValid(): void + { + $isValid = $this->adapter->isAuthorizationValid(['state' => 'f20f9604-7577-4ac8-8890-9a6fbf359259'], ['state' => 'f20f9604-7577-4ac8-8890-9a6fbf359259']); + $isNotValid = $this->adapter->isAuthorizationValid(['state' => 'f20f9604-7577-4ac8-8890-9a6fbf359259'], ['state' => '123-7577-4ac8-8890-9a6fbf359259']); + $isNotSet = $this->adapter->isAuthorizationValid([], ['state' => '123-7577-4ac8-8890-9a6fbf359259']); + + $this->assertTrue($isValid); + $this->assertFalse($isNotValid); + $this->assertFalse($isNotSet); + } + + public function testCreateOrUpdateUser(): void + { + $token = 'mock_token'; + $expectedRequests = [ + function($method, $url, $options): MockResponse { + $this->assertSame('GET', $method); + $this->assertSame('https://example.com/endpoint', $url); + + /** @var string $response */ + $response = \json_encode([ + 'token_endpoint' => 'https://token_endpoint.com', + 'userinfo_endpoint' => 'https://userinfo_endpoint.com', + ]); + + return new MockResponse($response); + }, + + function($method, $url, $options): MockResponse { + $this->assertSame('POST', $method); + $this->assertSame('https://token_endpoint.com/', $url); + + /** @var string $response */ + $response = \json_encode([ + 'access_token' => 'ya29.a0Ad52N38l9acjoNc975Apn4W4H8DK_TtX_S', + ]); + + return new MockResponse($response); + }, + + function($method, $url, $options): MockResponse { + $this->assertSame('GET', $method); + $this->assertSame('https://userinfo_endpoint.com/', $url); + + /** @var string $response */ + $response = \json_encode([ + 'email' => 'hello@sulu.io', + ]); + + return new MockResponse($response); + }, + ]; + + $this->httpClient->setResponseFactory($expectedRequests); + + $this->urlGenerator->generate('sulu_admin', [], UrlGeneratorInterface::ABSOLUTE_URL) + ->shouldBeCalled() + ->willReturn('https://sulu.io/admin'); + + $this->userRepository->findOneBy(Argument::any())->willReturn(null); + $this->contactRepository->createNew()->willReturn($this->prophesize(Contact::class)->reveal()); + $this->entityManager->persist(Argument::any())->shouldBeCalled(); + $role = $this->prophesize(Role::class); + $role->getAnonymous()->shouldBeCalled()->willReturn(false); + $role->getIdentifier()->willReturn('hello@sulu.io'); + $this->roleRepository->findOneBy(Argument::any()) + ->shouldBeCalled() + ->willReturn($role->reveal()); + $this->entityManager->flush()->shouldBeCalled(); + + $expectedUserBadge = new UserBadge('hello@sulu.io', null, ['email' => 'hello@sulu.io']); + + $result = $this->adapter->createOrUpdateUser($token); + + $this->assertEquals($expectedUserBadge, $result); + } +} diff --git a/src/Sulu/Bundle/SecurityBundle/Tests/Unit/SingleSignOn/SingleSignOnLoginRequestSubscriberTest.php b/src/Sulu/Bundle/SecurityBundle/Tests/Unit/SingleSignOn/SingleSignOnLoginRequestSubscriberTest.php new file mode 100644 index 00000000000..52c200d9be4 --- /dev/null +++ b/src/Sulu/Bundle/SecurityBundle/Tests/Unit/SingleSignOn/SingleSignOnLoginRequestSubscriberTest.php @@ -0,0 +1,300 @@ + + */ + private $singleSignOnAdapterProvider; + + /** + * @var ObjectProphecy + */ + private $urlGenerator; + + /** + * @var ObjectProphecy + */ + private $userRepository; + + private SingleSignOnLoginRequestSubscriber $subscriber; + + protected function setUp(): void + { + $this->singleSignOnAdapterProvider = $this->prophesize(SingleSignOnAdapterProvider::class); + $this->urlGenerator = $this->prophesize(UrlGeneratorInterface::class); + $this->userRepository = $this->prophesize(UserRepository::class); + + $this->subscriber = new SingleSignOnLoginRequestSubscriber( + $this->singleSignOnAdapterProvider->reveal(), + $this->urlGenerator->reveal(), + $this->userRepository->reveal(), + ); + } + + public function testGetSubscribedEvents(): void + { + $this->assertSame([ + RequestEvent::class => [ + ['onKernelRequest', 9], + ], + ], SingleSignOnLoginRequestSubscriber::getSubscribedEvents()); + } + + public function testOnKernelRequestNotMainRequest(): void + { + $request = Request::create('/admin/login', Request::METHOD_POST); + $request->attributes->set('_route', 'sulu_admin.login_check'); + $event = $this->createRequestEvent($request, HttpKernelInterface::SUB_REQUEST); + + $this->subscriber->onKernelRequest($event); + + $this->assertNull($event->getResponse()); + } + + public function testOnKernelRequestNotPost(): void + { + $request = Request::create('/admin/login'); + $event = $this->createRequestEvent($request); + $request->attributes->set('_route', 'sulu_admin.login_check'); + + $this->subscriber->onKernelRequest($event); + + $this->assertNull($event->getResponse()); + } + + public function testOnKernelRequestNotLoginRoute(): void + { + $request = Request::create('/admin/login'); + $event = $this->createRequestEvent($request); + $request->attributes->set('_route', 'sulu_admin.WRONG_NAME'); + + $this->subscriber->onKernelRequest($event); + + $this->assertNull($event->getResponse()); + } + + public function testOnKernelRequestNoIdentifier(): void + { + $request = Request::create('/admin/login', Request::METHOD_POST); + $event = $this->createRequestEvent($request); + $request->attributes->set('_route', 'sulu_admin.login_check'); + + $this->subscriber->onKernelRequest($event); + + $this->assertNull($event->getResponse()); + } + + public function testOnKernelRequestWrongIdentifierAndNoUser(): void + { + $request = Request::create('/admin/login', Request::METHOD_POST); + $request->attributes->set('_route', 'sulu_admin.login_check'); + $request->request->set('username', 'martin'); + $event = $this->createRequestEvent($request); + + $this->subscriber->onKernelRequest($event); + + $this->assertSame('{"method":"json_login"}', $event->getResponse()?->getContent()); + } + + public function testOnKernelRequestUserNameAndPassword(): void + { + $request = Request::create('/admin/login', Request::METHOD_POST); + $request->attributes->set('_route', 'sulu_admin.login_check'); + $request->request->set('username', 'martin'); + $request->request->set('password', '123'); + $event = $this->createRequestEvent($request); + + $this->subscriber->onKernelRequest($event); + + $this->assertNull($event->getResponse()); + } + + public function testOnKernelRequestResetPasswordUser(): void + { + $request = Request::create('/admin/login', Request::METHOD_POST); + $request->attributes->set('_route', 'sulu_security.reset_password.email'); + $request->request->set('username', 'martin'); + $event = $this->createRequestEvent($request); + + $this->subscriber->onKernelRequest($event); + + $this->assertNull($event->getResponse()); + } + + public function testOnKernelRequestResetPasswordEmail(): void + { + $request = Request::create('/admin/login', Request::METHOD_POST); + $request->attributes->set('_route', 'sulu_security.reset_password.email'); + $request->request->set('username', 'hello@sulu.io'); + $event = $this->createRequestEvent($request); + + $this->subscriber->onKernelRequest($event); + + $this->assertSame('{"method":"json_login"}', $event->getResponse()?->getContent()); + } + + public function testOnKernelRequestResetPasswordExistingUser(): void + { + $request = Request::create('/admin/login', Request::METHOD_POST); + $request->attributes->set('_route', 'sulu_security.reset_password.email'); + $request->request->set('username', 'admin'); + $event = $this->createRequestEvent($request); + $user = new User(); + $user->setEmail('admin@sulu.io'); + + $this->subscriber->onKernelRequest($event); + + $this->assertNull($event->getResponse()); + } + + public function testOnKernelRequestExistingUser(): void + { + $request = Request::create('/admin/login', Request::METHOD_POST); + $request->attributes->set('_route', 'sulu_security.reset_password.email'); + $request->request->set('username', 'admin'); + $event = $this->createRequestEvent($request); + $user = new User(); + $user->setEmail('admin@sulu.io'); + $this->userRepository->findUserByIdentifier('admin')->willReturn($user); + + $this->subscriber->onKernelRequest($event); + + $this->assertSame('{"method":"json_login"}', $event->getResponse()?->getContent()); + } + + public function testOnKernelRequestExistingUserAndPassword(): void + { + $request = Request::create('/admin/login', Request::METHOD_POST); + $request->attributes->set('_route', 'sulu_security.reset_password.email'); + $request->request->set('username', 'admin'); + $request->request->set('password', 'admin'); + $event = $this->createRequestEvent($request); + $user = new User(); + $user->setEmail('admin@sulu.io'); + $this->userRepository->findUserByIdentifier('admin')->willReturn($user); + + $this->subscriber->onKernelRequest($event); + + $this->assertNull($event->getResponse()); + } + + public function testOnKernelRequestSSIOEmail(): void + { + $redirectUrl = 'https://example.com/authorize'; + $request = Request::create('/admin/login', Request::METHOD_POST); + $request->attributes->set('_route', 'sulu_admin.login_check'); + $request->request->set('username', 'martin@sulu.io'); + $event = $this->createRequestEvent($request); + $openIdAdapter = $this->prophesize(OpenIdSingleSignOnAdapter::class); + $this->urlGenerator->generate('sulu_admin', [], UrlGeneratorInterface::ABSOLUTE_URL) + ->shouldBeCalled() + ->willReturn('/admin'); + $this->singleSignOnAdapterProvider->getAdapterByDomain('sulu.io')->willReturn($openIdAdapter->reveal()); + $openIdAdapter->generateLoginUrl($request, '/admin', 'sulu.io')->willReturn($redirectUrl); + + $this->subscriber->onKernelRequest($event); + + $this->assertSame('{"method":"redirect","url":' . \json_encode($redirectUrl) . '}', $event->getResponse()?->getContent()); + } + + public function testOnKernelRequestSSIOEmailPasswordReset(): void + { + $redirectUrl = 'https://example.com/authorize'; + $request = Request::create('/admin/login', Request::METHOD_POST); + $request->attributes->set('_route', 'sulu_security.reset_password.email'); + $request->request->set('username', 'martin@sulu.io'); + $event = $this->createRequestEvent($request); + $openIdAdapter = $this->prophesize(OpenIdSingleSignOnAdapter::class); + $this->urlGenerator->generate('sulu_admin', [], UrlGeneratorInterface::ABSOLUTE_URL) + ->shouldBeCalled() + ->willReturn('/admin'); + $this->singleSignOnAdapterProvider->getAdapterByDomain('sulu.io')->willReturn($openIdAdapter->reveal()); + $openIdAdapter->generateLoginUrl($request, '/admin', 'sulu.io')->willReturn($redirectUrl); + + $this->subscriber->onKernelRequest($event); + + $this->assertSame('{"method":"redirect","url":' . \json_encode($redirectUrl) . '}', $event->getResponse()?->getContent()); + } + + public function testOnKernelRequestExistingSSIOUser(): void + { + $redirectUrl = 'https://example.com/authorize'; + $request = Request::create('/admin/login', Request::METHOD_POST); + $request->attributes->set('_route', 'sulu_admin.login_check'); + $request->request->set('username', 'admin'); + $event = $this->createRequestEvent($request); + $openIdAdapter = $this->prophesize(OpenIdSingleSignOnAdapter::class); + $this->urlGenerator->generate('sulu_admin', [], UrlGeneratorInterface::ABSOLUTE_URL) + ->shouldBeCalled() + ->willReturn('/admin'); + $this->singleSignOnAdapterProvider->getAdapterByDomain('sulu.io')->willReturn($openIdAdapter->reveal()); + $openIdAdapter->generateLoginUrl($request, '/admin', 'sulu.io')->willReturn($redirectUrl); + $user = new User(); + $user->setEmail('admin@sulu.io'); + $this->userRepository->findUserByIdentifier('admin')->willReturn($user); + + $this->subscriber->onKernelRequest($event); + + $this->assertSame('{"method":"redirect","url":' . \json_encode($redirectUrl) . '}', $event->getResponse()?->getContent()); + } + + public function testOnKernelRequestExistingSSIOUserResetPassword(): void + { + $redirectUrl = 'https://example.com/authorize'; + $request = Request::create('/admin/login', Request::METHOD_POST); + $request->attributes->set('_route', 'sulu_security.reset_password.email'); + $request->request->set('username', 'admin'); + $event = $this->createRequestEvent($request); + $openIdAdapter = $this->prophesize(OpenIdSingleSignOnAdapter::class); + $this->urlGenerator->generate('sulu_admin', [], UrlGeneratorInterface::ABSOLUTE_URL) + ->shouldBeCalled() + ->willReturn('/admin'); + $this->singleSignOnAdapterProvider->getAdapterByDomain('sulu.io')->willReturn($openIdAdapter->reveal()); + $openIdAdapter->generateLoginUrl($request, '/admin', 'sulu.io')->willReturn($redirectUrl); + $user = new User(); + $user->setEmail('admin@sulu.io'); + $this->userRepository->findUserByIdentifier('admin')->willReturn($user); + + $this->subscriber->onKernelRequest($event); + + $this->assertSame('{"method":"redirect","url":' . \json_encode($redirectUrl) . '}', $event->getResponse()?->getContent()); + } + + private function createRequestEvent(Request $request, ?int $requestType = HttpKernelInterface::MAIN_REQUEST): RequestEvent + { + $httpKernel = $this->prophesize(HttpKernelInterface::class); + + return new RequestEvent( + $httpKernel->reveal(), + $request, + $requestType, + ); + } +} diff --git a/src/Sulu/Bundle/SecurityBundle/Tests/Unit/SingleSignOn/SingleSignOnTokeExtractorTest.php b/src/Sulu/Bundle/SecurityBundle/Tests/Unit/SingleSignOn/SingleSignOnTokeExtractorTest.php new file mode 100644 index 00000000000..11f39f15e4c --- /dev/null +++ b/src/Sulu/Bundle/SecurityBundle/Tests/Unit/SingleSignOn/SingleSignOnTokeExtractorTest.php @@ -0,0 +1,66 @@ + + */ + private $singleSignOnAdapterProvider; + + private SingleSignOnTokenExtractor $tokenExtractor; + + protected function setUp(): void + { + $this->singleSignOnAdapterProvider = $this->prophesize(SingleSignOnAdapterProvider::class); + + $this->tokenExtractor = new SingleSignOnTokenExtractor( + $this->singleSignOnAdapterProvider->reveal(), + ); + } + + public function testOnKernelRequestExistingSSIOUser(): void + { + $request = Request::create('/admin', Request::METHOD_GET); + $session = new Session(new MockArraySessionStorage()); + $session->set(OpenIdSingleSignOnAdapter::OPEN_ID_ATTRIBUTES, [ + 'state' => '3cc05262-d86b-4b93-9456-5594ae8f3ed0', + 'domain' => 'sulu.io', + ]); + $request->setSession($session); + $request->attributes->set('_route', 'sulu_admin'); + $request->query->set('code', '4/0AeaYSHCvSWVukKJ-rueX8WyKj-ycUM'); + $request->query->set('state', '3cc05262-d86b-4b93-9456-5594ae8f3ed0'); + $adapter = $this->prophesize(OpenIdSingleSignOnAdapter::class); + $adapter->isAuthorizationValid(Argument::any(), Argument::any())->willReturn(true); + $this->singleSignOnAdapterProvider->getAdapterByDomain('sulu.io')->willReturn($adapter->reveal()); + + $accessToken = $this->tokenExtractor->extractAccessToken($request); + $this->assertSame('sulu.io::4/0AeaYSHCvSWVukKJ-rueX8WyKj-ycUM', $accessToken); + } +} diff --git a/src/Sulu/Component/DocumentManager/Tests/Unit/Metadata/BaseMetadataFactoryTest.php b/src/Sulu/Component/DocumentManager/Tests/Unit/Metadata/BaseMetadataFactoryTest.php index d489118ffe3..110a99ce86b 100644 --- a/src/Sulu/Component/DocumentManager/Tests/Unit/Metadata/BaseMetadataFactoryTest.php +++ b/src/Sulu/Component/DocumentManager/Tests/Unit/Metadata/BaseMetadataFactoryTest.php @@ -163,6 +163,6 @@ public function testAllMetadata(): void $this->assertCount(2, $metadatas); $this->assertContainsOnlyInstancesOf(Metadata::class, $metadatas); $metadata = \reset($metadatas); - $this->assertEquals('Class\Page', $metadata ? $metadata->getClass() : null); + $this->assertEquals('Class\Page', $metadata->getClass()); } }