/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.syncope.fit.core;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.security.AccessControlException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.sql.DataSource;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.syncope.client.lib.AnonymousAuthenticationHandler;
import org.apache.syncope.client.lib.BasicAuthenticationHandler;
import org.apache.syncope.client.lib.SyncopeClient;
import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.SyncopeConstants;
import org.apache.syncope.common.lib.patch.DeassociationPatch;
import org.apache.syncope.common.lib.patch.MembershipPatch;
import org.apache.syncope.common.lib.patch.PasswordPatch;
import org.apache.syncope.common.lib.patch.StatusPatch;
import org.apache.syncope.common.lib.patch.StringPatchItem;
import org.apache.syncope.common.lib.patch.StringReplacePatchItem;
import org.apache.syncope.common.lib.patch.UserPatch;
import org.apache.syncope.common.lib.to.AnyObjectTO;
import org.apache.syncope.common.lib.to.AnyTypeClassTO;
import org.apache.syncope.common.lib.to.AnyTypeTO;
import org.apache.syncope.common.lib.to.GroupTO;
import org.apache.syncope.common.lib.to.MembershipTO;
import org.apache.syncope.common.lib.to.PagedResult;
import org.apache.syncope.common.lib.to.PlainSchemaTO;
import org.apache.syncope.common.lib.to.ProvisioningResult;
import org.apache.syncope.common.lib.to.RoleTO;
import org.apache.syncope.common.lib.to.UserTO;
import org.apache.syncope.common.lib.to.UserRequestForm;
import org.apache.syncope.common.lib.types.AnyTypeKind;
import org.apache.syncope.common.lib.types.AttrSchemaType;
import org.apache.syncope.common.lib.types.CipherAlgorithm;
import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.common.lib.types.PatchOperation;
import org.apache.syncope.common.lib.types.ResourceDeassociationAction;
import org.apache.syncope.common.lib.types.SchemaType;
import org.apache.syncope.common.lib.types.StandardEntitlement;
import org.apache.syncope.common.lib.types.StatusPatchType;
import org.apache.syncope.common.rest.api.RESTHeaders;
import org.apache.syncope.common.rest.api.beans.AnyQuery;
import org.apache.syncope.common.rest.api.beans.UserRequestQuery;
import org.apache.syncope.common.rest.api.service.AnyObjectService;
import org.apache.syncope.common.rest.api.service.SchemaService;
import org.apache.syncope.common.rest.api.service.UserService;
import org.apache.syncope.core.spring.security.Encryptor;
import org.apache.syncope.fit.AbstractITCase;
import org.apache.syncope.fit.ElasticsearchDetector;
import org.apache.syncope.fit.FlowableDetector;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig(locations = { "classpath:testJDBCEnv.xml" })
public class AuthenticationITCase extends AbstractITCase {

    @Autowired
    private DataSource testDataSource;

    private int getFailedLogins(final UserService userService, final String userKey) {
        UserTO readUserTO = userService.read(userKey);
        assertNotNull(readUserTO);
        assertNotNull(readUserTO.getFailedLogins());
        return readUserTO.getFailedLogins();
    }

    @Test
    public void readEntitlements() {
        // 1. as not authenticated (not allowed)
        try {
            clientFactory.create().self();
            fail("This should not happen");
        } catch (AccessControlException e) {
            assertNotNull(e);
        }

        // 2. as anonymous
        Triple<Map<String, Set<String>>, List<String>, UserTO> self = clientFactory.create(
                new AnonymousAuthenticationHandler(ANONYMOUS_UNAME, ANONYMOUS_KEY)).self();
        assertEquals(1, self.getLeft().size());
        assertTrue(self.getLeft().keySet().contains(StandardEntitlement.ANONYMOUS));
        assertEquals(Collections.emptyList(), self.getMiddle());
        assertEquals(ANONYMOUS_UNAME, self.getRight().getUsername());

        // 3. as admin
        self = adminClient.self();
        assertEquals(syncopeService.platform().getEntitlements().size(), self.getLeft().size());
        assertFalse(self.getLeft().keySet().contains(StandardEntitlement.ANONYMOUS));
        assertEquals(Collections.emptyList(), self.getMiddle());
        assertEquals(ADMIN_UNAME, self.getRight().getUsername());

        // 4. as user
        self = clientFactory.create("bellini", ADMIN_PWD).self();
        assertFalse(self.getLeft().isEmpty());
        assertFalse(self.getLeft().keySet().contains(StandardEntitlement.ANONYMOUS));
        assertEquals(Collections.emptyList(), self.getMiddle());
        assertEquals("bellini", self.getRight().getUsername());
    }

