Return-Path: X-Original-To: apmail-syncope-commits-archive@www.apache.org Delivered-To: apmail-syncope-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 9673F10E81 for ; Mon, 12 Jan 2015 16:31:41 +0000 (UTC) Received: (qmail 24406 invoked by uid 500); 12 Jan 2015 16:31:43 -0000 Delivered-To: apmail-syncope-commits-archive@syncope.apache.org Received: (qmail 24338 invoked by uid 500); 12 Jan 2015 16:31:43 -0000 Mailing-List: contact commits-help@syncope.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@syncope.apache.org Delivered-To: mailing list commits@syncope.apache.org Received: (qmail 23464 invoked by uid 99); 12 Jan 2015 16:31:42 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Mon, 12 Jan 2015 16:31:42 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id 17127A014BE; Mon, 12 Jan 2015 16:31:42 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit From: ilgrosso@apache.org To: commits@syncope.apache.org Date: Mon, 12 Jan 2015 16:32:24 -0000 Message-Id: <9821f63a8dce4b66a72cd7aacf7dbdf9@git.apache.org> In-Reply-To: <42bacbfafc744cb28210a7389d0a3305@git.apache.org> References: <42bacbfafc744cb28210a7389d0a3305@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [45/52] [abbrv] [partial] syncope git commit: [SYNCOPE-620] Unit tests all in http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/jexl/JexlUtil.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/jexl/JexlUtil.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/jexl/JexlUtil.java new file mode 100644 index 0000000..cb25033 --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/jexl/JexlUtil.java @@ -0,0 +1,289 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.jexl; + +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import org.apache.commons.jexl2.Expression; +import org.apache.commons.jexl2.JexlContext; +import org.apache.commons.jexl2.JexlEngine; +import org.apache.commons.jexl2.JexlException; +import org.apache.commons.jexl2.MapContext; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.common.lib.to.AbstractAttributableTO; +import org.apache.syncope.common.lib.to.AttrTO; +import org.apache.syncope.server.persistence.api.entity.Attributable; +import org.apache.syncope.server.persistence.api.entity.DerAttr; +import org.apache.syncope.server.persistence.api.entity.PlainAttr; +import org.apache.syncope.server.persistence.api.entity.VirAttr; +import org.apache.syncope.server.misc.DataFormat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JEXL reference is available. + */ +public final class JexlUtil { + + /** + * Logger. + * + */ + private static final Logger LOG = LoggerFactory.getLogger(JexlUtil.class); + + private static final String[] IGNORE_FIELDS = { "password", "clearPassword", "serialVersionUID", "class" }; + + private static JexlEngine jexlEngine; + + private static JexlEngine getEngine() { + synchronized (LOG) { + if (jexlEngine == null) { + jexlEngine = new JexlEngine(new ClassFreeUberspectImpl(null), null, null, null); + jexlEngine.setClassLoader(new EmptyClassLoader()); + jexlEngine.setCache(512); + jexlEngine.setLenient(true); + jexlEngine.setSilent(false); + } + } + + return jexlEngine; + } + + public static boolean isExpressionValid(final String expression) { + boolean result; + try { + getEngine().createExpression(expression); + result = true; + } catch (JexlException e) { + LOG.error("Invalid jexl expression: " + expression, e); + result = false; + } + + return result; + } + + public static String evaluate(final String expression, final JexlContext jexlContext) { + String result = StringUtils.EMPTY; + + if (StringUtils.isNotBlank(expression) && jexlContext != null) { + try { + Expression jexlExpression = getEngine().createExpression(expression); + Object evaluated = jexlExpression.evaluate(jexlContext); + if (evaluated != null) { + result = evaluated.toString(); + } + } catch (Exception e) { + LOG.error("Error while evaluating JEXL expression: " + expression, e); + } + } else { + LOG.debug("Expression not provided or invalid context"); + } + + return result; + } + + public static JexlContext addFieldsToContext(final Object object, final JexlContext jexlContext) { + JexlContext context = jexlContext == null ? new MapContext() : jexlContext; + + try { + for (PropertyDescriptor desc : Introspector.getBeanInfo(object.getClass()).getPropertyDescriptors()) { + final Class type = desc.getPropertyType(); + final String fieldName = desc.getName(); + + if ((!fieldName.startsWith("pc")) + && (!ArrayUtils.contains(IGNORE_FIELDS, fieldName)) + && (!Iterable.class.isAssignableFrom(type)) + && (!type.isArray())) { + try { + final Method getter = desc.getReadMethod(); + + final Object fieldValue; + + if (getter == null) { + final Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + fieldValue = field.get(object); + } else { + fieldValue = getter.invoke(object); + } + + context.set(fieldName, fieldValue == null + ? StringUtils.EMPTY + : (type.equals(Date.class) + ? DataFormat.format((Date) fieldValue, false) + : fieldValue)); + + LOG.debug("Add field {} with value {}", fieldName, fieldValue); + + } catch (Exception iae) { + LOG.error("Reading '{}' value error", fieldName, iae); + } + } + } + } catch (IntrospectionException ie) { + LOG.error("Reading class attributes error", ie); + } + + return context; + } + + public static JexlContext addAttrsToContext(final Collection attrs, + final JexlContext jexlContext) { + + JexlContext context = jexlContext == null + ? new MapContext() + : jexlContext; + + for (PlainAttr attr : attrs) { + if (attr.getSchema() != null) { + List attrValues = attr.getValuesAsStrings(); + String expressionValue = attrValues.isEmpty() + ? StringUtils.EMPTY + : attrValues.get(0); + + LOG.debug("Add attribute {} with value {}", attr.getSchema().getKey(), expressionValue); + + context.set(attr.getSchema().getKey(), expressionValue); + } + } + + return context; + } + + public static JexlContext addDerAttrsToContext(final Collection derAttrs, + final Collection attrs, final JexlContext jexlContext) { + + JexlContext context = jexlContext == null + ? new MapContext() + : jexlContext; + + for (DerAttr derAttr : derAttrs) { + if (derAttr.getSchema() != null) { + String expressionValue = derAttr.getValue(attrs); + if (expressionValue == null) { + expressionValue = StringUtils.EMPTY; + } + + LOG.debug("Add derived attribute {} with value {}", derAttr.getSchema().getKey(), expressionValue); + + context.set(derAttr.getSchema().getKey(), expressionValue); + } + } + + return context; + } + + public static JexlContext addVirAttrsToContext(final Collection virAttrs, + final JexlContext jexlContext) { + + JexlContext context = jexlContext == null + ? new MapContext() + : jexlContext; + + for (VirAttr virAttr : virAttrs) { + if (virAttr.getSchema() != null) { + List attrValues = virAttr.getValues(); + String expressionValue = attrValues.isEmpty() + ? StringUtils.EMPTY + : attrValues.get(0); + + LOG.debug("Add virtual attribute {} with value {}", virAttr.getSchema().getKey(), expressionValue); + + context.set(virAttr.getSchema().getKey(), expressionValue); + } + } + + return context; + } + + public static boolean evaluateMandatoryCondition( + final String mandatoryCondition, final Attributable attributable) { + + JexlContext jexlContext = new MapContext(); + addAttrsToContext(attributable.getPlainAttrs(), jexlContext); + addDerAttrsToContext(attributable.getDerAttrs(), attributable.getPlainAttrs(), jexlContext); + addVirAttrsToContext(attributable.getVirAttrs(), jexlContext); + + return Boolean.parseBoolean(evaluate(mandatoryCondition, jexlContext)); + } + + public static String evaluate(final String expression, + final Attributable attributable, final Collection attributes) { + + final JexlContext jexlContext = new MapContext(); + JexlUtil.addAttrsToContext(attributes, jexlContext); + JexlUtil.addFieldsToContext(attributable, jexlContext); + + // Evaluate expression using the context prepared before + return evaluate(expression, jexlContext); + } + + public static String evaluate(final String expression, final AbstractAttributableTO attributableTO) { + final JexlContext context = new MapContext(); + + addFieldsToContext(attributableTO, context); + + for (AttrTO plainAttr : attributableTO.getPlainAttrs()) { + List values = plainAttr.getValues(); + String expressionValue = values.isEmpty() + ? StringUtils.EMPTY + : values.get(0); + + LOG.debug("Add plain attribute {} with value {}", plainAttr.getSchema(), expressionValue); + + context.set(plainAttr.getSchema(), expressionValue); + } + for (AttrTO derAttr : attributableTO.getDerAttrs()) { + List values = derAttr.getValues(); + String expressionValue = values.isEmpty() + ? StringUtils.EMPTY + : values.get(0); + + LOG.debug("Add derived attribute {} with value {}", derAttr.getSchema(), expressionValue); + + context.set(derAttr.getSchema(), expressionValue); + } + for (AttrTO virAttr : attributableTO.getVirAttrs()) { + List values = virAttr.getValues(); + String expressionValue = values.isEmpty() + ? StringUtils.EMPTY + : values.get(0); + + LOG.debug("Add virtual attribute {} with value {}", virAttr.getSchema(), expressionValue); + + context.set(virAttr.getSchema(), expressionValue); + } + + // Evaluate expression using the context prepared before + return evaluate(expression, context); + } + + /** + * Private default constructor, for static-only classes. + */ + private JexlUtil() { + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/AccountPolicyEnforcer.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/AccountPolicyEnforcer.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/AccountPolicyEnforcer.java new file mode 100644 index 0000000..f850b98 --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/AccountPolicyEnforcer.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.policy; + +import java.util.regex.Pattern; +import org.apache.syncope.common.lib.types.AccountPolicySpec; +import org.apache.syncope.common.lib.types.PolicyType; +import org.apache.syncope.server.persistence.api.entity.user.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class AccountPolicyEnforcer implements PolicyEnforcer { + + private static final Pattern DEFAULT_PATTERN = Pattern.compile("[a-zA-Z0-9-_@. ]+"); + + @Autowired(required = false) + private UserSuspender userSuspender; + + @Override + public void enforce(final AccountPolicySpec policy, final PolicyType type, final User user) + throws AccountPolicyException, PolicyEnforceException { + + if (user.getUsername() == null) { + throw new PolicyEnforceException("Invalid account"); + } + + if (policy == null) { + throw new PolicyEnforceException("Invalid policy"); + } + + // check min length + if (policy.getMinLength() > 0 && policy.getMinLength() > user.getUsername().length()) { + throw new AccountPolicyException("Username too short"); + } + + // check max length + if (policy.getMaxLength() > 0 && policy.getMaxLength() < user.getUsername().length()) { + throw new AccountPolicyException("Username too long"); + } + + // check words not permitted + for (String word : policy.getWordsNotPermitted()) { + if (user.getUsername().contains(word)) { + throw new AccountPolicyException("Used word(s) not permitted"); + } + } + + // check case + if (policy.isAllUpperCase() && !user.getUsername().equals(user.getUsername().toUpperCase())) { + throw new AccountPolicyException("No lowercase characters permitted"); + } + if (policy.isAllLowerCase() && !user.getUsername().equals(user.getUsername().toLowerCase())) { + throw new AccountPolicyException("No uppercase characters permitted"); + } + + // check pattern + Pattern pattern = (policy.getPattern() == null) ? DEFAULT_PATTERN : Pattern.compile(policy.getPattern()); + if (!pattern.matcher(user.getUsername()).matches()) { + throw new AccountPolicyException("Username does not match pattern"); + } + + // check prefix + for (String prefix : policy.getPrefixesNotPermitted()) { + if (user.getUsername().startsWith(prefix)) { + throw new AccountPolicyException("Prefix not permitted"); + } + } + + // check suffix + for (String suffix : policy.getSuffixesNotPermitted()) { + if (user.getUsername().endsWith(suffix)) { + throw new AccountPolicyException("Suffix not permitted"); + } + } + + // check for subsequent failed logins + if (userSuspender != null + && user.getFailedLogins() != null && policy.getPermittedLoginRetries() > 0 + && user.getFailedLogins() > policy.getPermittedLoginRetries() && !user.isSuspended()) { + + userSuspender.suspend(user, policy.isPropagateSuspension()); + } + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/AccountPolicyException.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/AccountPolicyException.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/AccountPolicyException.java new file mode 100644 index 0000000..bfceecf --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/AccountPolicyException.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.policy; + +public class AccountPolicyException extends PolicyException { + + private static final long serialVersionUID = 2779416455067691813L; + + public AccountPolicyException() { + super(); + } + + public AccountPolicyException(final String message) { + super(message); + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/InvalidPasswordPolicySpecException.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/InvalidPasswordPolicySpecException.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/InvalidPasswordPolicySpecException.java new file mode 100644 index 0000000..dcd6d9a --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/InvalidPasswordPolicySpecException.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.policy; + +/** + * Raise when the merge of two or more PasswordPolicySpec leds to incompatible condition. + * + * @see org.apache.syncope.common.lib.types.PasswordPolicySpec + */ +public class InvalidPasswordPolicySpecException extends Exception { + + private static final long serialVersionUID = 4810651743226663580L; + + public InvalidPasswordPolicySpecException(final String msg) { + super(msg); + } + + public InvalidPasswordPolicySpecException(final String msg, final Exception e) { + super(msg, e); + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PasswordPolicyEnforcer.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PasswordPolicyEnforcer.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PasswordPolicyEnforcer.java new file mode 100644 index 0000000..ac8513d --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PasswordPolicyEnforcer.java @@ -0,0 +1,202 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.policy; + +import org.apache.syncope.common.lib.types.PasswordPolicySpec; +import org.apache.syncope.common.lib.types.PolicyType; +import org.apache.syncope.server.persistence.api.entity.user.User; +import org.springframework.stereotype.Component; + +@Component +public class PasswordPolicyEnforcer implements PolicyEnforcer { + + /* (non-Javadoc) + * @see + * org.apache.syncope.core.policy.PasswordPolicyEnforcer#enforce(org.apache.syncope.common.types.PasswordPolicySpec, + * org.apache.syncope.common.types.PolicyType, java.lang.String) + */ + @Override + public void enforce(final PasswordPolicySpec policy, final PolicyType type, final User user) + throws PasswordPolicyException, PolicyEnforceException { + + final String clearPassword = user.getClearPassword(); + final String password = user.getPassword(); + + if (policy == null) { + throw new PolicyEnforceException("Invalid policy"); + } + + if (password == null && !policy.isAllowNullPassword()) { + throw new PolicyEnforceException("Password must not be null and must be stored internally"); + } else if (password != null && clearPassword != null) { + // check length + if (policy.getMinLength() > 0 && policy.getMinLength() > clearPassword.length()) { + throw new PasswordPolicyException("Password too short"); + } + + if (policy.getMaxLength() > 0 && policy.getMaxLength() < clearPassword.length()) { + throw new PasswordPolicyException("Password too long"); + } + + // check words not permitted + for (String word : policy.getWordsNotPermitted()) { + if (clearPassword.contains(word)) { + throw new PasswordPolicyException("Used word(s) not permitted"); + } + } + + // check digits occurrence + if (policy.isDigitRequired() && !checkForDigit(clearPassword)) { + throw new PasswordPolicyException("Password must contain digit(s)"); + } + + // check lowercase alphabetic characters occurrence + if (policy.isLowercaseRequired() && !checkForLowercase(clearPassword)) { + throw new PasswordPolicyException("Password must contain lowercase alphabetic character(s)"); + } + + // check uppercase alphabetic characters occurrence + if (policy.isUppercaseRequired() && !checkForUppercase(clearPassword)) { + throw new PasswordPolicyException("Password must contain uppercase alphabetic character(s)"); + } + + // check prefix + for (String prefix : policy.getPrefixesNotPermitted()) { + if (clearPassword.startsWith(prefix)) { + throw new PasswordPolicyException("Prefix not permitted"); + } + } + + // check suffix + for (String suffix : policy.getSuffixesNotPermitted()) { + if (clearPassword.endsWith(suffix)) { + throw new PasswordPolicyException("Suffix not permitted"); + } + } + + // check digit first occurrence + if (policy.isMustStartWithDigit() && !checkForFirstDigit(clearPassword)) { + throw new PasswordPolicyException("Password must start with a digit"); + } + + if (policy.isMustntStartWithDigit() && checkForFirstDigit(clearPassword)) { + throw new PasswordPolicyException("Password mustn't start with a digit"); + } + + // check digit last occurrence + if (policy.isMustEndWithDigit() && !checkForLastDigit(clearPassword)) { + throw new PasswordPolicyException("Password must end with a digit"); + } + + if (policy.isMustntEndWithDigit() && checkForLastDigit(clearPassword)) { + throw new PasswordPolicyException("Password mustn't end with a digit"); + } + + // check alphanumeric characters occurence + if (policy.isAlphanumericRequired() && !checkForAlphanumeric(clearPassword)) { + throw new PasswordPolicyException("Password must contain alphanumeric character(s)"); + } + + // check non alphanumeric characters occurence + if (policy.isNonAlphanumericRequired() && !checkForNonAlphanumeric(clearPassword)) { + throw new PasswordPolicyException("Password must contain non-alphanumeric character(s)"); + } + + // check alphanumeric character first occurrence + if (policy.isMustStartWithAlpha() && !checkForFirstAlphanumeric(clearPassword)) { + throw new PasswordPolicyException("Password must start with an alphanumeric character"); + } + + if (policy.isMustntStartWithAlpha() && checkForFirstAlphanumeric(clearPassword)) { + throw new PasswordPolicyException("Password mustn't start with an alphanumeric character"); + } + + // check alphanumeric character last occurrence + if (policy.isMustEndWithAlpha() && !checkForLastAlphanumeric(clearPassword)) { + throw new PasswordPolicyException("Password must end with an alphanumeric character"); + } + + if (policy.isMustntEndWithAlpha() && checkForLastAlphanumeric(clearPassword)) { + throw new PasswordPolicyException("Password mustn't end with an alphanumeric character"); + } + + // check non alphanumeric character first occurrence + if (policy.isMustStartWithNonAlpha() && !checkForFirstNonAlphanumeric(clearPassword)) { + throw new PasswordPolicyException("Password must start with a non-alphanumeric character"); + } + + if (policy.isMustntStartWithNonAlpha() && checkForFirstNonAlphanumeric(clearPassword)) { + throw new PasswordPolicyException("Password mustn't start with a non-alphanumeric character"); + } + + // check non alphanumeric character last occurrence + if (policy.isMustEndWithNonAlpha() && !checkForLastNonAlphanumeric(clearPassword)) { + throw new PasswordPolicyException("Password must end with a non-alphanumeric character"); + } + + if (policy.isMustntEndWithNonAlpha() && checkForLastNonAlphanumeric(clearPassword)) { + throw new PasswordPolicyException("Password mustn't end with a non-alphanumeric character"); + } + } + } + + private boolean checkForDigit(final String str) { + return PolicyPattern.DIGIT.matcher(str).matches(); + } + + private boolean checkForLowercase(final String str) { + return PolicyPattern.ALPHA_LOWERCASE.matcher(str).matches(); + } + + private boolean checkForUppercase(final String str) { + return PolicyPattern.ALPHA_UPPERCASE.matcher(str).matches(); + } + + private boolean checkForFirstDigit(final String str) { + return PolicyPattern.FIRST_DIGIT.matcher(str).matches(); + } + + private boolean checkForLastDigit(final String str) { + return PolicyPattern.LAST_DIGIT.matcher(str).matches(); + } + + private boolean checkForAlphanumeric(final String str) { + return PolicyPattern.ALPHANUMERIC.matcher(str).matches(); + } + + private boolean checkForFirstAlphanumeric(final String str) { + return PolicyPattern.FIRST_ALPHANUMERIC.matcher(str).matches(); + } + + private boolean checkForLastAlphanumeric(final String str) { + return PolicyPattern.LAST_ALPHANUMERIC.matcher(str).matches(); + } + + private boolean checkForNonAlphanumeric(final String str) { + return PolicyPattern.NON_ALPHANUMERIC.matcher(str).matches(); + } + + private boolean checkForFirstNonAlphanumeric(final String str) { + return PolicyPattern.FIRST_NON_ALPHANUMERIC.matcher(str).matches(); + } + + private boolean checkForLastNonAlphanumeric(final String str) { + return PolicyPattern.LAST_NON_ALPHANUMERIC.matcher(str).matches(); + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PasswordPolicyException.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PasswordPolicyException.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PasswordPolicyException.java new file mode 100644 index 0000000..72534ba --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PasswordPolicyException.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.policy; + +public class PasswordPolicyException extends PolicyException { + + private static final long serialVersionUID = 8072104484395278469L; + + public PasswordPolicyException() { + super(); + } + + public PasswordPolicyException(final String message) { + super(message); + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyEnforceException.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyEnforceException.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyEnforceException.java new file mode 100644 index 0000000..7c33172 --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyEnforceException.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.policy; + +public class PolicyEnforceException extends PolicyException { + + private static final long serialVersionUID = 3247084727383061069L; + + public PolicyEnforceException() { + super(); + } + + public PolicyEnforceException(final String message) { + super(message); + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyEnforcer.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyEnforcer.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyEnforcer.java new file mode 100644 index 0000000..ba8e7e1 --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyEnforcer.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.policy; + +import java.io.InvalidObjectException; + +import org.apache.syncope.common.lib.types.PolicySpec; +import org.apache.syncope.common.lib.types.PolicyType; + +public interface PolicyEnforcer { + + void enforce(final T policy, final PolicyType type, final E object) + throws InvalidObjectException, PolicyException; +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyEvaluator.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyEvaluator.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyEvaluator.java new file mode 100644 index 0000000..21dde11 --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyEvaluator.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.policy; + +import java.util.List; +import org.apache.syncope.common.lib.types.AccountPolicySpec; +import org.apache.syncope.common.lib.types.PasswordPolicySpec; +import org.apache.syncope.common.lib.types.PolicySpec; +import org.apache.syncope.server.persistence.api.entity.Attributable; +import org.apache.syncope.server.persistence.api.entity.PlainAttr; +import org.apache.syncope.server.persistence.api.entity.Policy; +import org.apache.syncope.server.persistence.api.entity.user.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Component; + +@Component +public class PolicyEvaluator { + + /** + * Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(PolicyEvaluator.class); + + @SuppressWarnings("unchecked") + public T evaluate(final Policy policy, final Attributable attributable) { + if (policy == null) { + return null; + } + + T result = null; + switch (policy.getType()) { + case PASSWORD: + case GLOBAL_PASSWORD: + final PasswordPolicySpec ppSpec = policy.getSpecification(PasswordPolicySpec.class); + final PasswordPolicySpec evaluatedPPSpec = new PasswordPolicySpec(); + + BeanUtils.copyProperties(ppSpec, evaluatedPPSpec, new String[] { "schemasNotPermitted" }); + + for (String schema : ppSpec.getSchemasNotPermitted()) { + PlainAttr attr = attributable.getPlainAttr(schema); + if (attr != null) { + List values = attr.getValuesAsStrings(); + if (values != null && !values.isEmpty()) { + evaluatedPPSpec.getWordsNotPermitted().add(values.get(0)); + } + } + } + + // Password history verification and update + if (!(attributable instanceof User)) { + LOG.error("Cannot check previous passwords. attributable is not a user object: {}", + attributable.getClass().getName()); + result = (T) evaluatedPPSpec; + break; + } + User user = (User) attributable; + if (user.verifyPasswordHistory(user.getClearPassword(), ppSpec.getHistoryLength())) { + evaluatedPPSpec.getWordsNotPermitted().add(user.getClearPassword()); + } + result = (T) evaluatedPPSpec; + break; + + case ACCOUNT: + case GLOBAL_ACCOUNT: + final AccountPolicySpec spec = policy.getSpecification(AccountPolicySpec.class); + final AccountPolicySpec accountPolicy = new AccountPolicySpec(); + + BeanUtils.copyProperties(spec, accountPolicy, new String[] { "schemasNotPermitted" }); + + for (String schema : spec.getSchemasNotPermitted()) { + PlainAttr attr = attributable.getPlainAttr(schema); + if (attr != null) { + List values = attr.getValuesAsStrings(); + if (values != null && !values.isEmpty()) { + accountPolicy.getWordsNotPermitted().add(values.get(0)); + } + } + } + + result = (T) accountPolicy; + break; + + case SYNC: + case GLOBAL_SYNC: + default: + result = null; + } + + return result; + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyException.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyException.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyException.java new file mode 100644 index 0000000..ca6723c --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyException.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.policy; + +public class PolicyException extends RuntimeException { + + private static final long serialVersionUID = -6082115004491662910L; + + public PolicyException() { + super(); + } + + public PolicyException(final String message) { + super(message); + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyPattern.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyPattern.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyPattern.java new file mode 100644 index 0000000..409e6e9 --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/PolicyPattern.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.policy; + +import java.util.regex.Pattern; + +public final class PolicyPattern { + + public static final Pattern DIGIT = Pattern.compile(".*\\d+.*"); + + public static final Pattern ALPHA_LOWERCASE = Pattern.compile(".*[a-z]+.*"); + + public static final Pattern ALPHA_UPPERCASE = Pattern.compile(".*[A-Z]+.*"); + + public static final Pattern FIRST_DIGIT = Pattern.compile("\\d.*"); + + public static final Pattern LAST_DIGIT = Pattern.compile(".*\\d"); + + public static final Pattern ALPHANUMERIC = Pattern.compile(".*\\w.*"); + + public static final Pattern FIRST_ALPHANUMERIC = Pattern.compile("\\w.*"); + + public static final Pattern LAST_ALPHANUMERIC = Pattern.compile(".*\\w"); + + public static final Pattern NON_ALPHANUMERIC = Pattern.compile(".*\\W.*"); + + public static final Pattern FIRST_NON_ALPHANUMERIC = Pattern.compile("\\W.*"); + + public static final Pattern LAST_NON_ALPHANUMERIC = Pattern.compile(".*\\W"); + + private PolicyPattern() { + // private constructor for static utility class + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/UserSuspender.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/UserSuspender.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/UserSuspender.java new file mode 100644 index 0000000..1fbf6f6 --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/policy/UserSuspender.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.policy; + +import org.apache.syncope.server.persistence.api.entity.user.User; + +public interface UserSuspender { + + void suspend(User user, boolean propagateSuspension); +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/search/SearchCondConverter.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/search/SearchCondConverter.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/search/SearchCondConverter.java new file mode 100644 index 0000000..27e0d4a --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/search/SearchCondConverter.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.search; + +import org.apache.cxf.jaxrs.ext.search.SearchBean; +import org.apache.cxf.jaxrs.ext.search.fiql.FiqlParser; +import org.apache.syncope.common.lib.search.SyncopeFiqlSearchConditionBuilder; +import org.apache.syncope.server.persistence.api.dao.search.SearchCond; + +/** + * Converts FIQL expressions to Syncope's SearchCond. + */ +public final class SearchCondConverter { + + /** + * Parses a FIQL expression into Syncope's SearchCond, using CXF's FiqlParser. + * + * @param fiqlExpression FIQL string + * @return SearchCond instance for given FIQL expression + * @see FiqlParser + */ + public static SearchCond convert(final String fiqlExpression) { + FiqlParser fiqlParser = new FiqlParser( + SearchBean.class, SyncopeFiqlSearchConditionBuilder.CONTEXTUAL_PROPERTIES); + SearchCondVisitor searchCondVisitor = new SearchCondVisitor(); + + searchCondVisitor.visit(fiqlParser.parse(fiqlExpression)); + return searchCondVisitor.getQuery(); + } + + private SearchCondConverter() { + // empty constructor for static utility class + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/search/SearchCondVisitor.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/search/SearchCondVisitor.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/search/SearchCondVisitor.java new file mode 100644 index 0000000..baa96f4 --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/search/SearchCondVisitor.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.search; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.cxf.jaxrs.ext.search.ConditionType; +import org.apache.cxf.jaxrs.ext.search.SearchBean; +import org.apache.cxf.jaxrs.ext.search.SearchCondition; +import org.apache.cxf.jaxrs.ext.search.SearchUtils; +import org.apache.cxf.jaxrs.ext.search.visitor.AbstractSearchConditionVisitor; +import org.apache.syncope.common.lib.search.SearchableFields; +import org.apache.syncope.common.lib.search.SpecialAttr; +import org.apache.syncope.common.lib.to.RoleTO; +import org.apache.syncope.common.lib.to.UserTO; +import org.apache.syncope.server.persistence.api.dao.search.AttributeCond; +import org.apache.syncope.server.persistence.api.dao.search.EntitlementCond; +import org.apache.syncope.server.persistence.api.dao.search.MembershipCond; +import org.apache.syncope.server.persistence.api.dao.search.ResourceCond; +import org.apache.syncope.server.persistence.api.dao.search.SearchCond; +import org.apache.syncope.server.persistence.api.dao.search.SubjectCond; + +/** + * Converts CXF's SearchCondition into internal SearchCond. + */ +public class SearchCondVisitor extends AbstractSearchConditionVisitor { + + private static final List ATTRIBUTABLE_FIELDS; + + static { + ATTRIBUTABLE_FIELDS = new ArrayList(); + ATTRIBUTABLE_FIELDS.addAll(SearchableFields.get(UserTO.class)); + ATTRIBUTABLE_FIELDS.addAll(SearchableFields.get(RoleTO.class)); + } + + private SearchCond searchCond; + + public SearchCondVisitor() { + super(null); + } + + public SearchCondVisitor(final Map fieldMap) { + super(fieldMap); + } + + private AttributeCond createAttributeCond(final String schema) { + AttributeCond attributeCond = ATTRIBUTABLE_FIELDS.contains(schema) + ? new SubjectCond() + : new AttributeCond(); + attributeCond.setSchema(schema); + return attributeCond; + } + + private SearchCond visitPrimitive(final SearchCondition sc) { + String name = getRealPropertyName(sc.getStatement().getProperty()); + SpecialAttr specialAttrName = SpecialAttr.fromString(name); + + String value = SearchUtils.toSqlWildcardString(sc.getStatement().getValue().toString(), false). + replaceAll("\\\\_", "_"); + SpecialAttr specialAttrValue = SpecialAttr.fromString(value); + + AttributeCond attributeCond = createAttributeCond(name); + attributeCond.setExpression(value); + + SearchCond leaf; + switch (sc.getConditionType()) { + case EQUALS: + case NOT_EQUALS: + if (specialAttrName == null) { + if (specialAttrValue != null && specialAttrValue == SpecialAttr.NULL) { + attributeCond.setType(AttributeCond.Type.ISNULL); + attributeCond.setExpression(null); + } else if (value.indexOf('%') == -1) { + attributeCond.setType(AttributeCond.Type.EQ); + } else { + attributeCond.setType(AttributeCond.Type.LIKE); + } + + leaf = SearchCond.getLeafCond(attributeCond); + } else { + switch (specialAttrName) { + case ROLES: + MembershipCond membershipCond = new MembershipCond(); + membershipCond.setRoleId(Long.valueOf(value)); + leaf = SearchCond.getLeafCond(membershipCond); + break; + + case RESOURCES: + ResourceCond resourceCond = new ResourceCond(); + resourceCond.setResourceName(value); + leaf = SearchCond.getLeafCond(resourceCond); + break; + + case ENTITLEMENTS: + EntitlementCond entitlementCond = new EntitlementCond(); + entitlementCond.setExpression(value); + leaf = SearchCond.getLeafCond(entitlementCond); + break; + + default: + throw new IllegalArgumentException( + String.format("Special attr name %s is not supported", specialAttrName)); + } + } + if (sc.getConditionType() == ConditionType.NOT_EQUALS) { + if (leaf.getAttributeCond() != null + && leaf.getAttributeCond().getType() == AttributeCond.Type.ISNULL) { + + leaf.getAttributeCond().setType(AttributeCond.Type.ISNOTNULL); + } else if (leaf.getSubjectCond() != null + && leaf.getSubjectCond().getType() == SubjectCond.Type.ISNULL) { + + leaf.getSubjectCond().setType(AttributeCond.Type.ISNOTNULL); + } else { + leaf = SearchCond.getNotLeafCond(leaf); + } + } + break; + + case GREATER_OR_EQUALS: + attributeCond.setType(AttributeCond.Type.GE); + leaf = SearchCond.getLeafCond(attributeCond); + break; + + case GREATER_THAN: + attributeCond.setType(AttributeCond.Type.GT); + leaf = SearchCond.getLeafCond(attributeCond); + break; + + case LESS_OR_EQUALS: + attributeCond.setType(AttributeCond.Type.LE); + leaf = SearchCond.getLeafCond(attributeCond); + break; + + case LESS_THAN: + attributeCond.setType(AttributeCond.Type.LT); + leaf = SearchCond.getLeafCond(attributeCond); + break; + + default: + throw new IllegalArgumentException( + String.format("Condition type %s is not supported", sc.getConditionType().name())); + } + + return leaf; + } + + private SearchCond visitCompount(final SearchCondition sc) { + List searchConds = new ArrayList(); + for (SearchCondition searchCondition : sc.getSearchConditions()) { + searchConds.add(searchCondition.getStatement() == null + ? visitCompount(searchCondition) + : visitPrimitive(searchCondition)); + } + + SearchCond compound; + switch (sc.getConditionType()) { + case AND: + compound = SearchCond.getAndCond(searchConds); + break; + + case OR: + compound = SearchCond.getOrCond(searchConds); + break; + + default: + throw new IllegalArgumentException( + String.format("Condition type %s is not supported", sc.getConditionType().name())); + } + + return compound; + } + + @Override + public void visit(final SearchCondition sc) { + searchCond = sc.getStatement() == null + ? visitCompount(sc) + : visitPrimitive(sc); + } + + @Override + public SearchCond getQuery() { + return searchCond; + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/security/AuthContextUtil.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/security/AuthContextUtil.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/security/AuthContextUtil.java new file mode 100644 index 0000000..e58a157 --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/security/AuthContextUtil.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.security; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.syncope.common.lib.SyncopeConstants; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +public final class AuthContextUtil { + + public static String getAuthenticatedUsername() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication == null ? SyncopeConstants.UNAUTHENTICATED : authentication.getName(); + } + + public static Set getOwnedEntitlementNames() { + final Set result = new HashSet<>(); + + final SecurityContext ctx = SecurityContextHolder.getContext(); + + if (ctx != null && ctx.getAuthentication() != null && ctx.getAuthentication().getAuthorities() != null) { + for (GrantedAuthority authority : ctx.getAuthentication().getAuthorities()) { + result.add(authority.getAuthority()); + } + } + + return result; + } + + /** + * Extend the current authentication context to include the given role. + * + * @param roleKey role key + * @param roleEntitlement role entitlement + */ + public static void extendAuthContext(final Long roleKey, final String roleEntitlement) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + List authorities = new ArrayList(auth.getAuthorities()); + authorities.add(new SimpleGrantedAuthority(roleEntitlement)); + Authentication newAuth = new UsernamePasswordAuthenticationToken( + auth.getPrincipal(), auth.getCredentials(), authorities); + SecurityContextHolder.getContext().setAuthentication(newAuth); + } + + /** + * Private default constructor, for static-only classes. + */ + private AuthContextUtil() { + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/security/Encryptor.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/security/Encryptor.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/security/Encryptor.java new file mode 100644 index 0000000..6963472 --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/security/Encryptor.java @@ -0,0 +1,256 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.security; + +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.common.lib.types.CipherAlgorithm; +import org.jasypt.commons.CommonUtils; +import org.jasypt.digest.StandardStringDigester; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.crypto.bcrypt.BCrypt; +import org.springframework.security.crypto.codec.Base64; + +public final class Encryptor { + + private static final Logger LOG = LoggerFactory.getLogger(Encryptor.class); + + private static final Map INSTANCES = new ConcurrentHashMap(); + + private static final String DEFAULT_SECRET_KEY = "1abcdefghilmnopqrstuvz2!"; + + /** + * Default value for salted {@link StandardStringDigester#setIterations(int)}. + */ + private static final int DEFAULT_SALT_ITERATIONS = 1; + + /** + * Default value for {@link StandardStringDigester#setSaltSizeBytes(int)}. + */ + private static final int DEFAULT_SALT_SIZE_BYTES = 8; + + /** + * Default value for {@link StandardStringDigester#setInvertPositionOfPlainSaltInEncryptionResults(boolean)}. + */ + private static final boolean DEFAULT_IPOPSIER = true; + + /** + * Default value for salted {@link StandardStringDigester#setInvertPositionOfSaltInMessageBeforeDigesting(boolean)}. + */ + private static final boolean DEFAULT_IPOSIMBD = true; + + /** + * Default value for salted {@link StandardStringDigester#setUseLenientSaltSizeCheck(boolean)}. + */ + private static final boolean DEFAULT_ULSSC = true; + + private static String secretKey; + + private static Integer saltIterations; + + private static Integer saltSizeBytes; + + private static Boolean ipopsier; + + private static Boolean iposimbd; + + private static Boolean ulssc; + + static { + InputStream propStream = null; + try { + propStream = Encryptor.class.getResourceAsStream("/security.properties"); + Properties props = new Properties(); + props.load(propStream); + + secretKey = props.getProperty("secretKey"); + saltIterations = Integer.valueOf(props.getProperty("digester.saltIterations")); + saltSizeBytes = Integer.valueOf(props.getProperty("digester.saltSizeBytes")); + ipopsier = Boolean.valueOf(props.getProperty("digester.invertPositionOfPlainSaltInEncryptionResults")); + iposimbd = Boolean.valueOf(props.getProperty("digester.invertPositionOfSaltInMessageBeforeDigesting")); + ulssc = Boolean.valueOf(props.getProperty("digester.useLenientSaltSizeCheck")); + } catch (Exception e) { + LOG.error("Could not read security parameters", e); + } finally { + IOUtils.closeQuietly(propStream); + } + + if (secretKey == null) { + secretKey = DEFAULT_SECRET_KEY; + LOG.debug("secretKey not found, reverting to default"); + } + if (saltIterations == null) { + saltIterations = DEFAULT_SALT_ITERATIONS; + LOG.debug("digester.saltIterations not found, reverting to default"); + } + if (saltSizeBytes == null) { + saltSizeBytes = DEFAULT_SALT_SIZE_BYTES; + LOG.debug("digester.saltSizeBytes not found, reverting to default"); + } + if (ipopsier == null) { + ipopsier = DEFAULT_IPOPSIER; + LOG.debug("digester.invertPositionOfPlainSaltInEncryptionResults not found, reverting to default"); + } + if (iposimbd == null) { + iposimbd = DEFAULT_IPOSIMBD; + LOG.debug("digester.invertPositionOfSaltInMessageBeforeDigesting not found, reverting to default"); + } + if (ulssc == null) { + ulssc = DEFAULT_ULSSC; + LOG.debug("digester.useLenientSaltSizeCheck not found, reverting to default"); + } + } + + public static Encryptor getInstance() { + return getInstance(secretKey); + } + + public static Encryptor getInstance(final String secretKey) { + String actualKey = StringUtils.isBlank(secretKey) ? DEFAULT_SECRET_KEY : secretKey; + + Encryptor instance = INSTANCES.get(actualKey); + if (instance == null) { + instance = new Encryptor(actualKey); + INSTANCES.put(actualKey, instance); + } + + return instance; + } + + private SecretKeySpec keySpec; + + private Encryptor(final String secretKey) { + String actualKey = secretKey; + if (actualKey.length() < 16) { + StringBuilder actualKeyPadding = new StringBuilder(actualKey); + for (int i = 0; i < 16 - actualKey.length(); i++) { + actualKeyPadding.append('0'); + } + actualKey = actualKeyPadding.toString(); + LOG.debug("actualKey too short, adding some random characters"); + } + + try { + keySpec = new SecretKeySpec(ArrayUtils.subarray( + actualKey.getBytes(SyncopeConstants.DEFAULT_ENCODING), 0, 16), + CipherAlgorithm.AES.getAlgorithm()); + } catch (Exception e) { + LOG.error("Error during key specification", e); + } + } + + public String encode(final String value, final CipherAlgorithm cipherAlgorithm) + throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException { + + String encodedValue = null; + + if (value != null) { + if (cipherAlgorithm == null || cipherAlgorithm == CipherAlgorithm.AES) { + final byte[] cleartext = value.getBytes(SyncopeConstants.DEFAULT_ENCODING); + + final Cipher cipher = Cipher.getInstance(CipherAlgorithm.AES.getAlgorithm()); + cipher.init(Cipher.ENCRYPT_MODE, keySpec); + + encodedValue = new String(Base64.encode(cipher.doFinal(cleartext))); + } else if (cipherAlgorithm == CipherAlgorithm.BCRYPT) { + encodedValue = BCrypt.hashpw(value, BCrypt.gensalt()); + } else { + encodedValue = getDigester(cipherAlgorithm).digest(value); + } + } + + return encodedValue; + } + + public boolean verify(final String value, final CipherAlgorithm cipherAlgorithm, final String encodedValue) { + boolean res = false; + + try { + if (value != null) { + if (cipherAlgorithm == null || cipherAlgorithm == CipherAlgorithm.AES) { + res = encode(value, cipherAlgorithm).equals(encodedValue); + } else if (cipherAlgorithm == CipherAlgorithm.BCRYPT) { + res = BCrypt.checkpw(value, encodedValue); + } else { + res = getDigester(cipherAlgorithm).matches(value, encodedValue); + } + } + } catch (Exception e) { + LOG.error("Could not verify encoded value", e); + } + + return res; + } + + public String decode(final String encodedValue, final CipherAlgorithm cipherAlgorithm) + throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException { + + String value = null; + + if (encodedValue != null && cipherAlgorithm == CipherAlgorithm.AES) { + final byte[] encoded = encodedValue.getBytes(SyncopeConstants.DEFAULT_ENCODING); + + final Cipher cipher = Cipher.getInstance(CipherAlgorithm.AES.getAlgorithm()); + cipher.init(Cipher.DECRYPT_MODE, keySpec); + + value = new String(cipher.doFinal(Base64.decode(encoded)), SyncopeConstants.DEFAULT_ENCODING); + } + + return value; + } + + private StandardStringDigester getDigester(final CipherAlgorithm cipherAlgorithm) { + StandardStringDigester digester = new StandardStringDigester(); + + if (cipherAlgorithm.getAlgorithm().startsWith("S-")) { + // Salted ... + digester.setAlgorithm(cipherAlgorithm.getAlgorithm().replaceFirst("S\\-", "")); + digester.setIterations(saltIterations); + digester.setSaltSizeBytes(saltSizeBytes); + digester.setInvertPositionOfPlainSaltInEncryptionResults(ipopsier); + digester.setInvertPositionOfSaltInMessageBeforeDigesting(iposimbd); + digester.setUseLenientSaltSizeCheck(ulssc); + } else { + // Not salted ... + digester.setAlgorithm(cipherAlgorithm.getAlgorithm()); + digester.setIterations(1); + digester.setSaltSizeBytes(0); + } + + digester.setStringOutputType(CommonUtils.STRING_OUTPUT_TYPE_HEXADECIMAL); + return digester; + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/235f60fa/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/security/PasswordGenerator.java ---------------------------------------------------------------------- diff --git a/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/security/PasswordGenerator.java b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/security/PasswordGenerator.java new file mode 100644 index 0000000..3712ae5 --- /dev/null +++ b/syncope620/server/misc/src/main/java/org/apache/syncope/server/misc/security/PasswordGenerator.java @@ -0,0 +1,321 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.server.misc.security; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.common.lib.types.PasswordPolicySpec; +import org.apache.syncope.server.persistence.api.dao.PolicyDAO; +import org.apache.syncope.server.persistence.api.entity.ExternalResource; +import org.apache.syncope.server.persistence.api.entity.PasswordPolicy; +import org.apache.syncope.server.persistence.api.entity.role.Role; +import org.apache.syncope.server.persistence.api.entity.user.User; +import org.apache.syncope.server.misc.policy.InvalidPasswordPolicySpecException; +import org.apache.syncope.server.misc.policy.PolicyPattern; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Generate random passwords according to given policies. + * + * @see PasswordPolicy + */ +@Component +public class PasswordGenerator { + + private static final char[] SPECIAL_CHARS = { '!', '£', '%', '&', '(', ')', '?', '#', '$' }; + + @Autowired + private PolicyDAO policyDAO; + + public String generate(final List ppSpecs) + throws InvalidPasswordPolicySpecException { + + PasswordPolicySpec policySpec = merge(ppSpecs); + + check(policySpec); + + return generate(policySpec); + } + + public String generate(final User user) + throws InvalidPasswordPolicySpecException { + + List ppSpecs = new ArrayList(); + + PasswordPolicy globalPP = policyDAO.getGlobalPasswordPolicy(); + if (globalPP != null && globalPP.getSpecification(PasswordPolicySpec.class) != null) { + ppSpecs.add(globalPP.getSpecification(PasswordPolicySpec.class)); + } + + for (Role role : user.getRoles()) { + if (role.getPasswordPolicy() != null + && role.getPasswordPolicy().getSpecification(PasswordPolicySpec.class) != null) { + + ppSpecs.add(role.getPasswordPolicy().getSpecification(PasswordPolicySpec.class)); + } + } + + for (ExternalResource resource : user.getResources()) { + if (resource.getPasswordPolicy() != null + && resource.getPasswordPolicy().getSpecification(PasswordPolicySpec.class) != null) { + + ppSpecs.add(resource.getPasswordPolicy().getSpecification(PasswordPolicySpec.class)); + } + } + + PasswordPolicySpec policySpec = merge(ppSpecs); + check(policySpec); + return generate(policySpec); + } + + private PasswordPolicySpec merge(final List ppSpecs) { + PasswordPolicySpec fpps = new PasswordPolicySpec(); + fpps.setMinLength(0); + fpps.setMaxLength(1000); + + for (PasswordPolicySpec policySpec : ppSpecs) { + if (policySpec.getMinLength() > fpps.getMinLength()) { + fpps.setMinLength(policySpec.getMinLength()); + } + + if ((policySpec.getMaxLength() != 0) && ((policySpec.getMaxLength() < fpps.getMaxLength()))) { + fpps.setMaxLength(policySpec.getMaxLength()); + } + fpps.getPrefixesNotPermitted().addAll(policySpec.getPrefixesNotPermitted()); + fpps.getSuffixesNotPermitted().addAll(policySpec.getSuffixesNotPermitted()); + + if (!fpps.isNonAlphanumericRequired()) { + fpps.setNonAlphanumericRequired(policySpec.isNonAlphanumericRequired()); + } + + if (!fpps.isAlphanumericRequired()) { + fpps.setAlphanumericRequired(policySpec.isAlphanumericRequired()); + } + if (!fpps.isDigitRequired()) { + fpps.setDigitRequired(policySpec.isDigitRequired()); + } + + if (!fpps.isLowercaseRequired()) { + fpps.setLowercaseRequired(policySpec.isLowercaseRequired()); + } + if (!fpps.isUppercaseRequired()) { + fpps.setUppercaseRequired(policySpec.isUppercaseRequired()); + } + if (!fpps.isMustStartWithDigit()) { + fpps.setMustStartWithDigit(policySpec.isMustStartWithDigit()); + } + if (!fpps.isMustntStartWithDigit()) { + fpps.setMustntStartWithDigit(policySpec.isMustntStartWithDigit()); + } + if (!fpps.isMustEndWithDigit()) { + fpps.setMustEndWithDigit(policySpec.isMustEndWithDigit()); + } + if (fpps.isMustntEndWithDigit()) { + fpps.setMustntEndWithDigit(policySpec.isMustntEndWithDigit()); + } + if (!fpps.isMustStartWithAlpha()) { + fpps.setMustStartWithAlpha(policySpec.isMustStartWithAlpha()); + } + if (!fpps.isMustntStartWithAlpha()) { + fpps.setMustntStartWithAlpha(policySpec.isMustntStartWithAlpha()); + } + if (!fpps.isMustStartWithNonAlpha()) { + fpps.setMustStartWithNonAlpha(policySpec.isMustStartWithNonAlpha()); + } + if (!fpps.isMustntStartWithNonAlpha()) { + fpps.setMustntStartWithNonAlpha(policySpec.isMustntStartWithNonAlpha()); + } + if (!fpps.isMustEndWithNonAlpha()) { + fpps.setMustEndWithNonAlpha(policySpec.isMustEndWithNonAlpha()); + } + if (!fpps.isMustntEndWithNonAlpha()) { + fpps.setMustntEndWithNonAlpha(policySpec.isMustntEndWithNonAlpha()); + } + if (!fpps.isMustEndWithAlpha()) { + fpps.setMustEndWithAlpha(policySpec.isMustEndWithAlpha()); + } + if (!fpps.isMustntEndWithAlpha()) { + fpps.setMustntEndWithAlpha(policySpec.isMustntEndWithAlpha()); + } + } + return fpps; + } + + private void check(final PasswordPolicySpec policySpec) + throws InvalidPasswordPolicySpecException { + + if (policySpec.getMinLength() == 0) { + throw new InvalidPasswordPolicySpecException("Minimum length is zero"); + } + if (policySpec.isMustEndWithAlpha() && policySpec.isMustntEndWithAlpha()) { + throw new InvalidPasswordPolicySpecException( + "mustEndWithAlpha and mustntEndWithAlpha are both true"); + } + if (policySpec.isMustEndWithAlpha() && policySpec.isMustEndWithDigit()) { + throw new InvalidPasswordPolicySpecException( + "mustEndWithAlpha and mustEndWithDigit are both true"); + } + if (policySpec.isMustEndWithDigit() && policySpec.isMustntEndWithDigit()) { + throw new InvalidPasswordPolicySpecException( + "mustEndWithDigit and mustntEndWithDigit are both true"); + } + if (policySpec.isMustEndWithNonAlpha() && policySpec.isMustntEndWithNonAlpha()) { + throw new InvalidPasswordPolicySpecException( + "mustEndWithNonAlpha and mustntEndWithNonAlpha are both true"); + } + if (policySpec.isMustStartWithAlpha() && policySpec.isMustntStartWithAlpha()) { + throw new InvalidPasswordPolicySpecException( + "mustStartWithAlpha and mustntStartWithAlpha are both true"); + } + if (policySpec.isMustStartWithAlpha() && policySpec.isMustStartWithDigit()) { + throw new InvalidPasswordPolicySpecException( + "mustStartWithAlpha and mustStartWithDigit are both true"); + } + if (policySpec.isMustStartWithDigit() && policySpec.isMustntStartWithDigit()) { + throw new InvalidPasswordPolicySpecException( + "mustStartWithDigit and mustntStartWithDigit are both true"); + } + if (policySpec.isMustStartWithNonAlpha() && policySpec.isMustntStartWithNonAlpha()) { + throw new InvalidPasswordPolicySpecException( + "mustStartWithNonAlpha and mustntStartWithNonAlpha are both true"); + } + if (policySpec.getMinLength() > policySpec.getMaxLength()) { + throw new InvalidPasswordPolicySpecException("Minimun length (" + policySpec.getMinLength() + ")" + + "is greater than maximum length (" + policySpec.getMaxLength() + ")"); + } + } + + private String generate(final PasswordPolicySpec policySpec) { + String[] generatedPassword = new String[policySpec.getMinLength()]; + + for (int i = 0; i < generatedPassword.length; i++) { + generatedPassword[i] = ""; + } + + checkStartChar(generatedPassword, policySpec); + + checkEndChar(generatedPassword, policySpec); + + checkRequired(generatedPassword, policySpec); + + //filled empty chars + for (int firstEmptyChar = firstEmptyChar(generatedPassword); + firstEmptyChar < generatedPassword.length - 1; firstEmptyChar++) { + generatedPassword[firstEmptyChar] = SecureRandomUtil.generateRandomLetter(); + } + + checkPrefixAndSuffix(generatedPassword, policySpec); + + return StringUtils.join(generatedPassword); + } + + private void checkStartChar(final String[] generatedPassword, final PasswordPolicySpec policySpec) { + if (policySpec.isMustStartWithAlpha()) { + generatedPassword[0] = SecureRandomUtil.generateRandomLetter(); + } + if (policySpec.isMustStartWithNonAlpha() || policySpec.isMustStartWithDigit()) { + generatedPassword[0] = SecureRandomUtil.generateRandomNumber(); + } + if (policySpec.isMustntStartWithAlpha()) { + generatedPassword[0] = SecureRandomUtil.generateRandomNumber(); + + } + if (policySpec.isMustntStartWithDigit()) { + generatedPassword[0] = SecureRandomUtil.generateRandomLetter(); + + } + if (policySpec.isMustntStartWithNonAlpha()) { + generatedPassword[0] = SecureRandomUtil.generateRandomLetter(); + + } + } + + private void checkEndChar(final String[] generatedPassword, final PasswordPolicySpec policySpec) { + if (policySpec.isMustEndWithAlpha()) { + generatedPassword[policySpec.getMinLength() - 1] = SecureRandomUtil.generateRandomLetter(); + } + if (policySpec.isMustEndWithNonAlpha() || policySpec.isMustEndWithDigit()) { + generatedPassword[policySpec.getMinLength() - 1] = SecureRandomUtil.generateRandomNumber(); + } + + if (policySpec.isMustntEndWithAlpha()) { + generatedPassword[policySpec.getMinLength() - 1] = SecureRandomUtil.generateRandomNumber(); + } + if (policySpec.isMustntEndWithDigit()) { + generatedPassword[policySpec.getMinLength() - 1] = SecureRandomUtil.generateRandomLetter(); + } + if (policySpec.isMustntEndWithNonAlpha()) { + generatedPassword[policySpec.getMinLength() - 1] = SecureRandomUtil.generateRandomLetter(); + + } + } + + private int firstEmptyChar(final String[] generatedPStrings) { + int index = 0; + while (!generatedPStrings[index].isEmpty()) { + index++; + } + return index; + } + + private void checkRequired(final String[] generatedPassword, final PasswordPolicySpec policySpec) { + if (policySpec.isDigitRequired() + && !PolicyPattern.DIGIT.matcher(StringUtils.join(generatedPassword)).matches()) { + + generatedPassword[firstEmptyChar(generatedPassword)] = SecureRandomUtil.generateRandomNumber(); + } + + if (policySpec.isUppercaseRequired() + && !PolicyPattern.ALPHA_UPPERCASE.matcher(StringUtils.join(generatedPassword)).matches()) { + + generatedPassword[firstEmptyChar(generatedPassword)] = SecureRandomUtil.generateRandomLetter().toUpperCase(); + } + + if (policySpec.isLowercaseRequired() + && !PolicyPattern.ALPHA_LOWERCASE.matcher(StringUtils.join(generatedPassword)).matches()) { + + generatedPassword[firstEmptyChar(generatedPassword)] = SecureRandomUtil.generateRandomLetter().toLowerCase(); + } + + if (policySpec.isNonAlphanumericRequired() + && !PolicyPattern.NON_ALPHANUMERIC.matcher(StringUtils.join(generatedPassword)).matches()) { + + generatedPassword[firstEmptyChar(generatedPassword)] = + SecureRandomUtil.generateRandomSpecialCharacter(SPECIAL_CHARS); + } + } + + private void checkPrefixAndSuffix(final String[] generatedPassword, final PasswordPolicySpec policySpec) { + for (String prefix : policySpec.getPrefixesNotPermitted()) { + if (StringUtils.join(generatedPassword).startsWith(prefix)) { + checkStartChar(generatedPassword, policySpec); + } + } + + for (String suffix : policySpec.getSuffixesNotPermitted()) { + if (StringUtils.join(generatedPassword).endsWith(suffix)) { + checkEndChar(generatedPassword, policySpec); + } + } + } + +}