/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.keycloak.testsuite.client;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.FullNameMapper;
import org.keycloak.protocol.oidc.mappers.HardcodedRole;
import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
import org.keycloak.protocol.oidc.mappers.UserPropertyMapper;
import org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper;
import org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientTemplateRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.ComponentTypeRepresentation;
import org.keycloak.representations.idm.ConfigPropertyRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.clientregistration.RegistrationAccessToken;
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
import org.keycloak.services.clientregistration.policy.impl.ClientDisabledClientRegistrationPolicyFactory;
import org.keycloak.services.clientregistration.policy.impl.ClientTemplatesClientRegistrationPolicyFactory;
import org.keycloak.services.clientregistration.policy.impl.MaxClientsClientRegistrationPolicyFactory;
import org.keycloak.services.clientregistration.policy.impl.ProtocolMappersClientRegistrationPolicyFactory;
import org.keycloak.services.clientregistration.policy.impl.TrustedHostClientRegistrationPolicyFactory;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.util.JsonSerialization;

import javax.ws.rs.core.Response;

import static org.junit.Assert.assertTrue;

/**
 * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
 */
public class ClientRegistrationPoliciesTest extends AbstractClientRegistrationTest {

    private static final String PRIVATE_KEY = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=";
    private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB";

    @Override
    public void addTestRealms(List<RealmRepresentation> testRealms) {
        super.addTestRealms(testRealms);
        testRealms.get(0).setId(REALM_NAME);
        testRealms.get(0).setPrivateKey(PRIVATE_KEY);
        testRealms.get(0).setPublicKey(PUBLIC_KEY);
    }

    @After
    public void after() throws Exception {
        super.after();

        // Default setup of trustedHostPolicy
        ComponentRepresentation trustedHostPolicy = findPolicyByProviderAndAuth(TrustedHostClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAnon());
        trustedHostPolicy.getConfig().putSingle(TrustedHostClientRegistrationPolicyFactory.HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH, "true");
        trustedHostPolicy.getConfig().putSingle(TrustedHostClientRegistrationPolicyFactory.CLIENT_URIS_MUST_MATCH, "true");
        trustedHostPolicy.getConfig().put(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS, Collections.emptyList());
        realmResource().components().component(trustedHostPolicy.getId()).update(trustedHostPolicy);
    }

    private RealmResource realmResource() {
        return adminClient.realm(REALM_NAME);
    }

    private ClientRepresentation createRep(String clientId) {
        ClientRepresentation client = new ClientRepresentation();
        client.setClientId(clientId);
        client.setSecret("test-secret");
        return client;
    }

    private OIDCClientRepresentation createRepOidc() {
        return createRepOidc("http://localhost:8080/foo", "http://localhost:8080/foo");
    }

    private OIDCClientRepresentation createRepOidc(String clientBaseUri, String clientRedirectUri) {
        OIDCClientRepresentation client = new OIDCClientRepresentation();
        client.setClientName("RegistrationAccessTokenTest");
        client.setClientUri(clientBaseUri);
        client.setRedirectUris(Collections.singletonList(clientRedirectUri));
        return client;
    }

    public OIDCClientRepresentation create() throws ClientRegistrationException {
        OIDCClientRepresentation client = createRepOidc();

        OIDCClientRepresentation response = reg.oidc().create(client);

        reg.auth(Auth.token(response));

        return response;
    }

    private void assertOidcFail(ClientRegOp operation, OIDCClientRepresentation client, int expectedStatusCode) {
        assertOidcFail(operation, client, expectedStatusCode, null);
    }

    private void assertOidcFail(ClientRegOp operation, OIDCClientRepresentation client, int expectedStatusCode, String expectedErrorContains) {
        try {
            switch (operation) {
                case CREATE: reg.oidc().create(client);
                    break;
                case UPDATE: reg.oidc().update(client);
                    break;
                case DELETE: reg.oidc().delete(client);
                    break;
            }

            Assert.fail("Not expected to successfuly run operation " + operation.toString() + " on client");
        } catch (ClientRegistrationException expected) {
            HttpErrorException httpEx = (HttpErrorException) expected.getCause();
            Assert.assertEquals(expectedStatusCode, httpEx.getStatusLine().getStatusCode());
            if (expectedErrorContains != null) {
                assertTrue("Error response doesn't contain expected text. The error response text is: " + httpEx.getErrorResponse(), httpEx.getErrorResponse().contains(expectedErrorContains));
            }
        }
    }