    @Test
    public void userSchemaAuthorization() {
        String schemaName = "authTestSchema" + getUUIDString();

        // 1. create a schema (as admin)
        PlainSchemaTO schemaTO = new PlainSchemaTO();
        schemaTO.setKey(schemaName);
        schemaTO.setMandatoryCondition("false");
        schemaTO.setType(AttrSchemaType.String);

        PlainSchemaTO newPlainSchemaTO = createSchema(SchemaType.PLAIN, schemaTO);
        assertEquals(schemaTO, newPlainSchemaTO);

        // 2. create an user with the role created above (as admin)
        UserTO userTO = UserITCase.getUniqueSampleTO("auth@test.org");
        userTO = createUser(userTO).getEntity();
        assertNotNull(userTO);

        // 3. read the schema created above (as admin) - success
        schemaTO = schemaService.read(SchemaType.PLAIN, schemaName);
        assertNotNull(schemaTO);

        // 4. read the schema created above (as user) - success
        SchemaService schemaService2 = clientFactory.create(userTO.getUsername(), "password123").
                getService(SchemaService.class);
        schemaTO = schemaService2.read(SchemaType.PLAIN, schemaName);
        assertNotNull(schemaTO);

        // 5. update the schema create above (as user) - failure
        try {
            schemaService2.update(SchemaType.PLAIN, schemaTO);
            fail("Schema update as user should not work");
        } catch (ForbiddenException e) {
            assertNotNull(e);
        }

        assertEquals(0, getFailedLogins(userService, userTO.getKey()));
    }

    @Test
    public void userRead() {
        UserTO userTO = UserITCase.getUniqueSampleTO("testuserread@test.org");
        userTO.getRoles().add("User manager");

        userTO = createUser(userTO).getEntity();
        assertNotNull(userTO);

        UserService userService2 = clientFactory.create(userTO.getUsername(), "password123").
                getService(UserService.class);

        UserTO readUserTO = userService2.read("1417acbe-cbf6-4277-9372-e75e04f97000");
        assertNotNull(readUserTO);

        UserService userService3 = clientFactory.create("puccini", ADMIN_PWD).getService(UserService.class);

        try {
            userService3.read("b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee");
            fail("This should not happen");
        } catch (SyncopeClientException e) {
            assertNotNull(e);
            assertEquals(ClientExceptionType.DelegatedAdministration, e.getType());
        }
    }

