classdef SPFMJSCredentialHandler < handle
    %SPFMJSCredentialHandler - MJSCredentialHandler for SPF backed cluster

    % Copyright 2020-2021 The MathWorks, Inc.
    
    properties (Access = private)
        JobManagerHost
        JobManagerName
        RememberPassword
    end
    
    properties (Constant)
        ConnectTimeoutSeconds = 10;
    end
    
    methods
        function obj = SPFMJSCredentialHandler(jobManagerHost, jobManagerName)            
            obj.JobManagerHost = jobManagerHost;
            obj.JobManagerName = jobManagerName;
            obj.RememberPassword = parallel.internal.types.RememberPassword.Session;
        end
        
        function securityLevel = getSecurityLevel(obj)
            client = obj.getClient();
            securityLevel = str2double(client.getProperty('SecurityLevel')); 
        end
        
        function setRememberPassword(obj, value)
            if ischar(value)
                value = parallel.internal.types.RememberPassword.fromName(value);
            end
            obj.RememberPassword = value;
        end
        
        function addCredentials(obj, user, password)
            [mjsSupport, passwordProvider] = obj.createMJSSupport(user, password, false);
            % The above might use the stored password if the user already
            % exists and the password is in the store. To avoid this we
            % change the password to itself which forces the password to be
            % checked. If it is wrong, this will error.
            passwordProvider.putPasswordForChange(user, password);
            mjsSupport.changePassword(user);
        end
        
        function changePassword(obj, user, oldPassword, newPassword)
            errorIfUserDoesNotExist = true; % Don't create a new user to change their password
            [mjsSupport, passwordProvider] = obj.createMJSSupport(user, oldPassword, errorIfUserDoesNotExist);
            % Store the new password
            passwordProvider.putPasswordForChange(user, oldPassword);
            passwordProvider.put(user, newPassword);
            
            % Trigger MJS to ask the password provider for the new password
            mjsSupport.changePassword(user);
        end
        
        function changePasswordAsAdmin(obj, user, adminPassword, newUserPassword)
            % Store the admin password and connect as admin
            client = obj.getClient();
            adminUser = client.getProperty('AdminUsername');
            errorIfUserDoesNotExist = true; % Don't create a new user to change their password
            [mjsSupport, passwordProvider] = obj.createMJSSupport(adminUser, adminPassword, errorIfUserDoesNotExist);
            
            % Store the new user password in the temporary password provider
            passwordProvider.put(user, newUserPassword);
            passwordProvider.putPasswordForChange(adminUser, adminPassword);
            
            % Trigger MJS to ask the password provider for the new password
            mjsSupport.changePassword(user);
        end
        
        function logout(obj, user)
            client = obj.getClient();
            sessionID = client.getProperty('SessionID');
            store = parallel.internal.cluster.CredentialStoreProvider.getOrCreate(sessionID);
            store.remove(user);
        end
    end
    
    methods (Access = private)
        % Returns a new JobManagerClient
        function client = getClient(obj)
            client = parallel.internal.cluster.CachingJobManagerFinder.getOneJobManager(...
                obj.ConnectTimeoutSeconds, obj.JobManagerHost, obj.JobManagerName, true);
        end
        
        % Returns a new MJSSupportWrapper and its underlying password provider,
        % which can be used to inject passwords as necessary
        function [mjsSupport, passwordProvider] = createMJSSupport(obj, username, password, errorIfUserDoesNotExist)
            % Temporarily set the password provider factory to provide
            % MJSCredentialHandlerPasswordProvider and create a new access
            passwordProvider = parallel.internal.cluster.MJSCredentialHandlerPasswordProvider(errorIfUserDoesNotExist);
            passwordProvider.setRememberPassword(obj.RememberPassword);
            passwordProvider.put(username, password);
            passwordProviderFactory = parallel.internal.cluster.FixedPasswordProviderFactory(passwordProvider);
            oldFactory = parallel.internal.cluster.defaultPasswordProviderFactory(passwordProviderFactory);
            restoreFactory = onCleanup(@() parallel.internal.cluster.defaultPasswordProviderFactory(oldFactory));
            mjsSupport = parallel.internal.cluster.MJSSupportWrapperProvider.createClientSupport(obj.JobManagerHost, obj.JobManagerName, true, username);
        end
    end
end