    private void assertFail(ClientRegOp operation, ClientRepresentation client, int expectedStatusCode, String expectedErrorContains) {
        try {
            switch (operation) {
                case CREATE: reg.create(client);
                    break;
                case UPDATE: reg.update(client);
                    break;
                case DELETE: reg.delete(client);
                    break;
            }

            Assert.fail("Not expected to successfuly run operation " + operation.toString() + " on client");
        } catch (ClientRegistrationException expected) {
            HttpErrorException httpEx = (HttpErrorException) expected.getCause();
            Assert.assertEquals(expectedStatusCode, httpEx.getStatusLine().getStatusCode());
            if (expectedErrorContains != null) {
                assertTrue("Error response doesn't contain expected text. The error response text is: " + httpEx.getErrorResponse(), httpEx.getErrorResponse().contains(expectedErrorContains));
            }
        }
    }


    @Test
    public void testAnonCreateWithTrustedHost() throws Exception {
        // Failed to create client (untrusted host)
        OIDCClientRepresentation client = createRepOidc("http://root", "http://redirect");
        assertOidcFail(ClientRegOp.CREATE, client, 403, "Host not trusted");

        // Should still fail (bad redirect_uri)
        setTrustedHost("localhost");
        assertOidcFail(ClientRegOp.CREATE, client, 403, "URL doesn't match");

        // Should still fail (bad base_uri)
        client.setRedirectUris(Collections.singletonList("http://localhost:8080/foo"));
        assertOidcFail(ClientRegOp.CREATE, client, 403, "URL doesn't match");

        // Success create client
        client.setClientUri("http://localhost:8080/foo");
        OIDCClientRepresentation oidcClientRep = reg.oidc().create(client);


        // Test registration access token
        assertRegAccessToken(oidcClientRep.getRegistrationAccessToken(), RegistrationAuth.ANONYMOUS);
    }


    @Test
    public void testAnonUpdateWithTrustedHost() throws Exception {
        setTrustedHost("localhost");
        OIDCClientRepresentation client = create();

        // Fail update client
        client.setRedirectUris(Collections.singletonList("http://bad:8080/foo"));
        assertOidcFail(ClientRegOp.UPDATE, client, 403, "URL doesn't match");

        // Should be fine now
        client.setRedirectUris(Collections.singletonList("http://localhost:8080/foo"));
        reg.oidc().update(client);
    }


    @Test
    public void testRedirectUriWithDomain() throws Exception {
        // Change the policy to avoid checking hosts
        ComponentRepresentation trustedHostPolicyRep = findPolicyByProviderAndAuth(TrustedHostClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAnon());
        trustedHostPolicyRep.getConfig().putSingle(TrustedHostClientRegistrationPolicyFactory.HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH, "false");

        // Configure some trusted host and domain
        trustedHostPolicyRep.getConfig().put(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS, Arrays.asList("www.host.com", "*.example.com"));
        realmResource().components().component(trustedHostPolicyRep.getId()).update(trustedHostPolicyRep);

        // Verify client can be created with the redirectUri from trusted host and domain
        OIDCClientRepresentation oidcClientRep = createRepOidc("http://www.host.com", "http://www.example.com");
        reg.oidc().create(oidcClientRep);

        // Remove domain from the config
        trustedHostPolicyRep.getConfig().put(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS, Arrays.asList("www.host.com", "www1.example.com"));
        realmResource().components().component(trustedHostPolicyRep.getId()).update(trustedHostPolicyRep);

        // Check new client can't be created anymore
        oidcClientRep = createRepOidc("http://www.host.com", "http://www.example.com");
        assertOidcFail(ClientRegOp.CREATE, oidcClientRep, 403, "URL doesn't match");
    }