    @Test
    public void userSearch() {
        UserTO userTO = UserITCase.getUniqueSampleTO("testusersearch@test.org");
        userTO.getRoles().add("User reviewer");

        userTO = createUser(userTO).getEntity();
        assertNotNull(userTO);

        // 1. user assigned to role 1, with search entitlement on realms /odd and /even: won't find anything with 
        // root realm
        UserService userService2 = clientFactory.create(userTO.getUsername(), "password123").
                getService(UserService.class);

        PagedResult<UserTO> matchingUsers = userService2.search(new AnyQuery.Builder().
                realm(SyncopeConstants.ROOT_REALM).
                fiql(SyncopeClient.getUserSearchConditionBuilder().isNotNull("key").query()).build());
        assertNotNull(matchingUsers);
        assertFalse(matchingUsers.getResult().isEmpty());
        Set<String> matchingUserKeys = matchingUsers.getResult().stream().
                map(user -> user.getKey()).collect(Collectors.toSet());
        assertTrue(matchingUserKeys.contains("1417acbe-cbf6-4277-9372-e75e04f97000"));
        assertFalse(matchingUserKeys.contains("74cd8ece-715a-44a4-a736-e17b46c4e7e6"));
        assertFalse(matchingUserKeys.contains("823074dc-d280-436d-a7dd-07399fae48ec"));

        // 2. user assigned to role 4, with search entitlement on realm /even/two
        UserService userService3 = clientFactory.create("puccini", ADMIN_PWD).getService(UserService.class);

        matchingUsers = userService3.search(new AnyQuery.Builder().realm("/even/two").
                fiql(SyncopeClient.getUserSearchConditionBuilder().isNotNull("loginDate").query()).build());
        assertNotNull(matchingUsers);
        assertTrue(matchingUsers.getResult().stream().allMatch(matching -> "/even/two".equals(matching.getRealm())));
    }

    @Test
    public void delegatedUserCRUD() {
        String roleKey = null;
        String delegatedAdminKey = null;
        try {
            // 1. create role for full user administration, under realm /even/two
            RoleTO role = new RoleTO();
            role.setKey("Delegated user admin");
            role.getEntitlements().add(StandardEntitlement.USER_CREATE);
            role.getEntitlements().add(StandardEntitlement.USER_UPDATE);
            role.getEntitlements().add(StandardEntitlement.USER_DELETE);
            role.getEntitlements().add(StandardEntitlement.USER_SEARCH);
            role.getEntitlements().add(StandardEntitlement.USER_READ);
            role.getRealms().add("/even/two");

            roleKey = roleService.create(role).getHeaderString(RESTHeaders.RESOURCE_KEY);
            assertNotNull(roleKey);

            // 2. as admin, create delegated admin user, and assign the role just created
            UserTO delegatedAdmin = UserITCase.getUniqueSampleTO("admin@syncope.apache.org");
            delegatedAdmin.getRoles().add(roleKey);
            delegatedAdmin = createUser(delegatedAdmin).getEntity();
            delegatedAdminKey = delegatedAdmin.getKey();

            // 3. instantiate a delegate user service client, for further operatins
            UserService delegatedUserService =
                    clientFactory.create(delegatedAdmin.getUsername(), "password123").getService(UserService.class);

            // 4. as delegated, create user under realm / -> fail
            UserTO user = UserITCase.getUniqueSampleTO("delegated@syncope.apache.org");
            try {
                delegatedUserService.create(user, true);
                fail("This should not happen");
            } catch (SyncopeClientException e) {
                assertEquals(ClientExceptionType.DelegatedAdministration, e.getType());
            }

            // 5. set realm to /even/two -> succeed
            user.setRealm("/even/two");

            Response response = delegatedUserService.create(user, true);
            assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());

            user = response.readEntity(new GenericType<ProvisioningResult<UserTO>>() {
            }).getEntity();
            assertEquals("surname", user.getPlainAttr("surname").get().getValues().get(0));

            // 5. as delegated, update user attempting to move under realm / -> fail
            UserPatch userPatch = new UserPatch();
            userPatch.setKey(user.getKey());
            userPatch.setRealm(new StringReplacePatchItem.Builder().value("/odd").build());
            userPatch.getPlainAttrs().add(attrAddReplacePatch("surname", "surname2"));

            try {
                delegatedUserService.update(userPatch);
                fail("This should not happen");
            } catch (SyncopeClientException e) {
                assertEquals(ClientExceptionType.DelegatedAdministration, e.getType());
            }

            // 6. revert realm change -> succeed
            userPatch.setRealm(null);

            response = delegatedUserService.update(userPatch);
            assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());

