001// --------------------------------------------------------------------------------
002// Copyright 2002-2024 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.control.user.authentication.server.command;
018
019import com.echothree.model.control.contact.common.ContactMechanismPurposes;
020import com.echothree.model.control.contact.common.ContactMechanismTypes;
021import com.echothree.model.control.contact.server.control.ContactControl;
022import static com.echothree.model.control.party.common.PartyTypes.CUSTOMER;
023import static com.echothree.model.control.party.common.PartyTypes.EMPLOYEE;
024import static com.echothree.model.control.party.common.PartyTypes.VENDOR;
025import com.echothree.model.control.party.server.control.PartyControl;
026import com.echothree.model.control.security.common.SecurityRoleGroups;
027import static com.echothree.model.control.security.common.SecurityRoleGroups.Employee;
028import com.echothree.model.control.sequence.common.SequenceTypes;
029import com.echothree.model.control.sequence.server.logic.SequenceGeneratorLogic;
030import com.echothree.model.control.uom.common.UomConstants;
031import com.echothree.model.control.uom.server.control.UomControl;
032import com.echothree.model.control.user.common.UserConstants;
033import com.echothree.model.data.party.server.entity.Party;
034import com.echothree.model.data.party.server.entity.PartyRelationship;
035import com.echothree.model.data.party.server.entity.PartyType;
036import com.echothree.model.data.uom.server.entity.UnitOfMeasureKind;
037import com.echothree.model.data.user.common.pk.UserVisitPK;
038import com.echothree.model.data.user.server.entity.UserLoginPasswordString;
039import com.echothree.model.data.user.server.entity.UserLoginStatus;
040import com.echothree.util.common.form.BaseForm;
041import com.echothree.util.common.message.ExecutionErrors;
042import com.echothree.util.common.message.ExecutionWarnings;
043import com.echothree.util.common.validation.FieldDefinition;
044import com.echothree.util.server.control.BaseSimpleCommand;
045import com.echothree.util.server.control.CommandSecurityDefinition;
046import com.echothree.util.server.persistence.Session;
047import com.echothree.util.server.persistence.Sha1Utils;
048import com.echothree.util.server.string.UnitOfMeasureUtils;
049import java.util.List;
050
051public abstract class BaseLoginCommand<F extends BaseForm>
052        extends BaseSimpleCommand<F> {
053    
054    /** Creates a new instance of BaseLoginCommand */
055    protected BaseLoginCommand(final UserVisitPK userVisitPK, final F form, final CommandSecurityDefinition commandSecurityDefinition,
056            final List<FieldDefinition> formFieldDefinition) {
057        super(userVisitPK, form, commandSecurityDefinition, formFieldDefinition, false);
058    }
059    
060    protected UserLoginPasswordString checkPassword(final String password, final Party party, final String userLoginPasswordTypeName,
061            final boolean deleteOnSuccess) {
062        var userControl = getUserControl();
063        var userLoginPasswordType = userControl.getUserLoginPasswordTypeByName(userLoginPasswordTypeName);
064        var userLoginPassword = deleteOnSuccess? userControl.getUserLoginPasswordForUpdate(party, userLoginPasswordType):
065            userControl.getUserLoginPassword(party, userLoginPasswordType);
066        UserLoginPasswordString result = null;
067
068        if(userLoginPassword != null) {
069            var userLoginPasswordEncoderType = userLoginPassword.getUserLoginPasswordType().getUserLoginPasswordEncoderType();
070            var userLoginPasswordEncoderTypeName = userLoginPasswordEncoderType.getUserLoginPasswordEncoderTypeName();
071            var userLoginPasswordString = userControl.getUserLoginPasswordString(userLoginPassword);
072            
073            if(userLoginPasswordEncoderTypeName.equals(UserConstants.UserLoginPasswordEncoderType_SHA1)) {
074                result = Sha1Utils.getInstance().encode(userLoginPasswordString.getSalt(), password).equals(userLoginPasswordString.getPassword()) ? userLoginPasswordString: null;
075            } else if(userLoginPasswordEncoderTypeName.equals(UserConstants.UserLoginPasswordEncoderType_TEXT)) {
076                result = password.equals(userLoginPasswordString.getPassword())? userLoginPasswordString: null;
077            }
078            
079            if(deleteOnSuccess && result != null) {
080                userControl.deleteUserLoginPassword(userLoginPassword, getPartyPK());
081            }
082        }
083        
084        return result;
085    }
086    
087    // TODO: Recovered password should become regular password if that ends up being the password that matches, also,
088    // the recovered password should be deleted if the user logs in using their regular one. Changing a password should also
089    // make sure a recovered password does not exist.
090    protected boolean checkPasswords(final UserLoginStatus userLoginStatus, final String password, final Party party, final boolean doStatusChecks) {
091        var result = checkPassword(password, party, UserConstants.UserLoginPasswordType_STRING, false);
092        
093        if(result == null) {
094            result = checkPassword(password, party, UserConstants.UserLoginPasswordType_RECOVERED_STRING, true);
095        }
096        
097        if(result == null) {
098            addExecutionError(ExecutionErrors.IncorrectPassword.name());
099        } else if(doStatusChecks) {
100            var partyControl = Session.getModelController(PartyControl.class);
101            var partyTypePasswordStringPolicy = partyControl.getPartyTypePasswordStringPolicy(party.getLastDetail().getPartyType());
102            
103            if(partyTypePasswordStringPolicy != null) {
104                var partyTypePasswordStringPolicyDetail = partyTypePasswordStringPolicy.getLastDetail();
105                var maximumPasswordLifetime = partyTypePasswordStringPolicyDetail.getMaximumPasswordLifetime();
106                var expiredLoginsPermitted = partyTypePasswordStringPolicyDetail.getExpiredLoginsPermitted();
107                
108                if(maximumPasswordLifetime != null) {
109                    var expirationWarningTime = partyTypePasswordStringPolicyDetail.getExpirationWarningTime();
110                    var changedTime = result.getChangedTime();
111                    
112                    if((session.START_TIME - changedTime) > maximumPasswordLifetime) {
113                        userLoginStatus.setExpiredCount(userLoginStatus.getExpiredCount() + 1);
114                        
115                        addExecutionWarning(ExecutionWarnings.PasswordExpired.name());
116                    } else if(expirationWarningTime != null) {
117                        var expirationTime = changedTime + maximumPasswordLifetime;
118                        var warningTime = expirationTime - expirationWarningTime;
119                        
120                        if(session.START_TIME > warningTime) {
121                            var uomControl = Session.getModelController(UomControl.class);
122                            UnitOfMeasureKind timeUnitOfMeasureKind = uomControl.getUnitOfMeasureKindByUnitOfMeasureKindUseTypeUsingNames(UomConstants.UnitOfMeasureKindUseType_TIME);
123                            String remainingTime = UnitOfMeasureUtils.getInstance().formatUnitOfMeasure(getUserVisit(), timeUnitOfMeasureKind, Long.valueOf(expirationTime - session.START_TIME));
124                            
125                            addExecutionWarning(ExecutionWarnings.PasswordExpiration.name(), remainingTime);
126                        }
127                    }
128                }
129                
130                if(expiredLoginsPermitted != null && userLoginStatus.getExpiredCount() > expiredLoginsPermitted) {
131                    result = null;
132                    addExecutionError(ExecutionErrors.MaximumExpiredLoginsPermittedExceeded.name(), expiredLoginsPermitted);
133                }
134
135                if(userLoginStatus.getForceChange()) {
136                    addExecutionWarning(ExecutionWarnings.ForcePasswordChange.name());
137                }
138            }
139        }
140        
141        return result != null;
142    }
143
144    protected String getSecurityRoleGroupName(final PartyType partyType) {
145        String securityRoleGroupName = null;
146        var partyTypeName = partyType.getPartyTypeName();
147
148        if(partyTypeName.equals(CUSTOMER.name())) {
149            securityRoleGroupName = SecurityRoleGroups.Customer.name();
150        } else if(partyTypeName.equals(EMPLOYEE.name())) {
151            securityRoleGroupName = Employee.name();
152        } else if(partyTypeName.equals(VENDOR.name())) {
153            securityRoleGroupName = SecurityRoleGroups.Vendor.name();
154        }
155
156        return securityRoleGroupName;
157    }
158
159    protected void clearLoginFailures(final UserLoginStatus userLoginStatus) {
160        // Audit trail for callers of this function should be created by the callers.
161        userLoginStatus.setFailureCount(0);
162        userLoginStatus.setFirstFailureTime(null);
163        userLoginStatus.setLastFailureTime(null);
164    }
165
166    protected void addRemoteInet4AddressToParty(final Party party, final Integer remoteInet4Address) {
167        if(remoteInet4Address != null) {
168            var contactControl = Session.getModelController(ContactControl.class);
169            var partyPK = party.getPrimaryKey();
170            var partyContactMechanism = contactControl.getPartyContactMechanismByInet4Address(party, remoteInet4Address);
171
172            if(partyContactMechanism == null) {
173                var contactMechanismName = SequenceGeneratorLogic.getInstance().getNextSequenceValue(null, SequenceTypes.CONTACT_MECHANISM.name());
174                var contactMechanismType = contactControl.getContactMechanismTypeByName(ContactMechanismTypes.INET_4.name());
175                var contactMechanism = contactControl.createContactMechanism(contactMechanismName, contactMechanismType, Boolean.FALSE, partyPK);
176
177                contactControl.createContactInet4Address(contactMechanism, remoteInet4Address, partyPK);
178                partyContactMechanism = contactControl.createPartyContactMechanism(party, contactMechanism, null, Boolean.FALSE, 1, partyPK);
179            }
180
181            var contactMechanismPurpose = contactControl.getContactMechanismPurposeByName(ContactMechanismPurposes.INET_4_LOGIN.name());
182            var partyContactMechanismPurpose = contactControl.getPartyContactMechanismPurpose(partyContactMechanism, contactMechanismPurpose);
183            if(partyContactMechanismPurpose == null) {
184                contactControl.createPartyContactMechanismPurpose(partyContactMechanism, contactMechanismPurpose, Boolean.FALSE, 1, partyPK);
185            }
186        }
187    }
188
189    protected void successfulLogin(final UserLoginStatus userLoginStatus, final Party party, final PartyRelationship partyRelationship,
190            final Integer remoteInet4Address) {
191        var userControl = getUserControl();
192        var userVisit = getUserVisitForUpdate();
193        var userKey = userVisit.getUserKey();
194        var userKeyDetailValue = userControl.getUserKeyDetailValueByPKForUpdate(userKey.getLastDetail().getPrimaryKey());
195
196        userControl.associatePartyToUserVisit(userVisit, party, partyRelationship, session.START_TIME_LONG);
197        
198        // Only update the UserKeyDetail if the party has changed
199        var partyPK = party.getPrimaryKey();
200        var partyRelationshipPK = partyRelationship == null? null: partyRelationship.getPrimaryKey();
201        var userKeyPartyPK = userKeyDetailValue.getPartyPK();
202        var userKeyPartyRelationshipPK = userKeyDetailValue.getPartyRelationshipPK();
203        
204        if(userKeyPartyPK == null || !userKeyPartyPK.equals(partyPK)
205                || userKeyPartyRelationshipPK == null || !userKeyPartyRelationshipPK.equals(partyRelationshipPK)) {
206            userKeyDetailValue.setPartyPK(partyPK);
207            userKeyDetailValue.setPartyRelationshipPK(partyRelationshipPK);
208            userControl.updateUserKeyFromValue(userKeyDetailValue);
209        }
210
211        clearLoginFailures(userLoginStatus);
212
213        userLoginStatus.setLastLoginTime(session.START_TIME_LONG);
214
215        addRemoteInet4AddressToParty(party, remoteInet4Address);
216
217        // TODO: Create audit trail
218    }
219    
220    protected void unsuccessfulLogin(final UserLoginStatus userLoginStatus) {
221        var failureCount = userLoginStatus.getFailureCount();
222        
223        userLoginStatus.setFailureCount(failureCount + 1);
224        if(userLoginStatus.getFirstFailureTime() == null) {
225            userLoginStatus.setFirstFailureTime(session.START_TIME_LONG);
226        }
227        userLoginStatus.setLastFailureTime(session.START_TIME_LONG);
228        
229        // TODO: Create audit trail
230    }
231    
232}