    @Test
    public void testAnonConsentRequired() throws Exception {
        setTrustedHost("localhost");
        OIDCClientRepresentation client = create();

        // Assert new client has consent required
        String clientId = client.getClientId();
        ClientRepresentation clientRep = ApiUtil.findClientByClientId(realmResource(), clientId).toRepresentation();
        Assert.assertTrue(clientRep.isConsentRequired());

        // Try update with disabled consent required. Should fail
        clientRep.setConsentRequired(false);
        assertFail(ClientRegOp.UPDATE, clientRep, 403, "Not permitted to update consentRequired to false");

        // Try update with enabled consent required. Should pass
        clientRep.setConsentRequired(true);
        reg.update(clientRep);
    }


    @Test
    public void testAnonFullScopeAllowed() throws Exception {
        setTrustedHost("localhost");
        OIDCClientRepresentation client = create();

        // Assert new client has fullScopeAllowed disabled
        String clientId = client.getClientId();
        ClientRepresentation clientRep = ApiUtil.findClientByClientId(realmResource(), clientId).toRepresentation();
        Assert.assertFalse(clientRep.isFullScopeAllowed());

        // Try update with disabled consent required. Should fail
        clientRep.setFullScopeAllowed(true);
        assertFail(ClientRegOp.UPDATE, clientRep, 403, "Not permitted to enable fullScopeAllowed");

        // Try update with enabled consent required. Should pass
        clientRep.setFullScopeAllowed(false);
        reg.update(clientRep);
    }


    @Test
    public void testClientDisabledPolicy() throws Exception {
        setTrustedHost("localhost");

        // Assert new client is enabled
        OIDCClientRepresentation client = create();
        String clientId = client.getClientId();
        ClientRepresentation clientRep = ApiUtil.findClientByClientId(realmResource(), clientId).toRepresentation();
        Assert.assertTrue(clientRep.isEnabled());

        // Add client-disabled policy
        ComponentRepresentation rep = new ComponentRepresentation();
        rep.setName("Clients disabled");
        rep.setParentId(REALM_NAME);
        rep.setProviderId(ClientDisabledClientRegistrationPolicyFactory.PROVIDER_ID);
        rep.setProviderType(ClientRegistrationPolicy.class.getName());
        rep.setSubType(getPolicyAnon());
        Response response = realmResource().components().add(rep);
        String policyId = ApiUtil.getCreatedId(response);
        response.close();

        // Assert new client is disabled
        client = create();
        clientId = client.getClientId();
        clientRep = ApiUtil.findClientByClientId(realmResource(), clientId).toRepresentation();
        Assert.assertFalse(clientRep.isEnabled());

        // Try enable client. Should fail
        clientRep.setEnabled(true);
        assertFail(ClientRegOp.UPDATE, clientRep, 403, "Not permitted to enable client");

        // Try update disabled client. Should pass
        clientRep.setEnabled(false);
        reg.update(clientRep);

        // Revert
        realmResource().components().component(policyId).remove();
    }


    @Test
    public void testMaxClientsPolicy() throws Exception {
        setTrustedHost("localhost");

        int clientsCount = realmResource().clients().findAll().size();
        int newClientsLimit = clientsCount + 1;

        // Allow to create one more client to current limit
        ComponentRepresentation maxClientsPolicyRep = findPolicyByProviderAndAuth(MaxClientsClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAnon());
        maxClientsPolicyRep.getConfig().putSingle(MaxClientsClientRegistrationPolicyFactory.MAX_CLIENTS, String.valueOf(newClientsLimit));
        realmResource().components().component(maxClientsPolicyRep.getId()).update(maxClientsPolicyRep);

        // I can register one new client
        OIDCClientRepresentation client = create();

        // I can't register more clients
        assertOidcFail(ClientRegOp.CREATE, createRepOidc(), 403, "It's allowed to have max " + newClientsLimit + " clients per realm");

        // Revert
        maxClientsPolicyRep.getConfig().putSingle(MaxClientsClientRegistrationPolicyFactory.MAX_CLIENTS, String.valueOf(10000));
        realmResource().components().component(maxClientsPolicyRep.getId()).update(maxClientsPolicyRep);
    }


