001// --------------------------------------------------------------------------------
002// Copyright 2002-2025 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.user.server.entity.UserLoginPasswordString;
037import com.echothree.model.data.user.server.entity.UserLoginStatus;
038import com.echothree.util.common.form.BaseForm;
039import com.echothree.util.common.message.ExecutionErrors;
040import com.echothree.util.common.message.ExecutionWarnings;
041import com.echothree.util.common.validation.FieldDefinition;
042import com.echothree.util.server.control.BaseSimpleCommand;
043import com.echothree.util.server.control.CommandSecurityDefinition;
044import com.echothree.util.server.persistence.Session;
045import com.echothree.util.server.persistence.Sha1Utils;
046import com.echothree.util.server.string.UnitOfMeasureUtils;
047import java.util.List;
048
049public abstract class BaseLoginCommand<F extends BaseForm>
050        extends BaseSimpleCommand<F> {
051    
052    /** Creates a new instance of BaseLoginCommand */
053    protected BaseLoginCommand(final CommandSecurityDefinition commandSecurityDefinition,
054            final List<FieldDefinition> formFieldDefinition) {
055        super(commandSecurityDefinition, formFieldDefinition, false);
056    }
057    
058    protected UserLoginPasswordString checkPassword(final String password, final Party party, final String userLoginPasswordTypeName,
059            final boolean deleteOnSuccess) {
060        var userControl = getUserControl();
061        var userLoginPasswordType = userControl.getUserLoginPasswordTypeByName(userLoginPasswordTypeName);
062        var userLoginPassword = deleteOnSuccess? userControl.getUserLoginPasswordForUpdate(party, userLoginPasswordType):
063            userControl.getUserLoginPassword(party, userLoginPasswordType);
064        UserLoginPasswordString result = null;
065
066        if(userLoginPassword != null) {
067            var userLoginPasswordEncoderType = userLoginPassword.getUserLoginPasswordType().getUserLoginPasswordEncoderType();
068            var userLoginPasswordEncoderTypeName = userLoginPasswordEncoderType.getUserLoginPasswordEncoderTypeName();
069            var userLoginPasswordString = userControl.getUserLoginPasswordString(userLoginPassword);
070            
071            if(userLoginPasswordEncoderTypeName.equals(UserConstants.UserLoginPasswordEncoderType_SHA1)) {
072                result = Sha1Utils.getInstance().encode(userLoginPasswordString.getSalt(), password).equals(userLoginPasswordString.getPassword()) ? userLoginPasswordString: null;
073            } else if(userLoginPasswordEncoderTypeName.equals(UserConstants.UserLoginPasswordEncoderType_TEXT)) {
074                result = password.equals(userLoginPasswordString.getPassword())? userLoginPasswordString: null;
075            }
076            
077            if(deleteOnSuccess && result != null) {
078                userControl.deleteUserLoginPassword(userLoginPassword, getPartyPK());
079            }
080        }
081        
082        return result;
083    }
084    
085    // TODO: Recovered password should become regular password if that ends up being the password that matches, also,
086    // the recovered password should be deleted if the user logs in using their regular one. Changing a password should also
087    // make sure a recovered password does not exist.
088    protected boolean checkPasswords(final UserLoginStatus userLoginStatus, final String password, final Party party, final boolean doStatusChecks) {
089        var result = checkPassword(password, party, UserConstants.UserLoginPasswordType_STRING, false);
090        
091        if(result == null) {
092            result = checkPassword(password, party, UserConstants.UserLoginPasswordType_RECOVERED_STRING, true);
093        }
094        
095        if(result == null) {
096            addExecutionError(ExecutionErrors.IncorrectPassword.name());
097        } else if(doStatusChecks) {
098            var partyControl = Session.getModelController(PartyControl.class);
099            var partyTypePasswordStringPolicy = partyControl.getPartyTypePasswordStringPolicy(party.getLastDetail().getPartyType());
100            
101            if(partyTypePasswordStringPolicy != null) {
102                var partyTypePasswordStringPolicyDetail = partyTypePasswordStringPolicy.getLastDetail();
103                var maximumPasswordLifetime = partyTypePasswordStringPolicyDetail.getMaximumPasswordLifetime();
104                var expiredLoginsPermitted = partyTypePasswordStringPolicyDetail.getExpiredLoginsPermitted();
105                
106                if(maximumPasswordLifetime != null) {
107                    var expirationWarningTime = partyTypePasswordStringPolicyDetail.getExpirationWarningTime();
108                    var changedTime = result.getChangedTime();
109                    
110                    if((session.START_TIME - changedTime) > maximumPasswordLifetime) {
111                        userLoginStatus.setExpiredCount(userLoginStatus.getExpiredCount() + 1);
112                        
113                        addExecutionWarning(ExecutionWarnings.PasswordExpired.name());
114                    } else if(expirationWarningTime != null) {
115                        var expirationTime = changedTime + maximumPasswordLifetime;
116                        var warningTime = expirationTime - expirationWarningTime;
117                        
118                        if(session.START_TIME > warningTime) {
119                            var uomControl = Session.getModelController(UomControl.class);
120                            var timeUnitOfMeasureKind = uomControl.getUnitOfMeasureKindByUnitOfMeasureKindUseTypeUsingNames(UomConstants.UnitOfMeasureKindUseType_TIME);
121                            var remainingTime = UnitOfMeasureUtils.getInstance().formatUnitOfMeasure(getUserVisit(), timeUnitOfMeasureKind, Long.valueOf(expirationTime - session.START_TIME));
122                            
123                            addExecutionWarning(ExecutionWarnings.PasswordExpiration.name(), remainingTime);
124                        }
125                    }
126                }
127                
128                if(expiredLoginsPermitted != null && userLoginStatus.getExpiredCount() > expiredLoginsPermitted) {
129                    result = null;
130                    addExecutionError(ExecutionErrors.MaximumExpiredLoginsPermittedExceeded.name(), expiredLoginsPermitted);
131                }
132
133                if(userLoginStatus.getForceChange()) {
134                    addExecutionWarning(ExecutionWarnings.ForcePasswordChange.name());
135                }
136            }
137        }
138        
139        return result != null;
140    }
141
142    protected String getSecurityRoleGroupName(final PartyType partyType) {
143        String securityRoleGroupName = null;
144        var partyTypeName = partyType.getPartyTypeName();
145
146        if(partyTypeName.equals(CUSTOMER.name())) {
147            securityRoleGroupName = SecurityRoleGroups.Customer.name();
148        } else if(partyTypeName.equals(EMPLOYEE.name())) {
149            securityRoleGroupName = Employee.name();
150        } else if(partyTypeName.equals(VENDOR.name())) {
151            securityRoleGroupName = SecurityRoleGroups.Vendor.name();
152        }
153
154        return securityRoleGroupName;
155    }
156
157    protected void clearLoginFailures(final UserLoginStatus userLoginStatus) {
158        // Audit trail for callers of this function should be created by the callers.
159        userLoginStatus.setFailureCount(0);
160        userLoginStatus.setFirstFailureTime(null);
161        userLoginStatus.setLastFailureTime(null);
162    }
163
164    protected void addRemoteInet4AddressToParty(final Party party, final Integer remoteInet4Address) {
165        if(remoteInet4Address != null) {
166            var contactControl = Session.getModelController(ContactControl.class);
167            var partyPK = party.getPrimaryKey();
168            var partyContactMechanism = contactControl.getPartyContactMechanismByInet4Address(party, remoteInet4Address);
169
170            if(partyContactMechanism == null) {
171                var contactMechanismName = SequenceGeneratorLogic.getInstance().getNextSequenceValue(null, SequenceTypes.CONTACT_MECHANISM.name());
172                var contactMechanismType = contactControl.getContactMechanismTypeByName(ContactMechanismTypes.INET_4.name());
173                var contactMechanism = contactControl.createContactMechanism(contactMechanismName, contactMechanismType, false, partyPK);
174
175                contactControl.createContactInet4Address(contactMechanism, remoteInet4Address, partyPK);
176                partyContactMechanism = contactControl.createPartyContactMechanism(party, contactMechanism, null, false, 1, partyPK);
177            }
178
179            var contactMechanismPurpose = contactControl.getContactMechanismPurposeByName(ContactMechanismPurposes.INET_4_LOGIN.name());
180            var partyContactMechanismPurpose = contactControl.getPartyContactMechanismPurpose(partyContactMechanism, contactMechanismPurpose);
181            if(partyContactMechanismPurpose == null) {
182                contactControl.createPartyContactMechanismPurpose(partyContactMechanism, contactMechanismPurpose, false, 1, partyPK);
183            }
184        }
185    }
186
187    protected void successfulLogin(final UserLoginStatus userLoginStatus, final Party party, final PartyRelationship partyRelationship,
188            final Integer remoteInet4Address) {
189        var userControl = getUserControl();
190        var userVisit = getUserVisitForUpdate();
191        var userKey = userVisit.getUserKey();
192        var userKeyDetailValue = userControl.getUserKeyDetailValueByPKForUpdate(userKey.getLastDetail().getPrimaryKey());
193
194        userControl.associatePartyToUserVisit(userVisit, party, partyRelationship, session.START_TIME_LONG);
195        
196        // Only update the UserKeyDetail if the party has changed
197        var partyPK = party.getPrimaryKey();
198        var partyRelationshipPK = partyRelationship == null? null: partyRelationship.getPrimaryKey();
199        var userKeyPartyPK = userKeyDetailValue.getPartyPK();
200        var userKeyPartyRelationshipPK = userKeyDetailValue.getPartyRelationshipPK();
201        
202        if(userKeyPartyPK == null || !userKeyPartyPK.equals(partyPK)
203                || userKeyPartyRelationshipPK == null || !userKeyPartyRelationshipPK.equals(partyRelationshipPK)) {
204            userKeyDetailValue.setPartyPK(partyPK);
205            userKeyDetailValue.setPartyRelationshipPK(partyRelationshipPK);
206            userControl.updateUserKeyFromValue(userKeyDetailValue);
207        }
208
209        clearLoginFailures(userLoginStatus);
210
211        userLoginStatus.setLastLoginTime(session.START_TIME_LONG);
212
213        addRemoteInet4AddressToParty(party, remoteInet4Address);
214
215        // TODO: Create audit trail
216    }
217    
218    protected void unsuccessfulLogin(final UserLoginStatus userLoginStatus) {
219        var failureCount = userLoginStatus.getFailureCount();
220        
221        userLoginStatus.setFailureCount(failureCount + 1);
222        if(userLoginStatus.getFirstFailureTime() == null) {
223            userLoginStatus.setFirstFailureTime(session.START_TIME_LONG);
224        }
225        userLoginStatus.setLastFailureTime(session.START_TIME_LONG);
226        
227        // TODO: Create audit trail
228    }
229    
230}