Disabled user

Version 22

    The feature allows an administrator to temporarily revoke the usage of an account to a user.

     

    The requirements:

     

    • When a disabled user attempts to login with their correct credentials
      • The authentication must fail
      • A user friendly message should be presented to him: this is a best effort and sometimes it may not be possible to present him the message
    • A disabled user must not receive anymore email messages: the message is lost and will not be resent later
    • An attempt to reset the credentials of a disabled user will fail
    • The organization service API
      • must be augmented for enabling and disabling an user
      • queries returning users must be filtered
        • the behavior of the existing method changes to remove the disabled users
        • an overloaded query method is added with a UserStatus argument to control the filtering of the query
      • must sent a new type of event upon enabled status changes
    • From the user interface perspective disabled users
      • must not be listed in user selector components
      • must appear in the community management portlet and admin can filter to view only enabled users, disabled users or all users.

     

    The implementation in eXo Core (COR-293, COR-320 and JCR-2115):

    • Add new emum UserStatus with three constants {ENABLED, DISABLED, ANY}
    • UserHandler changes:
      • new method added for enabling or disabling an user
      • finders are overloaded with a new UserStatus parameter to filter disabled users
      • original finders filters by default (i.e they call the new finders and only display enabled users)
      • method that mutate state (like saveUser) will throw a DisabledUserException when a user is disabled
    • Authenticator change
      • added method to retrieve the exception during authentication called getLastExceptionOnValidateUser()
      • disabled user authentication will fail with a DisabledUserException

     

    Implementation constraints:

     

    • The implementation will be
      • achieved mostly in the GateIn project and no or little mofifications would be done in PicketLink
      • be based on attribute filtering
    • In case of a mixed backend (DB+LDAP) the results may display less accurate results (allowing repetition) for performance reason (this should be configurable)
    • The implementation will have a performance impact when filtering disabled users
    • The feature can be disabled at any time for users that don't want to pay the performance cost
    • Login phase should use an extra login module for filtering disabled users on the top of the JAAS stack in order to interact well with SSO and OAuth scenario

     

    Implementation notes

    UserDAOImpl implementation notes
    • Method setEnabled(String userName, boolean enabled, boolean broadcast) - Main needed thing is to change value of "enabled" attribute to requested value with usage of Picketlink IDM AttributesManager. Also listeners need to be triggered

    • Method authenticate(String username, String password) -Before authentication check, we can simply ask if user is disabled and throw DisabledUserException in that case. It seems that the call to method findUserByName(username) at the startup of method should be probably changed to findUserByName(username, UserStatus.BOTH) so that we can differ between the case when user doesn't exist at all (method should return false) or is disabled (method should throw DisabledUserException)

    • Finder methods for single user
      • User findUserByName(String userName) 
        This method is equivalent to findUserByName(userName, UserStatus.ENABLED)
      • User findUserByName(String userName, UserStatus status)
        Method could delegate the real logic to method getPopulatedUser(String userName, IdentitySession session, UserStatus status)

        This method could then filter the user after it's obtained from IDM and his attributes are populated. Method could filter users programmatically, so something like:

        return status.matches(user.isEnabled()) ? user : null;
        
        
        
        
        
        

     

      • Signature of UserDAOImpl methods findUserByUniqueAttribute and findUserByEmail could be also changed to have attribute UserStatus status and they could use same strategy like findUserByName (ie. filter users programatically after population and return null if user is disabled). It should be fine to change signature of these methods because those are only helper methods (not implementation of OrganizationService API methods)

     

    • Finder methods for multiple users
      • I would suggest to add the attribute into idm_configuration.xml into configuration of PicketlinkIDMOrganizationServiceImpl. The attribute could be something like
        <field name="filterDisabledUsersInQueries">
                <boolean>true</boolean>
        </field>
        
        
        
        
        
        

     

    It will be true by default, but customers could possibly switch it to "false", which means that queries will always return all users (including disabled users). Issue is that filtering of disabled users will have some performance impact. So people can switch to "false" if they are not interested in "Disabled user" functionality and their biggest priority is performance.

     

      • Constructor of class IDMUserListAccess could be changed to:
        public IDMUserListAccess(UserQueryBuilder userQueryBuilder, int pageSize, boolean countAll, UserStatus status)
        when particular finder methods could be changed to call IDMUserListAccess constructor with UserStatus.BOTH if they are called with option filterDisabledUsersInQueries is false.

        Example of possible usage in UserDAOImpl:
    
    public ListAccess<User> findUsersByQuery(Query q, UserStatus status) throws Exception {
      .....
            if(!filterDisabledUsersInQueries()) {
                status = UserStatus.BOTH;
            }
    
            if (q.getUserName() == null && q.getEmail() == null && q.getFirstName() == null && q.getLastName() == null) {
                list = new IDMUserListAccess(qb, 20, !countPaginatedUsers(), status);
            } else {
                list = new IDMUserListAccess(qb, 20, false, status);
            }
      .....
    }
    
    private boolean filterDisabledUsersInQueries() {
        return orgService.getConfiguration().isFilterDisabledUsersInQueries();
    }
    
    
    
    
    
    
    

     

      • There won't be any change in the semantics or functionality of existing methods in Picketlink IDM just because new GateIn "disabled user" feature. So for example Picketlink IDM method PersistenceManager.getUserCount() will still return count of all users, including those which are disabled in GateIn. Picketlink IDM provides Query API, so it is possible to add filtering for "enabled" attribute into query and return just users with enabled=='true' or enabled='false'

     


    NOTE: Picketlink IDM provides method "PersistenceManager.getUserCount()", which returns count of all users from Picketlink IDM. The issue is, that this method doesn't provide accurate results if we have setup with DB and LDAP and we have some users in both DB and LDAP. This method always return sum from DB count + LDAP count, but this is not accurate if there are some "overlapping" users.
    Example: We have 3 users john, mary, root in DB and 3 users john, alice, bob in LDAP. Method PersistenceManager.getUserCount() will return 3+3=6 users, but as we can see it should return only 5 users because user "john" is in both DB and LDAP.

     

    We handle this by providing option "countPaginatedUsers", so people can choose if they want better performance or always accurate results.

     

    If parameter countPaginatedUsers is true, then IDMUserListAccess doesn't rely on possibly inaccurate "PersistenceManager.getUserCount()" but it will try to always eagerly load all users from Picketlink IDM during first call to getSize() method. Those results are then cached in variable "fullResults" for later use (See IDMUserListAccess for better understanding)

     

     

      • It seems that new needed method in Picketlink IDM would be PersistenceManager.getUserCount(UserStatus status), which will allow to return just those users who have enabled=="true" (or enabled==null) when status = UserStatus.ENABLED and return just those users who have enabled=='false' when status = UserStatus.DISABLED. It's little hack to have method like this because Picketlink IDM is not aware of "disabled user" functionality, which is specific to GateIn. However it will be needed for performance reasons, so maybe we could do the exception in this case.
        Another possibility is to add method PersistenceManager.getUserCount(IdentitySearchCriteria criteria), which is much more flexible and not so hackish, however it wil be more challenge to implement this. We will discuss with Bolek and we will possibly provide needed changes in Picketlink IDM...

     

    Anyway, new method is not guaranteed to provide accurate results, but similarly like "getUserCount()" it will just try to provide best-effort.

     

      • In IDMUserListAcccess, we can have these 4 main combinations:
        • countAll=false, status = UserStatus.BOTH

    no change needed. We are using "fullResults" field to eagerly obtain results because we don't want to rely on inaccurate "getUserCount()" provided by Picketlink IDM

        • countAll=false, status = UserStatus.ENABLED or status = UserStatus.DISABLED

    We will still use "fullResults" to eagerly obtain results. We just need to add "enabled" attribute to UserQueryBuilder, so that Picketlink IDM will return only "enabled" or only "disabled" users (Maybe we will need to send 2 queries as mentioned in chapter "backwards compatibility")

        • countAll=true, status = UserStatus.BOTH

    No change needed. We are relying on count returned from Picketlink IDM by PersistenceManager.getUserCount() and during call to "load", we are obtaining needed results with paginated query to Picketlink IDM

        • countAll=true, status = UserStatus.ENABLED or status = UserStatus.DISABLED

    We will rely on count returned from Picketlink IDM by PersistenceManager.getUserCount(UserStatus status) (New method in Picketlink IDM as mentioned above) and we will obtain needed results with paginated query to Picketlink IDM. This query needs to filter disabled users.

      • IDMUserListAccess is used by all UserDAOImpl finder methods like findUsersByGroupId, findAllUsers, findUsersByQuery, findUsers, getUserPageList, so we should be good.
    Other DAO classes

    It seems that other DAO classes are not affected by "disabled user" feature. Question is if finder methods in MembershipDAOImpl should return memberships for disabled user as well? Or if attempt to create/update/delete UserProfile of disabled user should fail similarly like for UserDAOImpl? Specification doesn't say anything about it and also javadoc changes in https://github.com/exodev/core/commit/e19f9bee34b8a506aa264c188f922161930c829b and eXo OrganizationService implementations are not adding anything related to this. So I assume that our DAO also won't be affected.

    JAAS Authentication

    The OrganizationAuthenticatorImpl now have ThreadLocal variable lastExceptionOnValidateUser, which contains last exception thrown by UserHandler.authenticate. The code is here https://github.com/exodev/core/blob/e19f9bee34b8a506aa264c188f922161930c829b/exo.core.component.organization.api/src/main/java/org/exoplatform/services/organization/auth/OrganizationAuthenticatorImpl.java .

     

    This method could handle situation when JAAS login happens through the call to UserHandler.authenticate(). However it's not the case with alternative authentication methods like SSO or OAuth login. Also it relies on the fact that "consumer" layer (UI or CLI), which will later invoke authenticator.getLastExceptionOnValidateUser(), is invoked from the same Thread. Actually it's the case for JBoss7, Tomcat7 login when JAAS login from UI is triggered from LoginServlet, so JAAS call happens in same Thread. But I am not sure about CLI (Crash, GateIn management) login workflow... Another possible issue is, that ThreadLocal variable is actually not cleared, I've created JIRA for this https://jira.exoplatform.org/browse/COR-299 .

     

    So to handle alternative authentication flows like SSO or OAuth login, I would suggest to add another Login Module to the top of JAAS stack, which will fail-fast in case that User is disabled, so it won't allow SSO or OAuth login to happen. New login module could save the info about failed authentication into HttpSession attribute. There is no standard way to obtain HttpServletRequest or HttpSession from JAAS layer, but we are already using some alternatives to handle this on JBoss7/Tomcat7 . See SSOLoginModule.getCurrentHttpServletRequest()  .

     

    I am not 100% sure how to handle it in CLI login. If it's called from same Thread, we can use ThreadLocal variable instead of HttpSession attribute.

     

    Backwards compatibility

    It seems that all newly created users in GateIn will have attribute "enabled" set with value "true" by default. Thing is that people migrating from previous GateIn versions won't have this attribute filled for their users. It seems that for backwards compatibility, we should assume that user is enabled if attribute is not available.

     

    For filtering of users in IDMUserListAccess, this could be possible challenge because Picketlink IDM query API allows to filter by more attributeNames and more attributeValues, but it's using AND semantics (For example: It's possible to use query like: return all users where age>18 AND phone=='111222333' AND phone=='333222111' (Single attribute could have more values in Picketlink IDM, so returned user must have both these phone numbers set). However it's not easily possible to use query with OR semantics like: return all users where disabled==null OR disabled==true)

     

    We have provided a migration script that help to migrate the legacy data (adding the missed attribute). Here is the guide to run this script: gatein-portal/component/identity/DisableUserMigration_Manual.md at 3.7.0.Beta01 · gatein/gatein-portal · GitHub

     

    We also provide a configuration option: filterDisabledUsersInQueries for PicketLinkIDMOrganizationServiceImpl. When this option is set to false, the query for a list of user always return BOTH enabled and disabled users

     

    UI updates

    - In Organization Portlet, the tab "User Management" is the place for administrators to find/manage users in portal. Perhaps it should show only non-disabled users by default, and having a checkbox in the Search form to indicate whether the search query result includes disabled users or NOT.

     

    To easily distinguish disabled and non-disabled users from the result table, we would darken the row of disabled users by using CSS.

     

    There should be also a new button icon next to Edit and Delete ones in each user item in the table result, allowing administrators to disable/enable the corresponding user.

     

    - In UIUserSelector class, this is a re-usable WebUI component which can be used by applications to find/select users from portal for specific purposes, such as for permission setting. Therefore, it makes sense to show only non-disabled users by default, and also provide a boolean option to this component for developers to activate displaying disabled users in the usage.