    @Test
    public void testProviders() throws Exception {
        List<ComponentTypeRepresentation> reps = realmResource().clientRegistrationPolicy().getProviders();
        Map<String, ComponentTypeRepresentation> providersMap = reps.stream().collect(Collectors.toMap((ComponentTypeRepresentation rep) -> {
            return rep.getId();
        }, (ComponentTypeRepresentation rep) -> {
            return rep;
        }));

        // test that ProtocolMappersClientRegistrationPolicy provider contains available protocol mappers
        ComponentTypeRepresentation protMappersRep = providersMap.get(ProtocolMappersClientRegistrationPolicyFactory.PROVIDER_ID);
        List<String> availableMappers = getProviderConfigProperty(protMappersRep, ProtocolMappersClientRegistrationPolicyFactory.ALLOWED_PROTOCOL_MAPPER_TYPES);

        List<String> someExpectedMappers = Arrays.asList(UserAttributeStatementMapper.PROVIDER_ID,
                UserAttributeMapper.PROVIDER_ID,
                UserPropertyAttributeStatementMapper.PROVIDER_ID,
                UserPropertyMapper.PROVIDER_ID, HardcodedRole.PROVIDER_ID);
        availableMappers.containsAll(someExpectedMappers);

        // test that clientTemplate provider doesn't contain any client templates yet
        ComponentTypeRepresentation clientTemplateRep = providersMap.get(ClientTemplatesClientRegistrationPolicyFactory.PROVIDER_ID);
        List<String> clientTemplates = getProviderConfigProperty(clientTemplateRep, ClientTemplatesClientRegistrationPolicyFactory.ALLOWED_CLIENT_TEMPLATES);
        Assert.assertTrue(clientTemplates.isEmpty());

        // Add some clientTemplates
        ClientTemplateRepresentation clientTemplate = new ClientTemplateRepresentation();
        clientTemplate.setName("foo");
        Response response = realmResource().clientTemplates().create(clientTemplate);
        String fooTemplateId = ApiUtil.getCreatedId(response);
        response.close();

        clientTemplate = new ClientTemplateRepresentation();
        clientTemplate.setName("bar");
        response = realmResource().clientTemplates().create(clientTemplate);
        String barTemplateId = ApiUtil.getCreatedId(response);
        response.close();

        // send request again and test that clientTemplate provider contains added client templates
        reps = realmResource().clientRegistrationPolicy().getProviders();
        clientTemplateRep = reps.stream().filter((ComponentTypeRepresentation rep1) -> {

            return rep1.getId().equals(ClientTemplatesClientRegistrationPolicyFactory.PROVIDER_ID);

        }).findFirst().get();

        clientTemplates = getProviderConfigProperty(clientTemplateRep, ClientTemplatesClientRegistrationPolicyFactory.ALLOWED_CLIENT_TEMPLATES);
        Assert.assertNames(clientTemplates, "foo", "bar");

        // Revert client templates
        realmResource().clientTemplates().get(fooTemplateId).remove();
        realmResource().clientTemplates().get(barTemplateId).remove();
    }

    private List<String> getProviderConfigProperty(ComponentTypeRepresentation provider, String expectedConfigPropName) {
        Assert.assertNotNull(provider);

        List<ConfigPropertyRepresentation> list = provider.getProperties();

        list = list.stream().filter((ConfigPropertyRepresentation rep) -> {

            return rep.getName().equals(expectedConfigPropName);

        }).collect(Collectors.toList());

        Assert.assertEquals(list.size(), 1);
        ConfigPropertyRepresentation allowedProtocolMappers = list.get(0);

        Assert.assertEquals(allowedProtocolMappers.getName(), expectedConfigPropName);
        return allowedProtocolMappers.getOptions();
    }


