/*
 *   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
 *
 *     https://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.directory.fortress.core.impl;


import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.directory.fortress.core.GlobalErrIds;
import org.apache.directory.fortress.core.GlobalIds;
import org.apache.directory.fortress.core.PasswordException;
import org.apache.directory.fortress.core.SecurityException;
import org.apache.directory.fortress.core.ValidationException;
import org.apache.directory.fortress.core.model.AdminRole;
import org.apache.directory.fortress.core.model.Administrator;
import org.apache.directory.fortress.core.model.ConstraintUtil;
import org.apache.directory.fortress.core.model.ObjectFactory;
import org.apache.directory.fortress.core.model.OrgUnit;
import org.apache.directory.fortress.core.model.PwPolicy;
import org.apache.directory.fortress.core.model.Role;
import org.apache.directory.fortress.core.model.RoleConstraint;
import org.apache.directory.fortress.core.model.RoleConstraint.RCType;
import org.apache.directory.fortress.core.model.Session;
import org.apache.directory.fortress.core.model.User;
import org.apache.directory.fortress.core.model.UserAdminRole;
import org.apache.directory.fortress.core.model.UserRole;
import org.apache.directory.fortress.core.util.Config;
import org.apache.directory.fortress.core.util.VUtil;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Process module for the User entity.  This class performs data validations and error mapping.  It is typically called
 * by internal Fortress manager classes ({@link AdminMgrImpl}, {@link AccessMgrImpl},
 * {@link ReviewMgrImpl}, ...) and not intended for external non-Fortress clients.  This class will accept,
 * {@link org.apache.directory.fortress.core.model.User}, validate its contents and forward on to it's corresponding DAO class {@link org.apache.directory.fortress.core.impl.UserDAO}.
 * <p>
 * Class will throw {@link SecurityException} to caller in the event of security policy, data constraint violation or system
 * error internal to DAO object. This class will forward DAO exceptions ({@link org.apache.directory.fortress.core.FinderException},
 * {@link org.apache.directory.fortress.core.CreateException},{@link org.apache.directory.fortress.core.UpdateException},{@link org.apache.directory.fortress.core.RemoveException}),
 *  or {@link org.apache.directory.fortress.core.ValidationException} as {@link SecurityException}s with appropriate
 * error id from {@link org.apache.directory.fortress.core.GlobalErrIds}.
 * <p>
 * This class is thread safe.
 * <p>
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
final class UserP
{
    private static final String CLS_NM = UserP.class.getName();
    private static final Logger LOG = LoggerFactory.getLogger( CLS_NM );
    private UserDAO uDao = new UserDAO();
    private PolicyP policyP = new PolicyP();
    private AdminRoleP admRoleP = new AdminRoleP();
    private OrgUnitP orgUnitP = new OrgUnitP();

    /**
     * Takes a User entity that contains full or partial userId OR a full internal userId for search.
     *
     * @param user contains all or partial userId or full internal userId.
     * @return List of type User containing fully populated matching User entities.  If no records found this will be empty.
     * @throws SecurityException in the event of DAO search error.
     */
    List<User> search( User user ) throws SecurityException
    {
        return uDao.findUsers( user );
    }


    List<User> search( OrgUnit ou, boolean limitSize ) throws SecurityException
    {
        return uDao.findUsers( ou, limitSize );
    }


    /**
     * Search according to full or partial search string that maps to Fortress userid.
     * This search is used by RealmMgr for Websphere.
     *
     * @param user contains full or partial userId.
     * @param limit     specify the max number of records to return in result set.
     * @return List of type String containing userId of all matching User entities. If no records found this will be empty.
     * @throws SecurityException in the event of DAO search error.
     */
    List<String> search( User user, int limit ) throws SecurityException
    {
        return uDao.findUsers( user, limit );
    }


    /**
     * Return a list of Users that are authorized the given Role.
     *
     * @param role contains the role name targeted for search.
     * @return List of type User containing fully populated matching User entities. If no records found this will be empty.
     * @throws SecurityException in the event of DAO search error.
     */
    List<User> getAuthorizedUsers( Role role ) throws SecurityException
    {
        return uDao.getAuthorizedUsers( role );
    }


    /**
     * Return a list of Users that are authorized the given Role.
     *
     * @param roles contains the set of role names targeted for search.
     * @param contextId maps to sub-tree in DIT, e.g. ou=contextId, dc=example, dc=com.
     * @return Set of type String containing the userId's for matching User entities. If no records found this will be empty.
     * @throws SecurityException in the event of DAO search error.
     */
    Set<String> getAssignedUsers( Set<String> roles, String contextId ) throws SecurityException
    {
        return uDao.getAssignedUsers( roles, contextId );
    }


    /**
     * Return a list of Users that are authorized the given Role.
     * In RBAC the word "authorized" implies the hierarchical role relations graph is considered in result set.
     * This search is used by RealmMgr for Websphere.
     *
     * @param role
     * @param limit specify the max number of records to return in result set.
     * @return list of type String of userIds. If no records found this will be empty.
     * @throws SecurityException in the event of DAO search error.
     */
    List<String> getAuthorizedUsers( Role role, int limit ) throws SecurityException
    {
        return uDao.getAuthorizedUsers( role, limit );
    }


    /**
     * Return a list of Users assigned the given RBAC role.
     * "Assigned" implies the hierarchical role relation graph will NOT be considered in result set.
     *
     * @param role contains name of RBAC role used for search.
     * @return List of fully populated User entities matching target search. If no records found this will be empty.
     * @throws SecurityException in the event of DAO search error.
     */
    List<User> getAssignedUsers( Role role ) throws SecurityException
    {
        return uDao.getAssignedUsers( role, null );
    }

    /**
     * Return a list of Users assigned the given RBAC role.
     * "Assigned" implies the hierarchical role relation graph will NOT be considered in result set.
     *
     * @param role contains name of RBAC role used for search.
     * @param roleConstraint filter roles that have this role constraint
     * @return List of fully populated User entities matching target search. If no records found this will be empty.
     * @throws SecurityException in the event of DAO search error.
     */
    List<User> getAssignedUsers( Role role, RoleConstraint roleConstraint ) throws SecurityException
    {
        return uDao.getAssignedUsers( role, roleConstraint );
    }
    
    /**
     * Return a list of user roles for the provided role name, role constraint type and pa set name
     *
     * @param role
     * @param rcType
     * @param paSetName
     * @return
     * @throws SecurityException
     */
    List<UserRole> getAssignedUsers( Role role, RCType rcType, String paSetName ) throws SecurityException
    {
        return uDao.getUserRoles( role, rcType, paSetName );
    }
    
    /**
     * Return a list of Users assigned the given RBAC role.
     * "Assigned" implies the hierarchical role relation graph will NOT be considered in result set.
     *
     * @param role contains name of RBAC role used for search.
     * @return List of fully populated User entities matching target search. If no records found this will be empty.
     * @throws SecurityException in the event of DAO search error.
     */
    List<String> getAssignedUserIds( Role role ) throws SecurityException
    {
        return uDao.getAssignedUserIds( role );
    }


    /**
     * Return a list of Users assigned the given Administrative role.
     * "Assigned" implies the hierarchical role relation graph will NOT be considered in result set.
     *
     * @param role contains name of Admin role used for search.
     * @return List of fully populated User entities matching target search.  If no records found this will be empty.
     * @throws SecurityException in the event of DAO search error.
     */
    List<User> getAssignedUsers( AdminRole role ) throws SecurityException
    {
        return uDao.getAssignedUsers( role );
    }


    /**
     * Return the list of User's RBAC roles.
     *
     * @param user contains full userId for target operation.
     * @return List of type String containing RBAC role names.  If no records found this will be empty.
     * @throws SecurityException in the event of DAO search error.
     */
    List<String> getAssignedRoles( User user ) throws SecurityException
    {
        return uDao.getRoles( user );
    }


    /**
     * Return a fully populated User entity for a given userId.  If the User entry is not found a SecurityException
     * will be thrown.
     *
     * @param user  contains full userId value.
     * @param isRoles return user's assigned roles if "true".
     * @return User entity containing all attributes associated with User in directory.
     * @throws SecurityException in the event of User not found or DAO search error.
     */
    User read( User user, boolean isRoles ) throws SecurityException
    {
        return uDao.getUser( user, isRoles );
    }


    /**
     * Adds a new User entity to directory.  The User entity input object will be validated to ensure that:
     * userId is present, orgUnitId is valid, roles (optiona) are valid, reasonability checks on all of the
     * other populated values.
     *
     * @param entity User entity contains data targeted for insertion.
     * @return User entity copy of input + additional attributes (internalId) that were added by op.
     * @throws SecurityException in the event of data validation or DAO system error.
     */
    User add( User entity ) throws SecurityException
    {
        return add( entity, true );
    }


    /**
     * Adds a new User entity to directory.
     * The User entity input object will be validated to ensure that: userId is present, orgUnitId is valid,
     * roles (optiona) are valid, reasonability checks on all of the other populated values.
     *
     * @param entity   User entity contains data targeted for insertion.
     * @param validate if false will skip the validations described above.
     * @return User entity copy of input + additional attributes (internalId)
     * @throws SecurityException in the event of data validation or DAO system error.
     */
    User add( User entity, boolean validate ) throws SecurityException
    {
        if ( validate )
        {
            // Ensure the input data is valid.
            validate( entity, false );
        }

        entity = uDao.create( entity );

        return entity;
    }


    /**
     * Update existing user's attributes with the input entity.  Null or empty attributes will be ignored.
     * This method will ignore userId as input as change userId is not allowed.  If password is changed
     * OpenLDAP password policy will not be evaluated on behalf of the user.
     * Other User entity input data can be changed and will also be validated beforehand to ensure that:
     * orgUnitId is valid, roles (optional) are valid, reasonability checks will be performed on all of the populated fields.
     *
     * @param entity User entity contains data targeted for insertion.
     * @return User entity copy of input
     * @throws SecurityException in the event of data validation or DAO system error.
     */
    User update( User entity ) throws SecurityException
    {
        return update( entity, true );
    }


    /**
     * Update existing user's attributes with the input entity.  Null or empty attributes will be ignored.
     * This method will ignore userId or password as input.  The former is not allowed and latter is performed by other
     * methods in this class.
     * Other User entity input data can be changed and will also be validated beforehand to ensure that:
     * orgUnitId is valid, roles (optional) are valid, reasonability checks will be performed on all of the populated fields.
     *
     * @param entity   User entity contains data targeted for insertion.
     * @param validate if false will skip the validations described above.
     * @return User entity copy of input
     * @throws SecurityException in the event of data validation or DAO system error.
     */
    /**
     * Update existing user's attributes with the input entity.  Null or empty attributes will be ignored.
     * This method will ignore userId or password as input.  The former is not allowed and latter is performed by other
     * methods in this class.
     * Other User entity input data can be changed and will also be validated beforehand to ensure that:
     * orgUnitId is valid, roles (optional) are valid, reasonability checks will be performed on all of the populated fields.
     *
     * @param entity   User entity contains data targeted for insertion.
     * @param validate if false will skip the validations described above.
     * @return User entity copy of input
     * @throws SecurityException in the event of data validation or DAO system error.
     */
    User update( User entity, boolean validate ) throws SecurityException
    {
        if ( validate )
        {
            // Ensure the input data is valid.
            validate( entity, true );
        }
        entity = uDao.update( entity );
        return entity;
    }


    /**
     * Method performs a "soft" delete.  It disables User entity and flags as "deleted".  User must exist in directory
     * prior to making this call.
     *
     * @param user Contains the userId of the user targeted for deletion.
     * @return String contains user DN
     * @throws SecurityException in the event of data validation or DAO system error.
     */
    String softDelete( User user ) throws SecurityException
    {
        // Ensure this user isn't listed in Fortress config as a system user that can't be removed via API.
        // Is there a match between this userId and a Fortress system user?
        User checkUser = read( user, true );
        if ( checkUser.isSystem() != null && checkUser.isSystem() )
        {
            String warning = "softDelete userId [" + user.getUserId()
                + "] can't be removed due to policy violation, rc=" + GlobalErrIds.USER_PLCY_VIOLATION;
            throw new SecurityException( GlobalErrIds.USER_PLCY_VIOLATION, warning );
        }
        user.setDescription( "DELETED" );
        User outUser = uDao.update( user );
        return outUser.getDn();
    }


    /**
     * This method performs a "hard" delete.  It completely removes all data associated with this user from the directory.
     * User entity must exist in directory prior to making this call else exception will be thrown.
     *
     * @param user Contains the userid of the user targeted for deletion.
     * @return String contains user DN
     * @throws SecurityException in the event of data validation or DAO system error.
     */
    String delete( User user ) throws SecurityException
    {
        // Ensure this user isn't listed in Fortress config as a system user that can't be removed via API.
        // Is there a match between this userId and a Fortress system user?
        User checkUser = read( user, true );
        if ( checkUser.isSystem() != null && checkUser.isSystem() )
        {
            String warning = "delete userId [" + user.getUserId()
                + "] can't be removed due to policy violation, rc=" + GlobalErrIds.USER_PLCY_VIOLATION;
            throw new SecurityException( GlobalErrIds.USER_PLCY_VIOLATION, warning );
        }
        return uDao.remove( user );
    }


    /**
     * Removes the user's association from OpenLDAP password policy.  Once this association is removed, the User
     * password policy will default to that which is default for ldap server.
     *
     * @param user contains the userId for target user.
     * @throws SecurityException in the event of DAO error.
     */
    void deletePwPolicy( User user ) throws SecurityException
    {
        uDao.deletePwPolicy( user );
    }


    /**
     * This method performs authentication only.  It does not activate RBAC roles in session.  It will evaluate
     * password policies.
     *
     * @param user  Contains the userid of the user signing on along with password.
     * @return Session object will be returned if authentication successful.  This will not contain user's roles.
     * @throws SecurityException in the event of data validation failure, security policy violation or DAO error.
     */
    Session authenticate( User user ) throws SecurityException
    {
        Session session;
        session = uDao.checkPassword( user );

        if ( !session.isAuthenticated() )
        {
            String info = "UserP.authenticate failed  for userId [" + user.getUserId() + "] reason code ["
                + session.getErrorId() + "] msg [" + session.getMsg() + "]";
            throw new PasswordException( session.getErrorId(), info );
        }

        VUtil.getInstance().validateConstraints( session, VUtil.ConstraintType.USER, false );

        return session;
    }


    /**
     * CreateSession
     * <p>
     * This method is called by AccessMgr and is not intended for use outside Fortress core.  The successful
     * result is Session object that contains target user's RBAC and Admin role activations.  In addition to checking
     * user password validity it will apply configured password policy checks.  Method may also store parms passed in for
     * audit trail..
     * <ul>
     * <li> authenticate user password
     * <li> password policy evaluation with OpenLDAP PwPolicy
     * <li> evaluate temporal constraints on User and UserRole entities.
     * <li> allow selective role activations into User RBAC Session.
     * <li> require valid password if trusted == false.
     * <li> will disallow any user who is locked out due to OpenLDAP pw policy, regardless of trusted flag being set as parm on API.
     * <li> return User's RBAC Session containing User and UserRole attributes.
     * <li> throw a SecurityException for authentication failures, other policy violations, data validation errors or system failure.
     * </ul>
     * <p>
     * <p>
     * The function is valid if and only if:
     * <ul>
     * <li> the user is a member of the USERS data set
     * <li> the password is supplied (unless trusted).
     * <li> the (optional) active role set is a subset of the roles authorized for that user.
     * </ul>
     * <p>
     * <p>
     * The User parm contains the following (* indicates required)
     * <ul>
     * <li> String userId*
     * <li> char[] password
     * <li> List<UserRole> userRoles contains a list of RBAC role names authorized for user and targeted for activation within this session.
     * <li> List<UserAdminRole> userAdminRoles contains a list of Admin role names authorized for user and targeted for activation.
     * <li> Properties logonProps collection of auditable name/value pairs to store.  For example hostname:myservername or ip:192.168.1.99
     * </ul>
     * <p>
     * <p>
     * Notes:
     * <ul>
     * <li> roles that violate Dynamic Separation of Duty Relationships will not be activated into session.
     * <li> role activations will proceed in same order as supplied to User entity setter.
     * </ul>
     * <p>
     *
     * @param user    Contains userId, password (optional if "trusted"), optional User RBAC Roles: List<UserRole> rolesToBeActivated., optional User Admin Roles: List<UserAdminRole> adminRolesToBeActivated.
     * @param trusted if true password is not required.
     * @return Session object will contain authentication result code, RBAC and Admin role activations, OpenLDAP pw policy output and more.
     * @throws SecurityException in the event of data validation failure, security policy violation or DAO error.
     */
    Session createSession( User user, boolean trusted ) throws SecurityException
    {
        Session session;
        if ( trusted )
        {
            // Create the impl session without authentication of password.
            session = createSessionTrusted( user );
            // Check user temporal constraints.  This op usually performed during authentication.
            VUtil.getInstance().validateConstraints( session, VUtil.ConstraintType.USER, false );
        }
        else
        {
            // Create the impl session if the user authentication succeeds:
            VUtil.assertNotNullOrEmpty( user.getPassword(), GlobalErrIds.USER_PW_NULL, CLS_NM + ".createSession" );
            session = createSession( user );
        }
        // Normally, the context (tenant) gets set in the mgr layer and passed into here, as in the User.
        // However, the Session was created down here and must be set here as well, for role constraint (validation) to be multitenant, in validateConstraints method:
        session.setContextId( user.getContextId() );

        // Did the caller pass in a set of roles for selective activation?
        if ( CollectionUtils.isNotEmpty( user.getRoles() ) )
        {
            // Process selective activation of user's RBAC roles into session:
            List<UserRole> rlsActual = session.getRoles();
            List<UserRole> rlsFinal = new ArrayList<>();
            session.setRoles( rlsFinal );
            // Activate only the intersection between assigned and roles passed into this method:
            for ( UserRole role : user.getRoles() )
            {
                int indx = rlsActual.indexOf( role );
                if ( indx != -1 )
                {
                    UserRole candidateRole = rlsActual.get( indx );
                    rlsFinal.add( candidateRole );
                }
            }
        }
        // Did the caller pass in a set of dynamic constraints as properties?
        // TODO: Guard with a property? i.e. user.session.props.enabled
        if ( user.getProps() != null )
        {
            session.getUser().addProperties( user.getProperties() );
        }

        // Check role temporal constraints + activate roles:
        VUtil.getInstance().validateConstraints( session, VUtil.ConstraintType.ROLE, true );
        return session;
    }


    /**
     * This convenience/passthru method.  It loads a key,value pair of attributes, constained within the RoleConsraint, into the user properties, bound for the constraint validations during role activation step.
     * It saves a step or two for the user, who can simply pass constraint using value object, rather than properties.
     *
     * @param user same as before
     * @param constraints contains a list of attributes {@link RoleConstraint#key}, {@link RoleConstraint#value}, bound for role activation checks.
     * @param trusted same as always, true if no pw
     * @return session with activated roles, etc.
     * @throws SecurityException
     */
    Session createSession( User user, List<RoleConstraint> constraints, boolean trusted ) throws SecurityException
    {
        // Load the constraint key/value into a property bag, pass into runtime context via user entity.
        Properties props = new Properties(  );

        // Validate the constraint key/value fields are set:
        for( RoleConstraint constraint : constraints)
        {
            VUtil.assertNotNullOrEmpty( constraint.getKey(), GlobalErrIds.ROLE_CONSTRAINT_KEY_NULL, CLS_NM + ".createSession" );
            VUtil.assertNotNullOrEmpty( constraint.getValue(), GlobalErrIds.ROLE_CONSTRAINT_VALUE_NULL, CLS_NM + "" +
                ".createSession" );
            props.setProperty( constraint.getKey(), constraint.getValue() );
        }
        user.addProperties( props );
        return createSession( user, trusted );
    }


    /**
     * Called internal to this class only.  Will do all of the session activations of the public method
     * in addition to the password validation.
     *
     * @param inUser   Contains userId that represents rDn of node in ldap directory.
     * @return Session object will contain authentication result code, RBAC and Admin role activations, OpenLDAP pw policy output and more.
     * @throws SecurityException in the event of data validation failure, security policy violation or DAO error.
     */
    private Session createSession( User inUser )
        throws SecurityException
    {
        // read user entity:
        User user = read( inUser, true );
        user.setContextId( inUser.getContextId() );

        // authenticate password, check pw policies and validate user temporal constraints:
        Session session = authenticate( inUser );

        // Set the user entity into the session object:
        session.setUser( user );
        return session;
    }


    /**
     * Trusted session creation method called internal to this class only.  Will do all of the session activations of the public method
     *
     * @param inUser Contains userId that represents rDn of node in ldap directory.
     * @return Session object will contain authentication result code, RBAC and Admin role activations, OpenLDAP pw policy output and more.
     * @throws SecurityException in the event of data validation failure, security policy violation or DAO error.
     */
    private Session createSessionTrusted( User inUser )
        throws SecurityException
    {
        User user = read( inUser, true );
        user.setContextId( inUser.getContextId() );

        if ( user.isLocked() )
        {
            String warning = "createSession failed for userId [" + inUser.getUserId()
                + "] reason user is locked";
            LOG.warn( warning );
            throw new SecurityException( GlobalErrIds.USER_LOCKED_BY_CONST, warning );
        }

        Session session = new ObjectFactory().createSession();
        session.setUserId( inUser.getUserId() );
        // Set this flag to false because user's password was not authenticated.
        session.setAuthenticated( false );
        session.setUser( user );
        return session;
    }


    /**
     * Method will set the OpenLDAP pwlocked attribute which will lock user from being able to signon to the system.
     *
     * @param user Contains userId that represents rDn of node in ldap directory.
     * @throws SecurityException in the event of DAO error.
     */
    void lock( User user ) throws SecurityException
    {
        uDao.lock( user );
    }


    /**
     * Method will reset the OpenLDAP pwlocked attribute which will unlock user and allow to signon to the system.
     *
     * @param user Contains userId that represents rDn of node in ldap directory.
     * @throws SecurityException in the event of DAO  error.
     */
    void unlock( User user ) throws SecurityException
    {
        uDao.unlock( user );
    }


    /**
     * Method will change the user's password and validate user's pw policy in OpenLDAP.
     *
     * @param entity      contains userId and old password.
     * @param newPassword contains the new password which must pass the password policy constraints.
     * @throws SecurityException in the event of data validation failure, password policy violation or DAO error.
     */
    void changePassword( User entity, String newPassword ) throws SecurityException
    {
        String userId = entity.getUserId();
        boolean result = uDao.changePassword( entity, newPassword );
        if ( !result )
        {
            LOG.warn( "changePassword failed for user [{}]", userId );
        }
    }


    /**
     * Perform password reset on user entity.  This will change the User password and set the reset flag
     * in OpenLDAP will will force the user to change their password at next logon time.
     *
     * @param user contains the userId and the new password.
     * @throws SecurityException in the event of DAO error.
     */
    void resetPassword( User user ) throws SecurityException
    {
        uDao.resetUserPassword( user );
    }


    /**
     * This command assigns a user to a role.
     * <p>
     * <ul>
     * <li> The command is valid if and only if:
     * <li> The user is a member of the USERS data set
     * <li> The role is a member of the ROLES data set
     * <li> The user is not already assigned to the role
     * <li> The SSD constraints are satisfied after assignment.
     * </ul>
     * <p>
     * <p>
     * Successful completion of this op, the following occurs:
     * <p>
     * <ul>
     * <li> User entity (resides in people container) has role assignment added to aux object class attached to actual user record.
     * <li> Role entity (resides in role container) has userId added as role occupant.
     * <li> (optional) Temporal constraints may be associated with <code>ftUserAttrs</code> aux object class based on:
     * <ul>
     * <li> timeout - number (in minutes) of session inactivity time allowed.
     * <li> beginDate - YYYYMMDD - determines date when role may be activated.
     * <li> endDate - YYMMDD - indicates latest date role may be activated.
     * <li> beginLockDate - YYYYMMDD - determines beginning of enforced inactive status
     * <li> endLockDate - YYMMDD - determines end of enforced inactive status.
     * <li> beginTime - HHMM - determines begin hour role may be activated in user's session.
     * <li> endTime - HHMM - determines end hour role may be activated in user's session.*
     * <li> dayMask - 1234567, 1 = Sunday, 2 = Monday, etc - specifies which day of week role may be activated.
     * </ul>
     * </ul>
     *
     * @param uRole entity contains userId and role name for targeted assignment.
     * @return String containing the user's DN.  This value is used to update the "roleOccupant" attribute on associated role entity.
     * @throws SecurityException in the event data error in user or role objects or system error.
     */
    String assign( UserRole uRole ) throws SecurityException
    {
        validate( uRole );
        // "assign" custom Fortress role data, i.e. temporal constraints, onto the user node:
        return uDao.assign( uRole );
    }
    
    //TODO: add documentation
    void assign( UserRole uRole, RoleConstraint roleConstraint ) throws SecurityException
    {        
        validate( roleConstraint, uRole.getContextId() );
        
        uDao.assign( uRole, roleConstraint );
    }
    
    //TODO: add documentation, maybe change method name?
    void deassign( UserRole uRole, RoleConstraint roleConstraint ) throws SecurityException
    {
        //TODO: validate?
        
        uDao.deassign( uRole, roleConstraint );
    }


    /**
     * This command deletes the assignment of the User from the Role entities. The command is
     * valid if and only if the user is a member of the USERS data set, the role is a member of
     * the ROLES data set, and the user is assigned to the role.
     * Any sessions that currently have this role activated will not be effected.
     * Successful completion includes:
     * User entity in USER data set has role assignment removed.
     * Role entity in ROLE data set has userId removed as role occupant.
     * (optional) Temporal constraints will be removed from user aux object if set prior to call.
     *
     * @param uRole entity contains userId and RBAC Role name for targeted assignment.
     * @return String containing the user's DN.  This value is used to remove the "roleOccupant" attribute on associated RBAC Role entity.
     * @throws SecurityException - in the event data error in user or role objects or system error.
     */
    String deassign( UserRole uRole ) throws SecurityException
    {
        validate( uRole );
        // "deassign" custom Fortress role data from the user's node:
        return uDao.deassign( uRole );
    }


    /**
     * This command assigns a user to an admin role.
     * Successful completion of this op, the following occurs:
     * <p>
     * <ul>
     * <li> User entity (resides in people container) has role assignment added to aux object class attached to actual user record.
     * <li> AdminRole entity (resides in admin role container) has userId added as role occupant.
     * <li> (optional) Temporal constraints may be associated with <code>ftUserAttrs</code> aux object class based on:
     * <ul>
     * <li> timeout - number (in minutes) of session inactivity time allowed.
     * <li> beginDate - YYYYMMDD - determines date when role may be activated.
     * <li> endDate - YYMMDD - indicates latest date role may be activated.
     * <li> beginLockDate - YYYYMMDD - determines beginning of enforced inactive status
     * <li> endLockDate - YYMMDD - determines end of enforced inactive status.
     * <li> beginTime - HHMM - determines begin hour role may be activated in user's session.
     * <li> endTime - HHMM - determines end hour role may be activated in user's session.*
     * <li> dayMask - 1234567, 1 = Sunday, 2 = Monday, etc - specifies which day of week role may be activated.
     * </ul>
     * </ul>
     *
     * @param uRole entity contains userId and Admin Role name for targeted assignment.
     * @return String containing the user's DN.  This value is used to update the "roleOccupant" attribute on associated Admin Role entity.
     * @throws SecurityException in the event data error in user or role objects or system error.
     */
    String assign( UserAdminRole uRole ) throws SecurityException
    {
        validate( uRole );
        // Assign custom Fortress role data, i.e. temporal constraints, onto the user node:
        return uDao.assign( uRole );
    }


    /**
     * This method removes assigned admin role from user entity.  Both user and admin role entities must exist and have role relationship
     * before calling this method.
     * Successful completion:
     * del Role to User assignment in User data set
     * AND
     * User to Role assignment in Admin Role data set.
     *
     * @param uRole entity contains userId and Admin Role name for targeted assignment.
     * @return String containing the user's DN.  This value is used to remove the "roleOccupant" attribute on associated Admin Role entity.
     * @throws SecurityException - in the event data error in user or role objects or system error.
     */
    String deassign( UserAdminRole uRole ) throws SecurityException
    {
        validate( uRole );
        // Deassign custom Fortress role data from the user's node:
        return uDao.deassign( uRole );
    }


    /**
     * Ensure that the passed in variable has the correct fields set.
     *
     * @param uRole - name and userId must be checked.
     * @throws ValidationException - if either are null or empty.
     */
    private void validate( UserRole uRole ) throws ValidationException
    {
        if ( StringUtils.isEmpty( uRole.getUserId() ) )
        {
            throw new ValidationException( GlobalErrIds.USER_ID_NULL, CLS_NM + ".validate userId is NULL" );
        }
        if ( StringUtils.isEmpty( uRole.getName() ) )
        {
            throw new ValidationException( GlobalErrIds.ROLE_NM_NULL, CLS_NM + ".validate name is NULL" );
        }
    }
    
    /**
     * Ensure that the passed in role constraint is valid
     * 
     * @param rc RoleConstaint
     * @param contextId
     * @throws ValidationException
     */
    private void validate( RoleConstraint rc, String contextId ) throws ValidationException
    {
        // TODO: This should be for only one type:
        if( rc.getType() != RCType.USER)
        {
            if( StringUtils.isEmpty( rc.getKey() ))
            {
                throw new ValidationException( GlobalErrIds.PERM_ATTRIBUTE_SET_NM_NULL, CLS_NM + ".validate pa set name is NULL" );
            }
            try
            {
                PermP permP = new PermP();
                permP.validatePaSet( rc.getKey(), contextId );
            }
            catch( SecurityException e )
            {
                String error = "validate - paSetName not found with name [" + rc.getKey() + "] caught SecurityException=" + e;
                throw new ValidationException( GlobalErrIds.PERM_ATTRIBUTE_SET_NOT_FOUND, error );
            }
        }
        if ( rc.getType() == null )
        {
            throw new ValidationException( GlobalErrIds.ROLE_CONSTRAINT_TYPE_NULL, CLS_NM + ".validate type is NULL" );
        }
        if( StringUtils.isEmpty( rc.getValue() ))
        {
            throw new ValidationException( GlobalErrIds.ROLE_CONSTRAINT_VALUE_NULL, CLS_NM + ".validate value is NULL" );
        }
    }


    /**
     * Method will perform various validations to ensure the integrity of the User entity targeted for insertion
     * or updating in directory.  For example the ou attribute will be "read" from the OrgUnit dataset to ensure
     * that it is valid.  Data reasonability checks will be performed on all non-null attributes.
     * This method will also copy the source constraints to target entity iff the target input entity does not have set
     * prior to calling.
     *
     * @param entity   User entity contains data targeted for insertion or update.  The input role constraints will be accepted.
     * @param isUpdate if true update operation is being performed which specifies a different set of targeted attributes.
     * @throws SecurityException in the event of data validation error or DAO error on Org validation.
     */
    private void validate( User entity, boolean isUpdate )
        throws SecurityException
    {
        if ( !isUpdate )
        {
            // the UserId attribute is required on User:
            VUtil.userId( entity.getUserId() );

            // the cn attribute is optional as input.  entity will default to userId if cn not set by caller on add:
            if ( StringUtils.isNotEmpty( entity.getCn() ) )
            {
                VUtil.safeText( entity.getCn(), GlobalIds.CN_LEN );
            }
            // the sn attribute is optional as input.  entity will default to userId if sn not set by caller on add:
            if ( StringUtils.isNotEmpty( entity.getSn() ) )
            {
                VUtil.safeText( entity.getSn(), GlobalIds.SN_LEN );
            }
            // password is not required on user object but user cannot execute AccessMgr or DelAccessMgr methods w/out pw.
            if ( StringUtils.isNotEmpty( entity.getPassword() ) )
            {
                VUtil.safeText( entity.getPassword(), GlobalIds.PASSWORD_LEN );
            }
            // the OU attribute is required:
            if ( StringUtils.isEmpty( entity.getOu() ) )
            {
                String error = "OU validation failed, null or empty value";
                throw new ValidationException( GlobalErrIds.ORG_NULL_USER, error );
            }
            VUtil.orgUnit( entity.getOu() );
            // ensure ou exists in the OS-U pool:
            OrgUnit ou = new OrgUnit( entity.getOu(), OrgUnit.Type.USER );
            ou.setContextId( entity.getContextId() );
            if ( !orgUnitP.isValid( ou ) )
            {
                String error = "validate detected invalid orgUnit name [" + entity.getOu()
                    + "] adding user with userId [" + entity.getUserId() + "]";
                throw new ValidationException( GlobalErrIds.USER_OU_INVALID, error );
            }
            // description attribute is optional:
            if ( StringUtils.isNotEmpty( entity.getDescription() ) )
            {
                VUtil.description( entity.getDescription() );
            }
        }
        else
        {
            // on User update, all attributes are optional:
            if ( StringUtils.isNotEmpty( entity.getCn() ) )
            {
                VUtil.safeText( entity.getCn(), GlobalIds.CN_LEN );
            }
            if ( StringUtils.isNotEmpty( entity.getSn() ) )
            {
                VUtil.safeText( entity.getSn(), GlobalIds.SN_LEN );
            }
            if ( StringUtils.isNotEmpty( entity.getPassword() ) )
            {
                VUtil.safeText( entity.getPassword(), GlobalIds.PASSWORD_LEN );
            }
            if ( StringUtils.isNotEmpty( entity.getOu() ) )
            {
                VUtil.orgUnit( entity.getOu() );
                // ensure ou exists in the OS-U pool:
                OrgUnit ou = new OrgUnit( entity.getOu(), OrgUnit.Type.USER );
                ou.setContextId( entity.getContextId() );
                if ( !orgUnitP.isValid( ou ) )
                {
                    String error = "validate detected invalid orgUnit name [" + entity.getOu()
                        + "] updating user wth userId [" + entity.getUserId() + "]";
                    throw new ValidationException( GlobalErrIds.USER_OU_INVALID, error );
                }
            }
            if ( StringUtils.isNotEmpty( entity.getDescription() ) )
            {
                VUtil.description( entity.getDescription() );
            }
        }

        // password policy name must be valid if set:
        if ( StringUtils.isNotEmpty( entity.getPwPolicy() ) && ( Config.getInstance().isOpenldap() || Config.getInstance().isApacheds() ) )
        {
            PwPolicy policy = new PwPolicy( entity.getPwPolicy() );
            policy.setContextId( entity.getContextId() );
            if ( !policyP.isValid( policy ) )
            {
                String error = "validate detected invalid OpenLDAP policy name [" + entity.getPwPolicy()
                    + "] for userId [" + entity.getUserId()
                    + "]. Assignment is optional for User but must be valid if specified.";
                throw new ValidationException( GlobalErrIds.USER_PW_PLCY_INVALID, error );
            }
        }

        // 2 Validate constraints on User object:
        ConstraintUtil.validate( entity );
    }

    /**
     * Perform copy on ARBAC attributes.  This is used during assignment of {@link org.apache.directory.fortress.core.model.AdminRole} to {@link org.apache.directory.fortress.core.model.User}.
     * This method does not perform input validations.
     *
     * @param srcR contains source attributes to copy.
     * @param trgR contains the target reference.
     */
    void copyAdminAttrs(Administrator srcR, Administrator trgR)
    {
        trgR.setBeginInclusive(srcR.isBeginInclusive());
        trgR.setEndInclusive(srcR.isEndInclusive());
        trgR.setBeginRange(srcR.getBeginRange());
        trgR.setEndRange(srcR.getEndRange());
        // copy the user and perm pools:
        trgR.setOsPSet( srcR.getOsPSet() );
        trgR.setOsUSet( srcR.getOsUSet() );
    }
    
    List<RoleConstraint> findRoleConstraints( Set<String> roles, User user, RoleConstraint.RCType rcType, Set<String> paSets ) throws SecurityException
    {
        List<RoleConstraint> matchingConstraints = new ArrayList<RoleConstraint>();
        
        //TODO: can we do this in a query?
        List<UserRole> userRoles = uDao.getUser(user, true).getRoles();
        for(UserRole ur : userRoles){
            //only get constraints for passed in roles
            if(roles.contains(ur.getName()))
            {
                for(RoleConstraint rc : ur.getRoleConstraints())
                {
                    if(rc.getType().equals(rcType) && paSets.contains(rc.getKey()))
                    {
                        matchingConstraints.add(rc);
                    }
                }
            }
        }
        
        return matchingConstraints;
    }
}
