http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/security/SyncopeAuthenticationProvider.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/security/SyncopeAuthenticationProvider.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/security/SyncopeAuthenticationProvider.java new file mode 100644 index 0000000..607deb6 --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/security/SyncopeAuthenticationProvider.java @@ -0,0 +1,296 @@ +/* + * 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.core.misc.security; + +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.Set; +import javax.annotation.Resource; +import org.apache.syncope.common.lib.types.AttributableType; +import org.apache.syncope.common.lib.types.AuditElements; +import org.apache.syncope.common.lib.types.AuditElements.Result; +import org.apache.syncope.common.lib.types.CipherAlgorithm; +import org.apache.syncope.core.persistence.api.dao.ConfDAO; +import org.apache.syncope.core.persistence.api.dao.PolicyDAO; +import org.apache.syncope.core.persistence.api.dao.UserDAO; +import org.apache.syncope.core.persistence.api.entity.AccountPolicy; +import org.apache.syncope.core.persistence.api.entity.AttributableUtil; +import org.apache.syncope.core.persistence.api.entity.AttributableUtilFactory; +import org.apache.syncope.core.persistence.api.entity.ExternalResource; +import org.apache.syncope.core.persistence.api.entity.conf.CPlainAttr; +import org.apache.syncope.core.persistence.api.entity.role.Role; +import org.apache.syncope.core.persistence.api.entity.user.User; +import org.apache.syncope.core.provisioning.api.ConnectorFactory; +import org.apache.syncope.core.misc.AuditManager; +import org.apache.syncope.core.misc.MappingUtil; +import org.identityconnectors.framework.common.objects.Uid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.transaction.annotation.Transactional; + +@Configurable +public class SyncopeAuthenticationProvider implements AuthenticationProvider { + + /** + * Logger. + */ + protected static final Logger LOG = LoggerFactory.getLogger(SyncopeAuthenticationProvider.class); + + @Autowired + protected AuditManager auditManager; + + @Autowired + protected ConfDAO confDAO; + + @Autowired + protected UserDAO userDAO; + + @Autowired + protected PolicyDAO policyDAO; + + @Autowired + protected ConnectorFactory connFactory; + + @Autowired + protected AttributableUtilFactory attrUtilFactory; + + @Resource(name = "adminUser") + protected String adminUser; + + @Resource(name = "anonymousUser") + protected String anonymousUser; + + protected String adminPassword; + + protected String adminPasswordAlgorithm; + + protected String anonymousKey; + + protected UserDetailsService userDetailsService; + + protected final Encryptor encryptor = Encryptor.getInstance(); + + /** + * @param adminPassword the adminPassword to set + */ + public void setAdminPassword(final String adminPassword) { + this.adminPassword = adminPassword; + } + + /** + * @param adminPasswordAlgorithm the adminPasswordAlgorithm to set + */ + public void setAdminPasswordAlgorithm(final String adminPasswordAlgorithm) { + this.adminPasswordAlgorithm = adminPasswordAlgorithm; + } + + /** + * @param anonymousKey the anonymousKey to set + */ + public void setAnonymousKey(final String anonymousKey) { + this.anonymousKey = anonymousKey; + } + + public void setUserDetailsService(final UserDetailsService syncopeUserDetailsService) { + this.userDetailsService = syncopeUserDetailsService; + } + + @Override + @Transactional(noRollbackFor = { BadCredentialsException.class, DisabledException.class }) + public Authentication authenticate(final Authentication authentication) + throws AuthenticationException { + + boolean authenticated = false; + User user = null; + + String username = authentication.getName(); + if (anonymousUser.equals(username)) { + authenticated = authentication.getCredentials().toString().equals(anonymousKey); + } else if (adminUser.equals(username)) { + authenticated = encryptor.verify( + authentication.getCredentials().toString(), + CipherAlgorithm.valueOf(adminPasswordAlgorithm), + adminPassword); + } else { + user = userDAO.find(username); + + if (user != null) { + if (user.isSuspended() != null && user.isSuspended()) { + throw new DisabledException("User " + user.getUsername() + " is suspended"); + } + + CPlainAttr authStatuses = confDAO.find("authentication.statuses"); + if (authStatuses != null && !authStatuses.getValuesAsStrings().contains(user.getStatus())) { + throw new DisabledException("User " + user.getUsername() + " not allowed to authenticate"); + } + + authenticated = authenticate(user, authentication.getCredentials().toString()); + + updateLoginAttributes(user, authenticated); + } + } + + UsernamePasswordAuthenticationToken token; + if (authenticated) { + token = new UsernamePasswordAuthenticationToken( + authentication.getPrincipal(), + null, + userDetailsService.loadUserByUsername(authentication.getPrincipal().toString()).getAuthorities()); + + token.setDetails(authentication.getDetails()); + + auditManager.audit( + AuditElements.EventCategoryType.REST, + "AuthenticationController", + null, + "login", + Result.SUCCESS, + null, + authenticated, + authentication, + "Successfully authenticated, with roles: " + token.getAuthorities()); + + LOG.debug("User {} successfully authenticated, with roles {}", + authentication.getPrincipal(), token.getAuthorities()); + } else { + auditManager.audit( + AuditElements.EventCategoryType.REST, + "AuthenticationController", + null, + "login", + Result.FAILURE, + null, + authenticated, + authentication, + "User " + authentication.getPrincipal() + " not authenticated"); + + LOG.debug("User {} not authenticated", authentication.getPrincipal()); + + throw new BadCredentialsException("User " + authentication.getPrincipal() + " not authenticated"); + } + + return token; + } + + protected void updateLoginAttributes(User user, boolean authenticated) { + boolean userModified = false; + + if (authenticated) { + if (confDAO.find("log.lastlogindate", Boolean.toString(true)).getValues().get(0).getBooleanValue()) { + user.setLastLoginDate(new Date()); + userModified = true; + } + + if (user.getFailedLogins() != 0) { + user.setFailedLogins(0); + userModified = true; + } + } else { + user.setFailedLogins(user.getFailedLogins() + 1); + userModified = true; + } + + if (userModified) { + userDAO.save(user); + } + } + + protected Set<? extends ExternalResource> getPassthroughResources(final User user) { + Set<? extends ExternalResource> result = null; + + // 1. look for directly assigned resources, pick the ones whose account policy has authentication resources + for (ExternalResource resource : user.getOwnResources()) { + if (resource.getAccountPolicy() != null && !resource.getAccountPolicy().getResources().isEmpty()) { + if (result == null) { + result = resource.getAccountPolicy().getResources(); + } else { + result.retainAll(resource.getAccountPolicy().getResources()); + } + } + } + + // 2. look for owned roles, pick the ones whose account policy has authentication resources + for (Role role : user.getRoles()) { + if (role.getAccountPolicy() != null && !role.getAccountPolicy().getResources().isEmpty()) { + if (result == null) { + result = role.getAccountPolicy().getResources(); + } else { + result.retainAll(role.getAccountPolicy().getResources()); + } + } + } + + // 3. look for global account policy (if defined) + AccountPolicy global = policyDAO.getGlobalAccountPolicy(); + if (global != null && !global.getResources().isEmpty()) { + if (result == null) { + result = global.getResources(); + } else { + result.retainAll(global.getResources()); + } + } + + if (result == null) { + result = Collections.emptySet(); + } + + return result; + } + + protected boolean authenticate(final User user, final String password) { + boolean authenticated = encryptor.verify(password, user.getCipherAlgorithm(), user.getPassword()); + LOG.debug("{} authenticated on internal storage: {}", user.getUsername(), authenticated); + + final AttributableUtil attrUtil = attrUtilFactory.getInstance(AttributableType.USER); + for (Iterator<? extends ExternalResource> itor = getPassthroughResources(user).iterator(); + itor.hasNext() && !authenticated;) { + + ExternalResource resource = itor.next(); + String accountId = null; + try { + accountId = MappingUtil.getAccountIdValue(user, resource, attrUtil.getAccountIdItem(resource)); + Uid uid = connFactory.getConnector(resource).authenticate(accountId, password, null); + if (uid != null) { + authenticated = true; + } + } catch (Exception e) { + LOG.debug("Could not authenticate {} on {}", user.getUsername(), resource.getKey(), e); + } + LOG.debug("{} authenticated on {} as {}: {}", + user.getUsername(), resource.getKey(), accountId, authenticated); + } + + return authenticated; + } + + @Override + public boolean supports(final Class<? extends Object> type) { + return type.equals(UsernamePasswordAuthenticationToken.class); + } +}
http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/security/SyncopeUserDetailsService.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/security/SyncopeUserDetailsService.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/security/SyncopeUserDetailsService.java new file mode 100644 index 0000000..16eecf7 --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/security/SyncopeUserDetailsService.java @@ -0,0 +1,102 @@ +/* + * 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.core.misc.security; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Resource; +import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.core.persistence.api.RoleEntitlementUtil; +import org.apache.syncope.core.persistence.api.dao.EntitlementDAO; +import org.apache.syncope.core.persistence.api.dao.RoleDAO; +import org.apache.syncope.core.persistence.api.dao.UserDAO; +import org.apache.syncope.core.persistence.api.entity.Entitlement; +import org.apache.syncope.core.persistence.api.entity.role.Role; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; +import org.springframework.dao.DataAccessException; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +@Configurable +public class SyncopeUserDetailsService implements UserDetailsService { + + @Autowired + protected UserDAO userDAO; + + @Autowired + protected RoleDAO roleDAO; + + @Autowired + protected EntitlementDAO entitlementDAO; + + @Resource(name = "adminUser") + protected String adminUser; + + @Resource(name = "anonymousUser") + protected String anonymousUser; + + @Override + public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException, DataAccessException { + final Set<SimpleGrantedAuthority> authorities = new HashSet<>(); + if (anonymousUser.equals(username)) { + authorities.add(new SimpleGrantedAuthority(SyncopeConstants.ANONYMOUS_ENTITLEMENT)); + } else if (adminUser.equals(username)) { + for (Entitlement entitlement : entitlementDAO.findAll()) { + authorities.add(new SimpleGrantedAuthority(entitlement.getKey())); + } + } else { + org.apache.syncope.core.persistence.api.entity.user.User user = userDAO.find(username); + + if (user == null) { + throw new UsernameNotFoundException("Could not find any user with id " + username); + } + + // Give entitlements based on roles assigned to user (and their ancestors) + final Set<Role> roles = new HashSet<>(user.getRoles()); + for (Role role : user.getRoles()) { + roles.addAll(roleDAO.findAncestors(role)); + } + for (Role role : roles) { + for (Entitlement entitlement : role.getEntitlements()) { + authorities.add(new SimpleGrantedAuthority(entitlement.getKey())); + } + } + // Give role operational entitlements for owned roles + List<Role> ownedRoles = roleDAO.findOwnedByUser(user.getKey()); + if (!ownedRoles.isEmpty()) { + authorities.add(new SimpleGrantedAuthority("ROLE_CREATE")); + authorities.add(new SimpleGrantedAuthority("ROLE_READ")); + authorities.add(new SimpleGrantedAuthority("ROLE_UPDATE")); + authorities.add(new SimpleGrantedAuthority("ROLE_DELETE")); + + for (Role role : ownedRoles) { + authorities.add(new SimpleGrantedAuthority( + RoleEntitlementUtil.getEntitlementNameFromRoleKey(role.getKey()))); + } + } + } + + return new User(username, "<PASSWORD_PLACEHOLDER>", true, true, true, true, authorities); + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/security/UnauthorizedRoleException.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/security/UnauthorizedRoleException.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/security/UnauthorizedRoleException.java new file mode 100644 index 0000000..8c29871 --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/security/UnauthorizedRoleException.java @@ -0,0 +1,42 @@ +/* + * 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.core.misc.security; + +import java.util.Collections; +import java.util.Set; + +public class UnauthorizedRoleException extends RuntimeException { + + private static final long serialVersionUID = 7540587364235915081L; + + private final Set<Long> roleIds; + + public UnauthorizedRoleException(final Set<Long> roleIds) { + super("Missing entitlement for role(s) " + roleIds); + this.roleIds = roleIds; + } + + public UnauthorizedRoleException(final Long roleId) { + this(Collections.singleton(roleId)); + } + + public Set<Long> getRoleIds() { + return roleIds; + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/AttributeDeserializer.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/AttributeDeserializer.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/AttributeDeserializer.java new file mode 100644 index 0000000..3da92ee --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/AttributeDeserializer.java @@ -0,0 +1,84 @@ +/* + * 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.core.misc.serialization; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.identityconnectors.common.Base64; +import org.identityconnectors.common.security.GuardedString; +import org.identityconnectors.framework.common.objects.Attribute; +import org.identityconnectors.framework.common.objects.AttributeBuilder; +import org.identityconnectors.framework.common.objects.Name; +import org.identityconnectors.framework.common.objects.Uid; + +class AttributeDeserializer extends JsonDeserializer<Attribute> { + + @Override + public Attribute deserialize(final JsonParser jp, final DeserializationContext ctx) + throws IOException, JsonProcessingException { + + ObjectNode tree = jp.readValueAsTree(); + + String name = tree.get("name").asText(); + + List<Object> values = new ArrayList<Object>(); + for (Iterator<JsonNode> itor = tree.get("value").iterator(); itor.hasNext();) { + JsonNode node = itor.next(); + if (node.isNull()) { + values.add(null); + } else if (node.isObject()) { + values.add(((ObjectNode) node).traverse(jp.getCodec()).readValueAs(GuardedString.class)); + } else if (node.isBoolean()) { + values.add(node.asBoolean()); + } else if (node.isDouble()) { + values.add(node.asDouble()); + } else if (node.isLong()) { + values.add(node.asLong()); + } else if (node.isInt()) { + values.add(node.asInt()); + } else { + String text = node.asText(); + if (text.startsWith(AttributeSerializer.BYTE_ARRAY_PREFIX) + && text.endsWith(AttributeSerializer.BYTE_ARRAY_SUFFIX)) { + + values.add(Base64.decode(StringUtils.substringBetween( + text, AttributeSerializer.BYTE_ARRAY_PREFIX, AttributeSerializer.BYTE_ARRAY_SUFFIX))); + } else { + values.add(text); + } + } + } + + return Uid.NAME.equals(name) + ? new Uid(values.isEmpty() || values.get(0) == null ? null : values.get(0).toString()) + : Name.NAME.equals(name) + ? new Name(values.isEmpty() || values.get(0) == null ? null : values.get(0).toString()) + : AttributeBuilder.build(name, values); + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/AttributeSerializer.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/AttributeSerializer.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/AttributeSerializer.java new file mode 100644 index 0000000..2fff021 --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/AttributeSerializer.java @@ -0,0 +1,78 @@ +/* + * 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.core.misc.serialization; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import org.identityconnectors.common.Base64; +import org.identityconnectors.common.security.GuardedString; +import org.identityconnectors.framework.common.objects.Attribute; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class AttributeSerializer extends JsonSerializer<Attribute> { + + private static final Logger LOG = LoggerFactory.getLogger(AttributeSerializer.class); + + public static final String BYTE_ARRAY_PREFIX = "<binary>"; + + public static final String BYTE_ARRAY_SUFFIX = "</binary>"; + + @Override + public void serialize(final Attribute source, final JsonGenerator jgen, final SerializerProvider sp) + throws IOException, JsonProcessingException { + + jgen.writeStartObject(); + + jgen.writeStringField("name", source.getName()); + + jgen.writeFieldName("value"); + if (source.getValue() == null) { + jgen.writeNull(); + } else { + jgen.writeStartArray(); + for (Object value : source.getValue()) { + if (value == null) { + jgen.writeNull(); + } else if (value instanceof GuardedString) { + jgen.writeObject(value); + } else if (value instanceof Integer) { + jgen.writeNumber((Integer) value); + } else if (value instanceof Long) { + jgen.writeNumber((Long) value); + } else if (value instanceof Double) { + jgen.writeNumber((Double) value); + } else if (value instanceof Boolean) { + jgen.writeBoolean((Boolean) value); + } else if (value instanceof byte[]) { + jgen.writeString(BYTE_ARRAY_PREFIX + Base64.encode((byte[]) value) + BYTE_ARRAY_SUFFIX); + } else { + jgen.writeString(value.toString()); + } + } + jgen.writeEndArray(); + } + + jgen.writeEndObject(); + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/GuardedStringDeserializer.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/GuardedStringDeserializer.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/GuardedStringDeserializer.java new file mode 100644 index 0000000..d8c372d --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/GuardedStringDeserializer.java @@ -0,0 +1,94 @@ +/* + * 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.core.misc.serialization; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.IOException; +import java.lang.reflect.Field; +import org.identityconnectors.common.Base64; +import org.identityconnectors.common.security.EncryptorFactory; +import org.identityconnectors.common.security.GuardedString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class GuardedStringDeserializer extends JsonDeserializer<GuardedString> { + + private static final Logger LOG = LoggerFactory.getLogger(GuardedStringDeserializer.class); + + @Override + public GuardedString deserialize(final JsonParser jp, final DeserializationContext ctx) + throws IOException, JsonProcessingException { + + ObjectNode tree = jp.readValueAsTree(); + + boolean readOnly = false; + if (tree.has("readOnly")) { + readOnly = tree.get("readOnly").asBoolean(); + } + boolean disposed = false; + if (tree.has("disposed")) { + disposed = tree.get("disposed").asBoolean(); + } + byte[] encryptedBytes = null; + if (tree.has("encryptedBytes")) { + encryptedBytes = Base64.decode(tree.get("encryptedBytes").asText()); + } + String base64SHA1Hash = null; + if (tree.has("base64SHA1Hash")) { + base64SHA1Hash = tree.get("base64SHA1Hash").asText(); + } + + final byte[] clearBytes = EncryptorFactory.getInstance().getDefaultEncryptor().decrypt(encryptedBytes); + + GuardedString dest = new GuardedString(new String(clearBytes).toCharArray()); + + try { + Field field = GuardedString.class.getDeclaredField("readOnly"); + field.setAccessible(true); + field.setBoolean(dest, readOnly); + } catch (Exception e) { + LOG.error("Could not set field value to {}", readOnly, e); + } + + try { + Field field = GuardedString.class.getDeclaredField("disposed"); + field.setAccessible(true); + field.setBoolean(dest, disposed); + } catch (Exception e) { + LOG.error("Could not set field value to {}", disposed, e); + } + + if (base64SHA1Hash != null) { + try { + Field field = GuardedString.class.getDeclaredField("base64SHA1Hash"); + field.setAccessible(true); + field.set(dest, base64SHA1Hash); + } catch (Exception e) { + LOG.error("Could not set field value to {}", base64SHA1Hash, e); + } + } + + return dest; + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/GuardedStringSerializer.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/GuardedStringSerializer.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/GuardedStringSerializer.java new file mode 100644 index 0000000..de9f60a --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/GuardedStringSerializer.java @@ -0,0 +1,90 @@ +/* + * 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.core.misc.serialization; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.lang.reflect.Field; +import org.identityconnectors.common.Base64; +import org.identityconnectors.common.security.EncryptorFactory; +import org.identityconnectors.common.security.GuardedString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class GuardedStringSerializer extends JsonSerializer<GuardedString> { + + private static final Logger LOG = LoggerFactory.getLogger(GuardedStringSerializer.class); + + @Override + public void serialize(final GuardedString source, final JsonGenerator jgen, final SerializerProvider sp) + throws IOException, JsonProcessingException { + + jgen.writeStartObject(); + + boolean readOnly = false; + try { + Field field = GuardedString.class.getDeclaredField("readOnly"); + field.setAccessible(true); + readOnly = field.getBoolean(source); + } catch (Exception e) { + LOG.error("Could not get field value", e); + } + jgen.writeBooleanField("readOnly", readOnly); + + boolean disposed = false; + try { + Field field = GuardedString.class.getDeclaredField("disposed"); + field.setAccessible(true); + disposed = field.getBoolean(source); + } catch (Exception e) { + LOG.error("Could not get field value", e); + } + jgen.writeBooleanField("disposed", disposed); + + final StringBuilder cleartext = new StringBuilder(); + ((GuardedString) source).access(new GuardedString.Accessor() { + + @Override + public void access(final char[] clearChars) { + cleartext.append(clearChars); + } + }); + final byte[] encryptedBytes = + EncryptorFactory.getInstance().getDefaultEncryptor().encrypt(cleartext.toString().getBytes()); + jgen.writeStringField("encryptedBytes", Base64.encode(encryptedBytes)); + + String base64SHA1Hash = null; + try { + Field field = GuardedString.class.getDeclaredField("base64SHA1Hash"); + field.setAccessible(true); + base64SHA1Hash = field.get(source).toString(); + } catch (Exception e) { + LOG.error("Could not get field value", e); + } + if (base64SHA1Hash != null) { + jgen.writeStringField("base64SHA1Hash", base64SHA1Hash); + } + + jgen.writeEndObject(); + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/POJOHelper.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/POJOHelper.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/POJOHelper.java new file mode 100644 index 0000000..dd42568 --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/POJOHelper.java @@ -0,0 +1,80 @@ +/* + * 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.core.misc.serialization; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +import org.identityconnectors.common.security.GuardedString; +import org.identityconnectors.framework.common.objects.Attribute; +import org.identityconnectors.framework.common.objects.SyncToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helper class for serialization and deserialization of configuration objects (POJOs) in JSON. + */ +public final class POJOHelper { + + private static final Logger LOG = LoggerFactory.getLogger(POJOHelper.class); + + private static final ObjectMapper MAPPER; + + static { + SimpleModule pojoModule = new SimpleModule("POJOModule", new Version(1, 0, 0, null, null, null)); + pojoModule.addSerializer(GuardedString.class, new GuardedStringSerializer()); + pojoModule.addSerializer(Attribute.class, new AttributeSerializer()); + pojoModule.addSerializer(SyncToken.class, new SyncTokenSerializer()); + pojoModule.addDeserializer(GuardedString.class, new GuardedStringDeserializer()); + pojoModule.addDeserializer(Attribute.class, new AttributeDeserializer()); + pojoModule.addDeserializer(SyncToken.class, new SyncTokenDeserializer()); + + MAPPER = new ObjectMapper(); + MAPPER.registerModule(pojoModule); + MAPPER.registerModule(new AfterburnerModule()); + } + + public static String serialize(final Object object) { + String result = null; + + try { + result = MAPPER.writeValueAsString(object); + } catch (Exception e) { + LOG.error("During serialization", e); + } + + return result; + } + + public static <T extends Object> T deserialize(final String serialized, final Class<T> reference) { + T result = null; + + try { + result = MAPPER.readValue(serialized, reference); + } catch (Exception e) { + LOG.error("During deserialization", e); + } + + return result; + } + + private POJOHelper() { + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/SyncTokenDeserializer.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/SyncTokenDeserializer.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/SyncTokenDeserializer.java new file mode 100644 index 0000000..3497373 --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/SyncTokenDeserializer.java @@ -0,0 +1,65 @@ +/* + * 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.core.misc.serialization; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.IOException; +import org.apache.commons.codec.binary.Base64; +import org.identityconnectors.framework.common.objects.SyncToken; + +class SyncTokenDeserializer extends JsonDeserializer<SyncToken> { + + @Override + public SyncToken deserialize(final JsonParser jp, final DeserializationContext ctx) + throws IOException, JsonProcessingException { + + ObjectNode tree = jp.readValueAsTree(); + + Object value = null; + if (tree.has("value")) { + JsonNode node = tree.get("value"); + value = node.isNull() + ? null + : node.isBoolean() + ? node.asBoolean() + : node.isDouble() + ? node.asDouble() + : node.isLong() + ? node.asLong() + : node.isInt() + ? node.asInt() + : node.asText(); + + if (value instanceof String) { + String base64 = (String) value; + if (Base64.isBase64(base64)) { + value = Base64.decodeBase64(base64); + } + } + } + + return new SyncToken(value); + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/SyncTokenSerializer.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/SyncTokenSerializer.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/SyncTokenSerializer.java new file mode 100644 index 0000000..c2f2183 --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/SyncTokenSerializer.java @@ -0,0 +1,58 @@ +/* + * 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.core.misc.serialization; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import org.apache.commons.codec.binary.Base64; +import org.identityconnectors.framework.common.objects.SyncToken; + +class SyncTokenSerializer extends JsonSerializer<SyncToken> { + + @Override + public void serialize(final SyncToken source, final JsonGenerator jgen, final SerializerProvider sp) + throws IOException, JsonProcessingException { + + jgen.writeStartObject(); + + jgen.writeFieldName("value"); + + if (source.getValue() == null) { + jgen.writeNull(); + } else if (source.getValue() instanceof Boolean) { + jgen.writeBoolean((Boolean) source.getValue()); + } else if (source.getValue() instanceof Double) { + jgen.writeNumber((Double) source.getValue()); + } else if (source.getValue() instanceof Long) { + jgen.writeNumber((Long) source.getValue()); + } else if (source.getValue() instanceof Integer) { + jgen.writeNumber((Integer) source.getValue()); + } else if (source.getValue() instanceof byte[]) { + jgen.writeString(Base64.encodeBase64String((byte[]) source.getValue())); + } else { + jgen.writeString(source.getValue().toString()); + } + + jgen.writeEndObject(); + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/UnwrappedObjectMapper.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/UnwrappedObjectMapper.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/UnwrappedObjectMapper.java new file mode 100644 index 0000000..b79b6bd --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/serialization/UnwrappedObjectMapper.java @@ -0,0 +1,95 @@ +/* + * 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.core.misc.serialization; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.util.Map; + +/** + * Jackson ObjectMapper that unwraps singleton map values and enable default + * typing for handling abstract types serialization. + */ +public class UnwrappedObjectMapper extends ObjectMapper { + + private static final long serialVersionUID = -317191546835195103L; + + /** + * Unwraps the given value if it implements the Map interface and contains + * only a single entry. Otherwise the value is returned unmodified. + * + * @param value the potential Map to unwrap + * @return the unwrapped map or the original value + */ + private Object unwrapMap(final Object value) { + if (value instanceof Map) { + Map<?, ?> map = (Map<?, ?>) value; + if (map.size() == 1) { + return map.values().iterator().next(); + } + } + + return value; + } + + @Override + public void writeValue(final JsonGenerator jgen, final Object value) + throws IOException, JsonGenerationException, JsonMappingException { + + super.writeValue(jgen, unwrapMap(value)); + } + + @Override + public void writeValue(final File resultFile, final Object value) + throws IOException, JsonGenerationException, JsonMappingException { + + super.writeValue(resultFile, unwrapMap(value)); + } + + @Override + public void writeValue(final OutputStream out, final Object value) + throws IOException, JsonGenerationException, JsonMappingException { + + super.writeValue(out, unwrapMap(value)); + } + + @Override + public void writeValue(final Writer w, final Object value) + throws IOException, JsonGenerationException, JsonMappingException { + + super.writeValue(w, unwrapMap(value)); + } + + @Override + public byte[] writeValueAsBytes(final Object value) throws JsonProcessingException { + return super.writeValueAsBytes(unwrapMap(value)); + } + + @Override + public String writeValueAsString(final Object value) throws JsonProcessingException { + return super.writeValueAsString(unwrapMap(value)); + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/spring/ApplicationContextProvider.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/spring/ApplicationContextProvider.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/spring/ApplicationContextProvider.java new file mode 100644 index 0000000..dfbcbe4 --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/spring/ApplicationContextProvider.java @@ -0,0 +1,47 @@ +/* + * 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.core.misc.spring; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; + +public class ApplicationContextProvider implements ApplicationContextAware { + + private static ConfigurableApplicationContext ctx; + + public static ConfigurableApplicationContext getApplicationContext() { + return ctx; + } + + public static DefaultListableBeanFactory getBeanFactory() { + return (DefaultListableBeanFactory) ctx.getBeanFactory(); + } + + /** + * Wiring the ApplicationContext into a static method. + * + * @param ctx Spring application context + */ + @Override + public void setApplicationContext(final ApplicationContext ctx) { + ApplicationContextProvider.ctx = (ConfigurableApplicationContext) ctx; + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/spring/BeanUtils.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/spring/BeanUtils.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/spring/BeanUtils.java new file mode 100644 index 0000000..97b57db --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/spring/BeanUtils.java @@ -0,0 +1,201 @@ +/* + * 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.core.misc.spring; + +import static org.springframework.beans.BeanUtils.getPropertyDescriptor; +import static org.springframework.beans.BeanUtils.getPropertyDescriptors; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.springframework.beans.BeansException; +import org.springframework.beans.FatalBeanException; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Overrides Spring's BeanUtils not using collection setters but instead getters + addAll() / putAll(), + * in a JAXB friendly way. + * + * Refer to <a href="https://issues.apache.org/jira/browse/SYNCOPE-246">SYNCOPE-246</a> for more information. + * + * @see org.springframework.beans.BeanUtils + */ +public final class BeanUtils { + + private BeanUtils() { + // Empty private constructor for static utility classes + } + + /** + * Copy the property values of the given source bean into the target bean. + * <p> + * Note: The source and target classes do not have to match or even be derived + * from each other, as long as the properties match. Any bean properties that the + * source bean exposes but the target bean does not will silently be ignored. + * </p><p> + * This is just a convenience method. For more complex transfer needs, + * consider using a full BeanWrapper. + * </p> + * @param source the source bean + * @param target the target bean + * @throws BeansException if the copying failed + * @see org.springframework.beans.BeanWrapper + */ + public static void copyProperties(final Object source, final Object target) throws BeansException { + copyProperties(source, target, null, (String[]) null); + } + + /** + * Copy the property values of the given source bean into the given target bean, + * only setting properties defined in the given "editable" class (or interface). + * <p> + * Note: The source and target classes do not have to match or even be derived + * from each other, as long as the properties match. Any bean properties that the + * source bean exposes but the target bean does not will silently be ignored. + * </p><p> + * This is just a convenience method. For more complex transfer needs, + * consider using a full BeanWrapper. + * </p> + * + * @param source the source bean + * @param target the target bean + * @param editable the class (or interface) to restrict property setting to + * @throws BeansException if the copying failed + * @see org.springframework.beans.BeanWrapper + */ + public static void copyProperties(final Object source, final Object target, final Class<?> editable) + throws BeansException { + + copyProperties(source, target, editable, (String[]) null); + } + + /** + * Copy the property values of the given source bean into the given target bean, + * ignoring the given "ignoreProperties". + * <p> + * Note: The source and target classes do not have to match or even be derived + * from each other, as long as the properties match. Any bean properties that the + * source bean exposes but the target bean does not will silently be ignored. + * </p><p> + * This is just a convenience method. For more complex transfer needs, + * consider using a full BeanWrapper. + * </p> + * + * @param source the source bean + * @param target the target bean + * @param ignoreProperties array of property names to ignore + * @throws BeansException if the copying failed + * @see org.springframework.beans.BeanWrapper + */ + public static void copyProperties(final Object source, final Object target, final String... ignoreProperties) + throws BeansException { + + copyProperties(source, target, null, ignoreProperties); + } + + /** + * Copy the property values of the given source bean into the given target bean. + * <p> + * Note: The source and target classes do not have to match or even be derived + * from each other, as long as the properties match. Any bean properties that the + * source bean exposes but the target bean does not will silently be ignored. + * </p> + * + * @param source the source bean + * @param target the target bean + * @param editable the class (or interface) to restrict property setting to + * @param ignoreProperties array of property names to ignore + * @throws BeansException if the copying failed + * @see org.springframework.beans.BeanWrapper + */ + @SuppressWarnings("unchecked") + private static void copyProperties(final Object source, final Object target, final Class<?> editable, + final String... ignoreProperties) throws BeansException { + + Assert.notNull(source, "Source must not be null"); + Assert.notNull(target, "Target must not be null"); + + Class<?> actualEditable = target.getClass(); + if (editable != null) { + if (!editable.isInstance(target)) { + throw new IllegalArgumentException("Target class [" + target.getClass().getName() + + "] not assignable to Editable class [" + editable.getName() + "]"); + } + actualEditable = editable; + } + PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable); + List<String> ignoreList = (ignoreProperties == null) + ? Collections.<String>emptyList() : Arrays.asList(ignoreProperties); + + for (PropertyDescriptor targetPd : targetPds) { + if (ignoreProperties == null || (!ignoreList.contains(targetPd.getName()))) { + PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName()); + if (sourcePd != null) { + Method readMethod = sourcePd.getReadMethod(); + if (readMethod != null) { + Method writeMethod = targetPd.getWriteMethod(); + + try { + // Diverts from Spring's BeanUtils: if no write method is found and property is collection, + // try to use addAll() / putAll(). + if (writeMethod == null) { + Object value = readMethod.invoke(source); + Method targetReadMethod = targetPd.getReadMethod(); + if (targetReadMethod != null) { + if (!Modifier.isPublic(targetReadMethod.getDeclaringClass().getModifiers())) { + targetReadMethod.setAccessible(true); + } + Object destValue = targetReadMethod.invoke(target); + + if (value instanceof Collection && destValue instanceof Collection) { + ((Collection) destValue).clear(); + ((Collection) destValue).addAll((Collection) value); + } else if (value instanceof Map && destValue instanceof Map) { + ((Map) destValue).clear(); + ((Map) destValue).putAll((Map) value); + } + } + } else if (ClassUtils.isAssignable( + writeMethod.getParameterTypes()[0], readMethod.getReturnType())) { + + if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) { + readMethod.setAccessible(true); + } + Object value = readMethod.invoke(source); + if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) { + writeMethod.setAccessible(true); + } + writeMethod.invoke(target, value); + } + } catch (Throwable ex) { + throw new FatalBeanException( + "Could not copy property '" + targetPd.getName() + "' from source to target", ex); + } + } + } + } + } + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/spring/ResourceWithFallbackLoader.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/spring/ResourceWithFallbackLoader.java b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/spring/ResourceWithFallbackLoader.java new file mode 100644 index 0000000..0fdc116 --- /dev/null +++ b/syncope620/core/misc/src/main/java/org/apache/syncope/core/misc/spring/ResourceWithFallbackLoader.java @@ -0,0 +1,82 @@ +/* + * 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.core.misc.spring; + +import java.io.IOException; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.ResourcePatternResolver; + +public class ResourceWithFallbackLoader implements ResourceLoaderAware, ResourcePatternResolver { + + private ResourcePatternResolver resolver; + + private String primary; + + private String fallback; + + @Override + public void setResourceLoader(final ResourceLoader resourceLoader) { + this.resolver = (ResourcePatternResolver) resourceLoader; + } + + public void setPrimary(final String primary) { + this.primary = primary; + } + + public void setFallback(final String fallback) { + this.fallback = fallback; + } + + @Override + public Resource getResource(final String location) { + Resource resource = resolver.getResource(primary + location); + if (!resource.exists()) { + resource = resolver.getResource(fallback + location); + } + + return resource; + } + + public Resource getResource() { + return getResource(StringUtils.EMPTY); + } + + @Override + public Resource[] getResources(final String locationPattern) throws IOException { + Resource[] resources = resolver.getResources(primary + locationPattern); + if (ArrayUtils.isEmpty(resources)) { + resources = resolver.getResources(fallback + locationPattern); + } + + return resources; + } + + public Resource[] getResources() throws IOException { + return getResources(StringUtils.EMPTY); + } + + @Override + public ClassLoader getClassLoader() { + return resolver.getClassLoader(); + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/resources/security.properties ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/resources/security.properties b/syncope620/core/misc/src/main/resources/security.properties new file mode 100644 index 0000000..c0fcd37 --- /dev/null +++ b/syncope620/core/misc/src/main/resources/security.properties @@ -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. +adminUser=admin +adminPassword=5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8 +adminPasswordAlgorithm=SHA1 + +anonymousUser=${anonymousUser} +anonymousKey=${anonymousKey} + +secretKey=${secretKey} +# default for LDAP / RFC2307 SSHA +digester.saltIterations=1 +digester.saltSizeBytes=8 +digester.invertPositionOfPlainSaltInEncryptionResults=true +digester.invertPositionOfSaltInMessageBeforeDigesting=true +digester.useLenientSaltSizeCheck=true \ No newline at end of file http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/main/resources/securityContext.xml ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/main/resources/securityContext.xml b/syncope620/core/misc/src/main/resources/securityContext.xml new file mode 100644 index 0000000..a835970 --- /dev/null +++ b/syncope620/core/misc/src/main/resources/securityContext.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +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. +--> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:security="http://www.springframework.org/schema/security" + xsi:schemaLocation="http://www.springframework.org/schema/beans + http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/security + http://www.springframework.org/schema/security/spring-security.xsd"> + + <bean id="adminUser" class="java.lang.String"> + <constructor-arg value="${adminUser}"/> + </bean> + <bean id="anonymousUser" class="java.lang.String"> + <constructor-arg value="${anonymousUser}"/> + </bean> + + <security:global-method-security pre-post-annotations="enabled"/> + + <bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy"> + <security:filter-chain-map path-type="ant"> + <security:filter-chain pattern="/**" filters="securityContextPersistenceFilter"/> + </security:filter-chain-map> + </bean> + + <bean id="securityContextRepository" class='org.springframework.security.web.context.NullSecurityContextRepository'/> + + <bean id="securityContextPersistenceFilter" + class="org.springframework.security.web.context.SecurityContextPersistenceFilter"> + <property name="securityContextRepository" ref="securityContextRepository"/> + </bean> + + <security:http security-context-repository-ref="securityContextRepository" realm="Apache Syncope authentication"> + <security:http-basic/> + <security:anonymous username="${anonymousUser}"/> + <security:intercept-url pattern="/**"/> + </security:http> + + <bean id="syncopeUserDetailsService" class="org.apache.syncope.core.misc.security.SyncopeUserDetailsService"/> + + <bean id="syncopeAuthenticationProvider" class="org.apache.syncope.core.misc.security.SyncopeAuthenticationProvider"> + <property name="adminPassword" value="${adminPassword}"/> + <property name="adminPasswordAlgorithm" value="${adminPasswordAlgorithm}"/> + <property name="anonymousKey" value="${anonymousKey}"/> + <property name="userDetailsService" ref="syncopeUserDetailsService"/> + </bean> + + <security:authentication-manager> + <security:authentication-provider ref="syncopeAuthenticationProvider"/> + </security:authentication-manager> +</beans> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/test/java/org/apache/syncope/core/misc/search/SearchCondConverterTest.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/test/java/org/apache/syncope/core/misc/search/SearchCondConverterTest.java b/syncope620/core/misc/src/test/java/org/apache/syncope/core/misc/search/SearchCondConverterTest.java new file mode 100644 index 0000000..27cd0b6 --- /dev/null +++ b/syncope620/core/misc/src/test/java/org/apache/syncope/core/misc/search/SearchCondConverterTest.java @@ -0,0 +1,165 @@ +/* + * 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.core.misc.search; + +import static org.junit.Assert.assertEquals; + +import org.apache.syncope.common.lib.search.RoleFiqlSearchConditionBuilder; +import org.apache.syncope.common.lib.search.SpecialAttr; +import org.apache.syncope.common.lib.search.UserFiqlSearchConditionBuilder; +import org.apache.syncope.core.persistence.api.dao.search.AttributeCond; +import org.apache.syncope.core.persistence.api.dao.search.EntitlementCond; +import org.apache.syncope.core.persistence.api.dao.search.MembershipCond; +import org.apache.syncope.core.persistence.api.dao.search.ResourceCond; +import org.apache.syncope.core.persistence.api.dao.search.SearchCond; +import org.apache.syncope.core.persistence.api.dao.search.SubjectCond; +import org.junit.Test; + +public class SearchCondConverterTest { + + @Test + public void eq() { + String fiqlExpression = new UserFiqlSearchConditionBuilder().is("username").equalTo("rossini").query(); + assertEquals("username==rossini", fiqlExpression); + + SubjectCond attrCond = new SubjectCond(AttributeCond.Type.EQ); + attrCond.setSchema("username"); + attrCond.setExpression("rossini"); + SearchCond simpleCond = SearchCond.getLeafCond(attrCond); + + assertEquals(simpleCond, SearchCondConverter.convert(fiqlExpression)); + } + + @Test + public void like() { + String fiqlExpression = new UserFiqlSearchConditionBuilder().is("username").equalTo("ros*").query(); + assertEquals("username==ros*", fiqlExpression); + + AttributeCond attrCond = new SubjectCond(AttributeCond.Type.LIKE); + attrCond.setSchema("username"); + attrCond.setExpression("ros%"); + SearchCond simpleCond = SearchCond.getLeafCond(attrCond); + + assertEquals(simpleCond, SearchCondConverter.convert(fiqlExpression)); + } + + @Test + public void isNull() { + String fiqlExpression = new UserFiqlSearchConditionBuilder().is("loginDate").nullValue().query(); + assertEquals("loginDate==" + SpecialAttr.NULL, fiqlExpression); + + AttributeCond attrCond = new AttributeCond(AttributeCond.Type.ISNULL); + attrCond.setSchema("loginDate"); + SearchCond simpleCond = SearchCond.getLeafCond(attrCond); + + assertEquals(simpleCond, SearchCondConverter.convert(fiqlExpression)); + } + + @Test + public void isNotNull() { + String fiqlExpression = new UserFiqlSearchConditionBuilder().is("loginDate").notNullValue().query(); + assertEquals("loginDate!=" + SpecialAttr.NULL, fiqlExpression); + + AttributeCond attrCond = new AttributeCond(AttributeCond.Type.ISNOTNULL); + attrCond.setSchema("loginDate"); + SearchCond simpleCond = SearchCond.getLeafCond(attrCond); + + assertEquals(simpleCond, SearchCondConverter.convert(fiqlExpression)); + } + + @Test + public void roles() { + String fiqlExpression = new UserFiqlSearchConditionBuilder().hasRoles(1L).query(); + assertEquals(SpecialAttr.ROLES + "==1", fiqlExpression); + + MembershipCond membCond = new MembershipCond(); + membCond.setRoleId(1L); + SearchCond simpleCond = SearchCond.getLeafCond(membCond); + + assertEquals(simpleCond, SearchCondConverter.convert(fiqlExpression)); + } + + @Test + public void resources() { + String fiqlExpression = new UserFiqlSearchConditionBuilder().hasResources("resource-ldap").query(); + assertEquals(SpecialAttr.RESOURCES + "==resource-ldap", fiqlExpression); + + ResourceCond resCond = new ResourceCond(); + resCond.setResourceName("resource-ldap"); + SearchCond simpleCond = SearchCond.getLeafCond(resCond); + + assertEquals(simpleCond, SearchCondConverter.convert(fiqlExpression)); + } + + @Test + public void entitlements() { + String fiqlExpression = new RoleFiqlSearchConditionBuilder().hasEntitlements("USER_LIST").query(); + assertEquals(SpecialAttr.ENTITLEMENTS + "==USER_LIST", fiqlExpression); + + EntitlementCond entCond = new EntitlementCond(); + entCond.setExpression("USER_LIST"); + SearchCond simpleCond = SearchCond.getLeafCond(entCond); + + assertEquals(simpleCond, SearchCondConverter.convert(fiqlExpression)); + } + + @Test + public void and() { + String fiqlExpression = new UserFiqlSearchConditionBuilder(). + is("fullname").equalTo("*o*").and("fullname").equalTo("*i*").query(); + assertEquals("fullname==*o*;fullname==*i*", fiqlExpression); + + AttributeCond fullnameLeafCond1 = new AttributeCond(AttributeCond.Type.LIKE); + fullnameLeafCond1.setSchema("fullname"); + fullnameLeafCond1.setExpression("%o%"); + AttributeCond fullnameLeafCond2 = new AttributeCond(AttributeCond.Type.LIKE); + fullnameLeafCond2.setSchema("fullname"); + fullnameLeafCond2.setExpression("%i%"); + SearchCond andCond = SearchCond.getAndCond( + SearchCond.getLeafCond(fullnameLeafCond1), + SearchCond.getLeafCond(fullnameLeafCond2)); + + assertEquals(andCond, SearchCondConverter.convert(fiqlExpression)); + } + + @Test + public void or() { + String fiqlExpression = new UserFiqlSearchConditionBuilder(). + is("fullname").equalTo("*o*", "*i*", "*ini").query(); + assertEquals("fullname==*o*,fullname==*i*,fullname==*ini", fiqlExpression); + + AttributeCond fullnameLeafCond1 = new AttributeCond(AttributeCond.Type.LIKE); + fullnameLeafCond1.setSchema("fullname"); + fullnameLeafCond1.setExpression("%o%"); + AttributeCond fullnameLeafCond2 = new AttributeCond(AttributeCond.Type.LIKE); + fullnameLeafCond2.setSchema("fullname"); + fullnameLeafCond2.setExpression("%i%"); + AttributeCond fullnameLeafCond3 = new AttributeCond(AttributeCond.Type.LIKE); + fullnameLeafCond3.setSchema("fullname"); + fullnameLeafCond3.setExpression("%ini"); + SearchCond orCond = SearchCond.getOrCond( + SearchCond.getLeafCond(fullnameLeafCond1), + SearchCond.getOrCond( + SearchCond.getLeafCond(fullnameLeafCond2), + SearchCond.getLeafCond(fullnameLeafCond3))); + + assertEquals(orCond, SearchCondConverter.convert(fiqlExpression)); + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/test/java/org/apache/syncope/core/misc/security/EncryptorTest.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/test/java/org/apache/syncope/core/misc/security/EncryptorTest.java b/syncope620/core/misc/src/test/java/org/apache/syncope/core/misc/security/EncryptorTest.java new file mode 100644 index 0000000..98e8061 --- /dev/null +++ b/syncope620/core/misc/src/test/java/org/apache/syncope/core/misc/security/EncryptorTest.java @@ -0,0 +1,58 @@ +/* + * 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.core.misc.security; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.apache.syncope.common.lib.types.CipherAlgorithm; +import org.junit.Test; + +/** + * Test class to test all encryption algorithms. + */ +public class EncryptorTest { + + private final String password = "password"; + + private final Encryptor encryptor = Encryptor.getInstance(); + + /** + * Verify all algorithms. + */ + @Test + public void testEncoder() throws Exception { + for (CipherAlgorithm cipherAlgorithm : CipherAlgorithm.values()) { + final String encPassword = encryptor.encode(password, cipherAlgorithm); + + assertNotNull(encPassword); + assertTrue(encryptor.verify(password, cipherAlgorithm, encPassword)); + assertFalse(encryptor.verify("pass", cipherAlgorithm, encPassword)); + + // check that same password encoded with BCRYPT or Salted versions results in different digest + if (cipherAlgorithm.equals(CipherAlgorithm.BCRYPT) || cipherAlgorithm.getAlgorithm().startsWith("S-")) { + final String encSamePassword = encryptor.encode(password, cipherAlgorithm); + assertNotNull(encSamePassword); + assertFalse(encSamePassword.equals(encPassword)); + assertTrue(encryptor.verify(password, cipherAlgorithm, encSamePassword)); + } + } + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/misc/src/test/java/org/apache/syncope/core/misc/security/PasswordGeneratorTest.java ---------------------------------------------------------------------- diff --git a/syncope620/core/misc/src/test/java/org/apache/syncope/core/misc/security/PasswordGeneratorTest.java b/syncope620/core/misc/src/test/java/org/apache/syncope/core/misc/security/PasswordGeneratorTest.java new file mode 100644 index 0000000..9c959b3 --- /dev/null +++ b/syncope620/core/misc/src/test/java/org/apache/syncope/core/misc/security/PasswordGeneratorTest.java @@ -0,0 +1,124 @@ +/* + * 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.core.misc.security; + +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.apache.syncope.common.lib.types.PasswordPolicySpec; +import org.apache.syncope.core.misc.policy.InvalidPasswordPolicySpecException; +import org.apache.syncope.core.misc.policy.PolicyPattern; +import org.junit.Test; + +public class PasswordGeneratorTest { + + private final PasswordGenerator passwordGenerator = new PasswordGenerator(); + + @Test + public void startEndWithDigit() + throws InvalidPasswordPolicySpecException { + + PasswordPolicySpec passwordPolicySpec = createBasePasswordPolicySpec(); + passwordPolicySpec.setMustStartWithDigit(true); + + PasswordPolicySpec passwordPolicySpec2 = createBasePasswordPolicySpec(); + passwordPolicySpec.setMustEndWithDigit(true); + List<PasswordPolicySpec> passwordPolicySpecs = new ArrayList<>(); + passwordPolicySpecs.add(passwordPolicySpec); + passwordPolicySpecs.add(passwordPolicySpec2); + String generatedPassword = passwordGenerator.generate(passwordPolicySpecs); + assertTrue(Character.isDigit(generatedPassword.charAt(0))); + assertTrue(Character.isDigit(generatedPassword.charAt(generatedPassword.length() - 1))); + } + + @Test + public void startWithDigitAndWithAlpha() + throws InvalidPasswordPolicySpecException { + + PasswordPolicySpec passwordPolicySpec = createBasePasswordPolicySpec(); + passwordPolicySpec.setMustStartWithDigit(true); + + PasswordPolicySpec passwordPolicySpec2 = createBasePasswordPolicySpec(); + passwordPolicySpec.setMustEndWithAlpha(true); + List<PasswordPolicySpec> passwordPolicySpecs = new ArrayList<>(); + passwordPolicySpecs.add(passwordPolicySpec); + passwordPolicySpecs.add(passwordPolicySpec2); + String generatedPassword = passwordGenerator.generate(passwordPolicySpecs); + assertTrue(Character.isDigit(generatedPassword.charAt(0))); + assertTrue(Character.isLetter(generatedPassword.charAt(generatedPassword.length() - 1))); + } + + @Test + public void passwordWithNonAlpha() + throws InvalidPasswordPolicySpecException { + + PasswordPolicySpec passwordPolicySpec = createBasePasswordPolicySpec(); + passwordPolicySpec.setNonAlphanumericRequired(true); + + PasswordPolicySpec passwordPolicySpec2 = createBasePasswordPolicySpec(); + passwordPolicySpec.setMustEndWithAlpha(true); + List<PasswordPolicySpec> passwordPolicySpecs = new ArrayList<>(); + passwordPolicySpecs.add(passwordPolicySpec); + passwordPolicySpecs.add(passwordPolicySpec2); + String generatedPassword = passwordGenerator.generate(passwordPolicySpecs); + assertTrue(PolicyPattern.NON_ALPHANUMERIC.matcher(generatedPassword).matches()); + assertTrue(Character.isLetter(generatedPassword.charAt(generatedPassword.length() - 1))); + } + + @Test(expected = InvalidPasswordPolicySpecException.class) + public void incopatiblePolicies() + throws InvalidPasswordPolicySpecException { + + PasswordPolicySpec passwordPolicySpec = createBasePasswordPolicySpec(); + passwordPolicySpec.setMinLength(12); + + PasswordPolicySpec passwordPolicySpec2 = createBasePasswordPolicySpec(); + passwordPolicySpec.setMaxLength(10); + + List<PasswordPolicySpec> passwordPolicySpecs = new ArrayList<>(); + passwordPolicySpecs.add(passwordPolicySpec); + passwordPolicySpecs.add(passwordPolicySpec2); + passwordGenerator.generate(passwordPolicySpecs); + } + + private PasswordPolicySpec createBasePasswordPolicySpec() { + PasswordPolicySpec basePasswordPolicySpec = new PasswordPolicySpec(); + basePasswordPolicySpec.setAlphanumericRequired(false); + basePasswordPolicySpec.setDigitRequired(false); + basePasswordPolicySpec.setLowercaseRequired(false); + basePasswordPolicySpec.setMaxLength(1000); + basePasswordPolicySpec.setMinLength(8); + basePasswordPolicySpec.setMustEndWithAlpha(false); + basePasswordPolicySpec.setMustEndWithDigit(false); + basePasswordPolicySpec.setMustEndWithNonAlpha(false); + basePasswordPolicySpec.setMustStartWithAlpha(false); + basePasswordPolicySpec.setMustStartWithDigit(false); + basePasswordPolicySpec.setMustStartWithNonAlpha(false); + basePasswordPolicySpec.setMustntEndWithAlpha(false); + basePasswordPolicySpec.setMustntEndWithDigit(false); + basePasswordPolicySpec.setMustntEndWithNonAlpha(false); + basePasswordPolicySpec.setMustntStartWithAlpha(false); + basePasswordPolicySpec.setMustntStartWithDigit(false); + basePasswordPolicySpec.setMustntStartWithNonAlpha(false); + basePasswordPolicySpec.setNonAlphanumericRequired(false); + basePasswordPolicySpec.setUppercaseRequired(false); + return basePasswordPolicySpec; + } +}