            user = response.readEntity(new GenericType<ProvisioningResult<UserTO>>() {
            }).getEntity();
            assertEquals("surname2", user.getPlainAttr("surname").get().getValues().get(0));

            // 7. as delegated, delete user
            delegatedUserService.delete(user.getKey());

            try {
                userService.read(user.getKey());
                fail("This should not happen");
            } catch (SyncopeClientException e) {
                assertEquals(ClientExceptionType.NotFound, e.getType());
            }
        } finally {
            if (roleKey != null) {
                roleService.delete(roleKey);
            }
            if (delegatedAdminKey != null) {
                userService.delete(delegatedAdminKey);
            }
        }
    }

    @Test
    public void checkFailedLogins() {
        UserTO userTO = UserITCase.getUniqueSampleTO("checkFailedLogin@syncope.apache.org");
        userTO.getRoles().add("User manager");

        userTO = createUser(userTO).getEntity();
        assertNotNull(userTO);
        String userKey = userTO.getKey();

        UserService userService2 = clientFactory.create(userTO.getUsername(), "password123").
                getService(UserService.class);
        assertEquals(0, getFailedLogins(userService2, userKey));

        // authentications failed ...
        try {
            clientFactory.create(userTO.getUsername(), "wrongpwd1");
            fail("This should not happen");
        } catch (AccessControlException e) {
            assertNotNull(e);
        }
        try {
            clientFactory.create(userTO.getUsername(), "wrongpwd1");
            fail("This should not happen");
        } catch (AccessControlException e) {
            assertNotNull(e);
        }
        assertEquals(2, getFailedLogins(userService, userKey));

        UserService userService4 = clientFactory.create(userTO.getUsername(), "password123").
                getService(UserService.class);
        assertEquals(0, getFailedLogins(userService4, userKey));
    }

    @Test
    public void checkUserSuspension() {
        UserTO userTO = UserITCase.getUniqueSampleTO("checkSuspension@syncope.apache.org");
        userTO.setRealm("/odd");
        userTO.getRoles().add("User manager");

        userTO = createUser(userTO).getEntity();
        String userKey = userTO.getKey();
        assertNotNull(userTO);

        assertEquals(0, getFailedLogins(userService, userKey));

        // authentications failed ...
        try {
            clientFactory.create(userTO.getUsername(), "wrongpwd1");
            fail("This should not happen");
        } catch (AccessControlException e) {
            assertNotNull(e);
        }
        try {
            clientFactory.create(userTO.getUsername(), "wrongpwd1");
            fail("This should not happen");
        } catch (AccessControlException e) {
            assertNotNull(e);
        }
        try {
            clientFactory.create(userTO.getUsername(), "wrongpwd1");
            fail("This should not happen");
        } catch (AccessControlException e) {
            assertNotNull(e);
        }

        assertEquals(3, getFailedLogins(userService, userKey));

        // last authentication before suspension
        try {
            clientFactory.create(userTO.getUsername(), "wrongpwd1");
            fail("This should not happen");
        } catch (AccessControlException e) {
            assertNotNull(e);
        }

        userTO = userService.read(userTO.getKey());
        assertNotNull(userTO);
        assertNotNull(userTO.getFailedLogins());
        assertEquals(3, userTO.getFailedLogins().intValue());
        assertEquals("suspended", userTO.getStatus());

        // Access with correct credentials should fail as user is suspended
        try {
            clientFactory.create(userTO.getUsername(), "password123");
            fail("This should not happen");
        } catch (AccessControlException e) {
            assertNotNull(e);
        }

        StatusPatch reactivate = new StatusPatch.Builder().key(userTO.getKey()).
                type(StatusPatchType.REACTIVATE).build();
        userTO = userService.status(reactivate).readEntity(new GenericType<ProvisioningResult<UserTO>>() {
        }).getEntity();
        assertNotNull(userTO);
        assertEquals("active", userTO.getStatus());

        SyncopeClient goodPwdClient = clientFactory.create(userTO.getUsername(), "password123");
        assertEquals(0, goodPwdClient.self().getRight().getFailedLogins().intValue());
    }

    @Test
    public void anyTypeEntitlement() {
        final String anyTypeKey = "FOLDER " + getUUIDString();

        // 1. no entitlement exists (yet) for the any type to be created
        assertFalse(syncopeService.platform().getEntitlements().stream().
                anyMatch(entitlement -> entitlement.contains(anyTypeKey)));

        // 2. create plain schema, any type class and any type
        PlainSchemaTO path = new PlainSchemaTO();
        path.setKey("path" + getUUIDString());
        path.setType(AttrSchemaType.String);
        path = createSchema(SchemaType.PLAIN, path);

        AnyTypeClassTO anyTypeClass = new AnyTypeClassTO();
        anyTypeClass.setKey("folder" + getUUIDString());
        anyTypeClass.getPlainSchemas().add(path.getKey());
        anyTypeClassService.create(anyTypeClass);

        AnyTypeTO anyTypeTO = new AnyTypeTO();
        anyTypeTO.setKey(anyTypeKey);
        anyTypeTO.setKind(AnyTypeKind.ANY_OBJECT);
        anyTypeTO.getClasses().add(anyTypeClass.getKey());
        anyTypeService.create(anyTypeTO);

        // 2. now entitlement exists for the any type just created
        assertTrue(syncopeService.platform().getEntitlements().stream().
                anyMatch(entitlement -> entitlement.contains(anyTypeKey)));

        // 3. attempt to create an instance of the type above: fail because no entitlement was assigned
        AnyObjectTO folder = new AnyObjectTO();
        folder.setName("home");
        folder.setRealm(SyncopeConstants.ROOT_REALM);
        folder.setType(anyTypeKey);
        folder.getPlainAttrs().add(attrTO(path.getKey(), "/home"));

        SyncopeClient belliniClient = clientFactory.create("bellini", ADMIN_PWD);
        try {
            belliniClient.getService(AnyObjectService.class).create(folder);
            fail("This should not happen");
        } catch (SyncopeClientException e) {
            assertEquals(ClientExceptionType.DelegatedAdministration, e.getType());
        }

        // 4. give create entitlement for the any type just created
        RoleTO role = new RoleTO();
        role.setKey("role" + getUUIDString());
        role.getRealms().add(SyncopeConstants.ROOT_REALM);
        role.getEntitlements().add(anyTypeKey + "_READ");
        role.getEntitlements().add(anyTypeKey + "_CREATE");
        role = createRole(role);

        UserTO bellini = userService.read("bellini");
        UserPatch patch = new UserPatch();
        patch.setKey(bellini.getKey());
        patch.getRoles().add(new StringPatchItem.Builder().
                operation(PatchOperation.ADD_REPLACE).value(role.getKey()).build());
        bellini = updateUser(patch).getEntity();
        assertTrue(bellini.getRoles().contains(role.getKey()));

        // 5. now the instance of the type above can be created successfully
        belliniClient.logout();
        belliniClient.login(new BasicAuthenticationHandler("bellini", ADMIN_PWD));
        belliniClient.getService(AnyObjectService.class).create(folder);
    }

    @Test
    public void asGroupOwner() {
        // 0. prepare
        UserTO owner = UserITCase.getUniqueSampleTO("owner@syncope.org");
        owner = createUser(owner).getEntity();
        assertNotNull(owner);

        GroupTO group = GroupITCase.getSampleTO("forgroupownership");
        group.setUserOwner(owner.getKey());
        group = createGroup(group).getEntity();
        assertNotNull(group);
        assertEquals(owner.getKey(), group.getUserOwner());

        UserTO member = UserITCase.getUniqueSampleTO("forgroupownership@syncope.org");
        member.getMemberships().add(new MembershipTO.Builder().group(group.getKey()).build());
        member.getMemberships().add(new MembershipTO.Builder().group("37d15e4c-cdc1-460b-a591-8505c8133806").build());
        member = createUser(member).getEntity();
        assertEquals(2, member.getMemberships().size());
        String memberKey = member.getKey();

        if (ElasticsearchDetector.isElasticSearchEnabled(syncopeService)) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException ex) {
                // ignore
            }
        }

        PagedResult<UserTO> matching = userService.search(
                new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).
                        fiql(SyncopeClient.getUserSearchConditionBuilder().inGroups(group.getKey()).query()).
                        page(1).size(1000).build());
        int fullMatchSize = matching.getResult().size();
        assertTrue(matching.getResult().stream().anyMatch(user -> memberKey.equals(user.getKey())));

        UserService groupOwnerService = clientFactory.create(owner.getUsername(), "password123").
                getService(UserService.class);

        // 1. search
        matching = groupOwnerService.search(
                new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).
                        fiql(SyncopeClient.getUserSearchConditionBuilder().isNotNull("key").query()).
                        page(1).size(1000).build());
        assertEquals(fullMatchSize, matching.getResult().size());
        assertTrue(matching.getResult().stream().anyMatch(user -> memberKey.equals(user.getKey())));

        // 2. update and read
        UserPatch memberPatch = new UserPatch();
        memberPatch.setKey(memberKey);
        memberPatch.setUsername(new StringReplacePatchItem.Builder().value("new" + getUUIDString()).build());

        Response response = groupOwnerService.update(memberPatch);
        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());

        member = groupOwnerService.read(memberKey);
        assertEquals(memberPatch.getUsername().getValue(), member.getUsername());
        assertEquals(2, member.getMemberships().size());

        // 3. update with membership removal -> fail
        memberPatch.setUsername(null);
        memberPatch.getMemberships().add(new MembershipPatch.Builder().
                operation(PatchOperation.DELETE).group(group.getKey()).build());
        try {
            groupOwnerService.update(memberPatch);
            fail();
        } catch (SyncopeClientException e) {
            assertEquals(ClientExceptionType.DelegatedAdministration, e.getType());
        }

        // 4. update non-member -> fail
        UserTO nonmember = createUser(UserITCase.getUniqueSampleTO("nonmember@syncope.org")).getEntity();
        UserPatch nonmemberPatch = new UserPatch();
        nonmemberPatch.setKey(nonmember.getKey());
        nonmemberPatch.setUsername(new StringReplacePatchItem.Builder().value("new" + getUUIDString()).build());
        try {
            groupOwnerService.update(nonmemberPatch);
            fail();
        } catch (SyncopeClientException e) {
            assertEquals(ClientExceptionType.DelegatedAdministration, e.getType());
        }

        // 5. update user under /even
        member = UserITCase.getUniqueSampleTO("forgroupownership2@syncope.org");
        member.setRealm("/even");
        member.getMemberships().add(new MembershipTO.Builder().group(group.getKey()).build());
        member = createUser(member).getEntity();

        memberPatch = new UserPatch();
        memberPatch.setKey(member.getKey());
        memberPatch.setUsername(new StringReplacePatchItem.Builder().value("new" + getUUIDString()).build());
        response = groupOwnerService.update(memberPatch);
        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());

        // 6 delete
        groupOwnerService.delete(memberKey);
        try {
            userService.read(memberKey);
            fail();
        } catch (SyncopeClientException e) {
            assertEquals(ClientExceptionType.NotFound, e.getType());
        }
    }

    @Test
    public void issueSYNCOPE434() {
        assumeFalse(clientFactory.getContentType() == SyncopeClientFactoryBean.ContentType.XML);
        assumeTrue(FlowableDetector.isFlowableEnabledForUserWorkflow(syncopeService));

        // 1. create user with group 'groupForWorkflowApproval' 
        // (users with group groupForWorkflowApproval are defined in workflow as subject to approval)
        UserTO userTO = UserITCase.getUniqueSampleTO("createWithReject@syncope.apache.org");
        userTO.getMemberships().add(
                new MembershipTO.Builder().group("0cbcabd2-4410-4b6b-8f05-a052b451d18f").build());

        userTO = createUser(userTO).getEntity();
        assertNotNull(userTO);
        assertEquals("createApproval", userTO.getStatus());

        // 2. try to authenticate: fail
        try {
            clientFactory.create(userTO.getUsername(), "password123").self();
            fail("This should not happen");
        } catch (AccessControlException e) {
            assertNotNull(e);
        }

        // 3. approve user
        UserRequestForm form = userRequestService.listForms(
                new UserRequestQuery.Builder().user(userTO.getKey()).build()).getResult().get(0);
        form = userRequestService.claimForm(form.getTaskId());
        form.getProperty("approveCreate").get().setValue(Boolean.TRUE.toString());
        userTO = userRequestService.submitForm(form).readEntity(new GenericType<ProvisioningResult<UserTO>>() {
        }).getEntity();
        assertNotNull(userTO);
        assertEquals("active", userTO.getStatus());

        // 4. try to authenticate again: success
        Triple<Map<String, Set<String>>, List<String>, UserTO> self =
                clientFactory.create(userTO.getUsername(), "password123").self();
        assertNotNull(self);
        assertNotNull(self.getLeft());
        assertEquals(Collections.emptyList(), self.getMiddle());
        assertNotNull(self.getRight());
    }

    @Test
    public void issueSYNCOPE164() throws Exception {
        // 1. create user with db resource
        UserTO user = UserITCase.getUniqueSampleTO("syncope164@syncope.apache.org");
        user.setRealm("/even/two");
        user.setPassword("password123");
        user.getResources().add(RESOURCE_NAME_TESTDB);
        user = createUser(user).getEntity();
        assertNotNull(user);

        // 2. unlink the resource from the created user
        DeassociationPatch deassociationPatch = new DeassociationPatch.Builder().key(user.getKey()).
                action(ResourceDeassociationAction.UNLINK).resource(RESOURCE_NAME_TESTDB).build();
        assertNotNull(parseBatchResponse(userService.deassociate(deassociationPatch)));

        // 3. change password on Syncope
        UserPatch userPatch = new UserPatch();
        userPatch.setKey(user.getKey());
        userPatch.setPassword(new PasswordPatch.Builder().value("password234").build());
        user = updateUser(userPatch).getEntity();
        assertNotNull(user);

        // 4. check that the db resource has still the initial password value
        JdbcTemplate jdbcTemplate = new JdbcTemplate(testDataSource);
        String value = queryForObject(jdbcTemplate, MAX_WAIT_SECONDS,
                "SELECT PASSWORD FROM test WHERE ID=?", String.class, user.getUsername());
        assertEquals(Encryptor.getInstance().encode("password123", CipherAlgorithm.SHA1), value.toUpperCase());

        // 5. successfully authenticate with old (on db resource) and new (on internal storage) password values
        Triple<Map<String, Set<String>>, List<String>, UserTO> self =
                clientFactory.create(user.getUsername(), "password123").self();
        assertNotNull(self);
        self = clientFactory.create(user.getUsername(), "password234").self();
        assertNotNull(self);
    }

    @Test
    public void issueSYNCOPE706() {
        String username = getUUIDString();
        try {
            userService.read(username);
            fail("This should not happen");
        } catch (SyncopeClientException e) {
            assertEquals(ClientExceptionType.NotFound, e.getType());
        }

        try {
            clientFactory.create(username, "anypassword").self();
            fail("This should not happen");
        } catch (AccessControlException e) {
            assertNotNull(e.getMessage());
        }
    }
}