    @Test
    public void testClientTemplatesPolicy() throws Exception {
        setTrustedHost("localhost");

        // Add some clientTemplate through Admin REST
        ClientTemplateRepresentation clientTemplate = new ClientTemplateRepresentation();
        clientTemplate.setName("foo");
        Response response = realmResource().clientTemplates().create(clientTemplate);
        String clientTemplateId = ApiUtil.getCreatedId(response);
        response.close();

        // I can't register new client with this template
        ClientRepresentation clientRep = createRep("test-app");
        clientRep.setClientTemplate("foo");
        assertFail(ClientRegOp.CREATE, clientRep, 403, "Not permitted to use specified clientTemplate");

        // Register client without template - should success
        clientRep.setClientTemplate(null);
        ClientRepresentation registeredClient = reg.create(clientRep);
        reg.auth(Auth.token(registeredClient));

        // Try to update client with template - should fail
        registeredClient.setClientTemplate("foo");
        assertFail(ClientRegOp.UPDATE, registeredClient, 403, "Not permitted to use specified clientTemplate");

        // Update client with the clientTemplate via Admin REST
        ClientRepresentation client = ApiUtil.findClientByClientId(realmResource(), "test-app").toRepresentation();
        client.setClientTemplate("foo");
        realmResource().clients().get(client.getId()).update(client);

        // Now the update via clientRegistration is permitted too as template was already set
        reg.update(registeredClient);

        // Revert client template
        realmResource().clients().get(client.getId()).remove();
        realmResource().clientTemplates().get(clientTemplateId).remove();
    }


    @Test
    public void testClientTemplatesPolicyWithPermittedTemplate() throws Exception {
        setTrustedHost("localhost");

        // Add some clientTemplate through Admin REST
        ClientTemplateRepresentation clientTemplate = new ClientTemplateRepresentation();
        clientTemplate.setName("foo");
        Response response = realmResource().clientTemplates().create(clientTemplate);
        String clientTemplateId = ApiUtil.getCreatedId(response);
        response.close();

        // I can't register new client with this template
        ClientRepresentation clientRep = createRep("test-app");
        clientRep.setClientTemplate("foo");
        assertFail(ClientRegOp.CREATE, clientRep, 403, "Not permitted to use specified clientTemplate");

        // Update the policy to allow the "foo" template
        ComponentRepresentation clientTemplatesPolicyRep = findPolicyByProviderAndAuth(ClientTemplatesClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAnon());
        clientTemplatesPolicyRep.getConfig().putSingle(ClientTemplatesClientRegistrationPolicyFactory.ALLOWED_CLIENT_TEMPLATES, "foo");
        realmResource().components().component(clientTemplatesPolicyRep.getId()).update(clientTemplatesPolicyRep);

        // Check that I can register client now
        ClientRepresentation registeredClient = reg.create(clientRep);
        Assert.assertNotNull(registeredClient.getRegistrationAccessToken());

        // Revert client template
        ApiUtil.findClientResourceByClientId(realmResource(), "test-app").remove();
        realmResource().clientTemplates().get(clientTemplateId).remove();
    }


    // PROTOCOL MAPPERS

