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.model.control.party.server.logic;
018
019import com.echothree.model.control.party.server.control.PartyControl;
020import com.echothree.model.control.uom.common.UomConstants;
021import com.echothree.model.control.uom.server.control.UomControl;
022import com.echothree.model.control.user.server.control.UserControl;
023import com.echothree.model.data.party.server.entity.Party;
024import com.echothree.model.data.party.server.entity.PartyType;
025import com.echothree.model.data.party.server.entity.PartyTypePasswordStringPolicy;
026import com.echothree.model.data.party.server.entity.PartyTypePasswordStringPolicyDetail;
027import com.echothree.model.data.uom.server.entity.UnitOfMeasureKind;
028import com.echothree.model.data.user.server.entity.UserLoginPassword;
029import com.echothree.model.data.user.server.entity.UserLoginPasswordString;
030import com.echothree.model.data.user.server.entity.UserVisit;
031import com.echothree.model.data.user.server.value.UserLoginPasswordStringValue;
032import com.echothree.util.common.message.ExecutionErrors;
033import com.echothree.util.common.string.StringUtils;
034import com.echothree.util.server.message.ExecutionErrorAccumulator;
035import com.echothree.util.server.persistence.Session;
036import com.echothree.util.server.persistence.Sha1Utils;
037import com.echothree.util.server.string.UnitOfMeasureUtils;
038import java.util.HashMap;
039import java.util.Map;
040
041public class PasswordStringPolicyLogic {
042    
043    private PasswordStringPolicyLogic() {
044        super();
045    }
046    
047    private static class PasswordStringPolicyLogicHolder {
048        static PasswordStringPolicyLogic instance = new PasswordStringPolicyLogic();
049    }
050    
051    public static PasswordStringPolicyLogic getInstance() {
052        return PasswordStringPolicyLogicHolder.instance;
053    }
054    
055    private void checkAllowChange(final ExecutionErrorAccumulator ema, final PartyTypePasswordStringPolicyDetail policyDetail) {
056        if(!policyDetail.getAllowChange()) {
057            ema.addExecutionError(ExecutionErrors.PasswordChangeNotAllowed.name());
058        }
059    }
060    
061    private void checkPasswordHistory(final ExecutionErrorAccumulator ema,
062            final PartyTypePasswordStringPolicyDetail policyDetail, final UserLoginPassword ulp, final String password) {
063        Integer passwordHistory = policyDetail.getPasswordHistory();
064        
065        if(passwordHistory != null) {
066            var userControl = Session.getModelController(UserControl.class);
067            
068            for(UserLoginPasswordString userLoginPasswordString: userControl.getUserLoginPasswordStringHistory(ulp, passwordHistory)) {
069                String salt = userLoginPasswordString.getSalt();
070                
071                if(Sha1Utils.getInstance().encode(salt, password).equals(userLoginPasswordString.getPassword())) {
072                    ema.addExecutionError(ExecutionErrors.PasswordInRecentHistory.name(), passwordHistory);
073                    break;
074                }
075            }
076        }
077    }
078    
079    private void checkMinimumPasswordLifetime(final Session session, final UserVisit userVisit,
080            final ExecutionErrorAccumulator ema, final PartyTypePasswordStringPolicyDetail policyDetail, final UserLoginPasswordStringValue ulpsv) {
081        Long minimumPasswordLifetime = policyDetail.getMinimumPasswordLifetime();
082        
083        if(minimumPasswordLifetime != null) {
084            long currentPasswordLifetime = session.START_TIME - ulpsv.getChangedTime();
085            
086            if(currentPasswordLifetime < minimumPasswordLifetime) {
087                var uomControl = Session.getModelController(UomControl.class);
088                UnitOfMeasureKind timeUnitOfMeasureKind = uomControl.getUnitOfMeasureKindByUnitOfMeasureKindUseTypeUsingNames(UomConstants.UnitOfMeasureKindUseType_TIME);
089                String fmtMinimumPasswordLifetime = UnitOfMeasureUtils.getInstance().formatUnitOfMeasure(userVisit,
090                        timeUnitOfMeasureKind, minimumPasswordLifetime);
091                String fmtCurrentPasswordLifetime = UnitOfMeasureUtils.getInstance().formatUnitOfMeasure(userVisit,
092                        timeUnitOfMeasureKind, Long.valueOf(currentPasswordLifetime));
093                
094                ema.addExecutionError(ExecutionErrors.PasswordMinimumLifetimeNotMet.name(), fmtMinimumPasswordLifetime, fmtCurrentPasswordLifetime);
095            }
096        }
097    }
098    
099    private void checkLength(final ExecutionErrorAccumulator ema, final PartyTypePasswordStringPolicyDetail policyDetail,
100            final String password) {
101        int length = password.length();
102        Integer minimumLegnth = policyDetail.getMinimumLength();
103        Integer maximumLength = policyDetail.getMaximumLength();
104        
105        if(minimumLegnth != null && length < minimumLegnth) {
106            ema.addExecutionError(ExecutionErrors.PasswordLessThanMinimumLength.name(), minimumLegnth);
107        }
108        
109        if(maximumLength != null && length < maximumLength) {
110            ema.addExecutionError(ExecutionErrors.PasswordGreaterThanMaximumLength.name(), maximumLength);
111        }
112    }
113    
114    private int getTypeCount(final Map<Integer, Integer> types, final byte type) {
115        Integer count = types.get(Integer.valueOf(type));
116        
117        return count == null? 0: count;
118    }
119    
120    private void checkCharacterTypes(final ExecutionErrorAccumulator ema, final PartyTypePasswordStringPolicyDetail policyDetail,
121            final String password) {
122        Integer requiredDigitCount = policyDetail.getRequiredDigitCount();
123        Integer requiredLetterCount = policyDetail.getRequiredLetterCount();
124        Integer requiredUpperCaseCount = policyDetail.getRequiredUpperCaseCount();
125        Integer requiredLowerCaseCount = policyDetail.getRequiredLowerCaseCount();
126        Integer maximumRepeated = policyDetail.getMaximumRepeated();
127        Integer minimumCharacterTypes = policyDetail.getMinimumCharacterTypes();
128        Map<Integer, Integer> types = new HashMap<>();
129        int lastCh = 0;
130        int repeat = 0;
131        int maxRepeat = 0;
132        
133        for(int ch : StringUtils.getInstance().codePoints(password)) {
134            Integer type = Character.getType(ch);
135            Integer count = types.get(type);
136            
137            if(count == null) {
138                types.put(type, 1);
139            } else {
140                types.put(type, count + 1);
141            }
142            
143            if(ch == lastCh) {
144                repeat++;
145            } else {
146                lastCh = ch;
147                
148                if(repeat > maxRepeat) {
149                    maxRepeat = repeat;
150                }
151                
152                repeat = 1;
153            }
154        }
155        
156        if(repeat > maxRepeat) {
157            maxRepeat = repeat;
158        }
159        
160        int upperCaseCount = getTypeCount(types, Character.UPPERCASE_LETTER);
161        int lowerCaseCount = getTypeCount(types, Character.LOWERCASE_LETTER);
162        int letterCount = upperCaseCount + lowerCaseCount;
163        
164        if(requiredDigitCount != null) {
165            int digitCount = getTypeCount(types, Character.DECIMAL_DIGIT_NUMBER);
166            
167            if(digitCount < requiredDigitCount) {
168                ema.addExecutionError(ExecutionErrors.PasswordRequiredDigitCountNotMet.name(), requiredDigitCount);
169            }
170        }
171        
172        if(requiredLetterCount != null) {
173            if(letterCount < requiredLetterCount) {
174                ema.addExecutionError(ExecutionErrors.PasswordRequiredLetterCountNotMet.name(), requiredLetterCount);
175            }
176        }
177        
178        if(requiredUpperCaseCount != null) {
179            if(upperCaseCount < requiredUpperCaseCount) {
180                ema.addExecutionError(ExecutionErrors.PasswordRequiredUpperCaseCountNotMet.name(), requiredUpperCaseCount);
181            }
182        }
183        
184        if(requiredLowerCaseCount != null) {
185            if(lowerCaseCount < requiredLowerCaseCount) {
186                ema.addExecutionError(ExecutionErrors.PasswordRequiredLowerCaseCountNotMet.name(), requiredLowerCaseCount);
187            }
188        }
189        
190        if(maximumRepeated != null) {
191            if(maxRepeat < maximumRepeated) {
192                ema.addExecutionError(ExecutionErrors.PasswordMaximumRepeatedExceeded.name(), maximumRepeated);
193            }
194        }
195        
196        if(minimumCharacterTypes != null) {
197            int characterTypes = types.size();
198            
199            if(characterTypes < minimumCharacterTypes) {
200                ema.addExecutionError(ExecutionErrors.PasswordMinimumCharacterTypesNotMet.name(), minimumCharacterTypes);
201            }
202        }
203    }
204    
205    public PartyTypePasswordStringPolicy checkStringPassword(final Session session, final UserVisit userVisit, final ExecutionErrorAccumulator ema,
206            final PartyType partyType, final UserLoginPassword ulp, final UserLoginPasswordStringValue ulpsv, final String password) {
207        var partyControl = Session.getModelController(PartyControl.class);
208        PartyTypePasswordStringPolicy policy = partyControl.getPartyTypePasswordStringPolicy(partyType);
209        
210        if(policy != null) {
211            PartyTypePasswordStringPolicyDetail policyDetail = policy.getLastDetail();
212            
213            if(ulp != null) {
214                checkPasswordHistory(ema, policyDetail, ulp, password);
215            }
216            
217            if(ulpsv != null) {
218                checkAllowChange(ema, policyDetail);
219                checkMinimumPasswordLifetime(session, userVisit, ema, policyDetail, ulpsv);
220            }
221            
222            checkLength(ema, policyDetail, password);
223            checkCharacterTypes(ema, policyDetail, password);
224        }
225
226        return policy;
227    }
228    
229    public PartyTypePasswordStringPolicy checkStringPassword(final Session session, final UserVisit userVisit, final ExecutionErrorAccumulator ema,
230            final Party party, final UserLoginPassword ulp, final UserLoginPasswordStringValue ulpsv, final String password) {
231        PartyType partyType = party.getLastDetail().getPartyType();
232        
233        return checkStringPassword(session, userVisit, ema, partyType, ulp, ulpsv, password);
234    }
235    
236}