    @Test
    public void testProtocolMappersCreate() throws Exception {
        setTrustedHost("localhost");

        // Try to add client with some "hardcoded role" mapper. Should fail
        ClientRepresentation clientRep = createRep("test-app");
        clientRep.setProtocolMappers(Collections.singletonList(createHardcodedMapperRep()));
        assertFail(ClientRegOp.CREATE, clientRep, 403, "ProtocolMapper type not allowed");

        // Try the same authenticated. Should still fail.
        ClientInitialAccessPresentation token = adminClient.realm(REALM_NAME).clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10));
        reg.auth(Auth.token(token));
        assertFail(ClientRegOp.CREATE, clientRep, 403, "ProtocolMapper type not allowed");

        // Update the "authenticated" policy and allow hardcoded role mapper
        ComponentRepresentation protocolMapperPolicyRep = findPolicyByProviderAndAuth(ProtocolMappersClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAuth());
        protocolMapperPolicyRep.getConfig().add(ProtocolMappersClientRegistrationPolicyFactory.ALLOWED_PROTOCOL_MAPPER_TYPES, HardcodedRole.PROVIDER_ID);
        realmResource().components().component(protocolMapperPolicyRep.getId()).update(protocolMapperPolicyRep);

        // Check authenticated registration is permitted
        ClientRepresentation registeredClient = reg.create(clientRep);
        Assert.assertNotNull(registeredClient.getRegistrationAccessToken());

        // Check "anonymous" registration still fails
        clientRep = createRep("test-app-2");
        clientRep.setProtocolMappers(Collections.singletonList(createHardcodedMapperRep()));
        reg.auth(null);
        assertFail(ClientRegOp.CREATE, clientRep, 403, "ProtocolMapper type not allowed");

        // Revert policy change
        ApiUtil.findClientResourceByClientId(realmResource(), "test-app").remove();
        protocolMapperPolicyRep.getConfig().remove(ProtocolMappersClientRegistrationPolicyFactory.ALLOWED_PROTOCOL_MAPPER_TYPES, HardcodedRole.PROVIDER_ID);
        realmResource().components().component(protocolMapperPolicyRep.getId()).update(protocolMapperPolicyRep);
    }


    private ProtocolMapperRepresentation createHardcodedMapperRep() {
        ProtocolMapperRepresentation protocolMapper = new ProtocolMapperRepresentation();
        protocolMapper.setName("Hardcoded foo role");
        protocolMapper.setProtocolMapper(HardcodedRole.PROVIDER_ID);
        protocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
        protocolMapper.setConsentRequired(false);
        protocolMapper.setConsentText(null);
        protocolMapper.getConfig().put(HardcodedRole.ROLE_CONFIG, "foo-role");
        return protocolMapper;
    }


    @Test
    public void testProtocolMappersUpdate() throws Exception {
        setTrustedHost("localhost");

        // Check I can add client with allowed protocolMappers
        ProtocolMapperRepresentation protocolMapper = new ProtocolMapperRepresentation();
        protocolMapper.setName("Full name");
        protocolMapper.setProtocolMapper(FullNameMapper.PROVIDER_ID);
        protocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
        protocolMapper.setConsentRequired(true);
        protocolMapper.setConsentText("Full name");

        ClientRepresentation clientRep = createRep("test-app");
        clientRep.setProtocolMappers(Collections.singletonList(protocolMapper));

        ClientRepresentation registeredClient = reg.create(clientRep);
        reg.auth(Auth.token(registeredClient));

        // Add some disallowed protocolMapper
        registeredClient.getProtocolMappers().add(createHardcodedMapperRep());

        // Check I can't update client because of protocolMapper
        assertFail(ClientRegOp.UPDATE, registeredClient, 403, "ProtocolMapper type not allowed");

        // Remove "bad" protocolMapper
        registeredClient.getProtocolMappers().removeIf((ProtocolMapperRepresentation mapper) -> {
            return mapper.getProtocolMapper().equals(HardcodedRole.PROVIDER_ID);
        });

        // Check I can update client now
        reg.update(registeredClient);

        // Revert client
        ApiUtil.findClientResourceByClientId(realmResource(), "test-app").remove();
    }


    @Test
    public void testProtocolMappersConsentRequired() throws Exception {
        setTrustedHost("localhost");

        // Register client and assert it has builtin protocol mappers
        ClientRepresentation clientRep = createRep("test-app");
        ClientRepresentation registeredClient = reg.create(clientRep);

        long usernamePropMappersCount = registeredClient.getProtocolMappers().stream().filter((ProtocolMapperRepresentation protocolMapper) -> {
            return protocolMapper.getProtocolMapper().equals(UserPropertyMapper.PROVIDER_ID);
        }).count();
        Assert.assertTrue(usernamePropMappersCount > 0);

        // Remove USernamePropertyMapper from the policy configuration
        ComponentRepresentation protocolMapperPolicyRep = findPolicyByProviderAndAuth(ProtocolMappersClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAnon());
        protocolMapperPolicyRep.getConfig().getList(ProtocolMappersClientRegistrationPolicyFactory.ALLOWED_PROTOCOL_MAPPER_TYPES).remove(UserPropertyMapper.PROVIDER_ID);
        realmResource().components().component(protocolMapperPolicyRep.getId()).update(protocolMapperPolicyRep);

        // Register another client. Assert it doesn't have builtin mappers anymore
        clientRep = createRep("test-app-2");
        registeredClient = reg.create(clientRep);

        usernamePropMappersCount = registeredClient.getProtocolMappers().stream().filter((ProtocolMapperRepresentation protocolMapper) -> {
            return protocolMapper.getProtocolMapper().equals(UserPropertyMapper.PROVIDER_ID);
        }).count();
        Assert.assertEquals(0, usernamePropMappersCount);

        // Revert
        ApiUtil.findClientResourceByClientId(realmResource(), "test-app").remove();
        protocolMapperPolicyRep.getConfig().getList(ProtocolMappersClientRegistrationPolicyFactory.ALLOWED_PROTOCOL_MAPPER_TYPES).add(UserPropertyMapper.PROVIDER_ID);
        realmResource().components().component(protocolMapperPolicyRep.getId()).update(protocolMapperPolicyRep);
    }


    @Test
    public void testProtocolMappersRemoveBuiltins() throws Exception {
        setTrustedHost("localhost");

        // Change policy to allow hardcoded mapper

        ComponentRepresentation protocolMapperPolicyRep = findPolicyByProviderAndAuth(ProtocolMappersClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAnon());
        protocolMapperPolicyRep.getConfig().add(ProtocolMappersClientRegistrationPolicyFactory.ALLOWED_PROTOCOL_MAPPER_TYPES, HardcodedRole.PROVIDER_ID);
        realmResource().components().component(protocolMapperPolicyRep.getId()).update(protocolMapperPolicyRep);

        // Create client with hardcoded mapper
        ClientRepresentation clientRep = createRep("test-app");
        clientRep.setProtocolMappers(Collections.singletonList(createHardcodedMapperRep()));
        ClientRepresentation registeredClient = reg.create(clientRep);

        Assert.assertEquals(1, registeredClient.getProtocolMappers().size());
        ProtocolMapperRepresentation hardcodedMapper = registeredClient.getProtocolMappers().get(0);
        Assert.assertTrue(hardcodedMapper.isConsentRequired());
        Assert.assertEquals("Hardcoded foo role", hardcodedMapper.getConsentText());

        // Revert
        ApiUtil.findClientResourceByClientId(realmResource(), "test-app").remove();
        protocolMapperPolicyRep.getConfig().remove(ProtocolMappersClientRegistrationPolicyFactory.ALLOWED_PROTOCOL_MAPPER_TYPES, HardcodedRole.PROVIDER_ID);
        realmResource().components().component(protocolMapperPolicyRep.getId()).update(protocolMapperPolicyRep);
    }

    // HELPER METHODS


    private String getPolicyAnon() {
        return ClientRegistrationPolicyManager.getComponentTypeKey(RegistrationAuth.ANONYMOUS);
    }

    private String getPolicyAuth() {
        return ClientRegistrationPolicyManager.getComponentTypeKey(RegistrationAuth.AUTHENTICATED);
    }

    private ComponentRepresentation findPolicyByProviderAndAuth(String providerId, String authType) {
        // Change the policy to avoid checking hosts
        List<ComponentRepresentation> reps = realmResource().components().query(REALM_NAME, ClientRegistrationPolicy.class.getName());
        for (ComponentRepresentation rep : reps) {
            if (rep.getSubType().equals(authType) && rep.getProviderId().equals(providerId)) {
                return rep;
            }
        }
        return null;
    }

    private void setTrustedHost(String hostname) {
        ComponentRepresentation trustedHostRep = findPolicyByProviderAndAuth(TrustedHostClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAnon());
        trustedHostRep.getConfig().putSingle(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS, hostname);
        realmResource().components().component(trustedHostRep.getId()).update(trustedHostRep);
    }

    private void assertRegAccessToken(String registrationAccessToken, RegistrationAuth expectedRegAuth) throws Exception {
        byte[] content = new JWSInput(registrationAccessToken).getContent();
        RegistrationAccessToken regAccessToken = JsonSerialization.readValue(content, RegistrationAccessToken.class);
        Assert.assertEquals(regAccessToken.getRegistrationAuth(), expectedRegAuth.toString().toLowerCase());
    }

    private enum ClientRegOp {
        CREATE, READ, UPDATE, DELETE
    }

}
