diff --git a/symmetric-core/src/main/java/org/jumpmind/symmetric/common/ParameterConstants.java b/symmetric-core/src/main/java/org/jumpmind/symmetric/common/ParameterConstants.java index a183676bf7..2434f7293e 100644 --- a/symmetric-core/src/main/java/org/jumpmind/symmetric/common/ParameterConstants.java +++ b/symmetric-core/src/main/java/org/jumpmind/symmetric/common/ParameterConstants.java @@ -353,6 +353,7 @@ private ParameterConstants() { public final static String WEB_BATCH_URI_HANDLER_ENABLE = "web.batch.servlet.enable"; public final static String NODE_COPY_MODE_ENABLED = "node.copy.mode.enabled"; + public final static String NODE_PASSWORD_FAILED_ATTEMPTS = "node.password.failed.attempts"; public final static String NODE_OFFLINE = "node.offline"; public final static String NODE_OFFLINE_INCOMING_DIR = "node.offline.incoming.dir"; diff --git a/symmetric-core/src/main/java/org/jumpmind/symmetric/model/NodeSecurity.java b/symmetric-core/src/main/java/org/jumpmind/symmetric/model/NodeSecurity.java index b89013922d..d1f55f6ff3 100644 --- a/symmetric-core/src/main/java/org/jumpmind/symmetric/model/NodeSecurity.java +++ b/symmetric-core/src/main/java/org/jumpmind/symmetric/model/NodeSecurity.java @@ -54,7 +54,9 @@ public class NodeSecurity implements Serializable { private long revInitialLoadId; private String revInitialLoadCreateBy; - + + private int failedLogins; + private String createdAtNodeId; public String getNodeId() { @@ -173,4 +175,12 @@ public long getRevInitialLoadId() { return revInitialLoadId; } + public int getFailedLogins() { + return failedLogins; + } + + public void setFailedLogins(int failedLogins) { + this.failedLogins = failedLogins; + } + } \ No newline at end of file diff --git a/symmetric-core/src/main/java/org/jumpmind/symmetric/service/INodeService.java b/symmetric-core/src/main/java/org/jumpmind/symmetric/service/INodeService.java index f0cd7185c1..252d90acad 100644 --- a/symmetric-core/src/main/java/org/jumpmind/symmetric/service/INodeService.java +++ b/symmetric-core/src/main/java/org/jumpmind/symmetric/service/INodeService.java @@ -43,7 +43,7 @@ public interface INodeService { public enum AuthenticationStatus { - SYNC_DISABLED, REGISTRATION_REQUIRED, FORBIDDEN, ACCEPTED; + SYNC_DISABLED, REGISTRATION_REQUIRED, FORBIDDEN, ACCEPTED, LOCKED; }; public Node findNode(String nodeId); @@ -209,5 +209,8 @@ public void ignoreNodeChannelForExternalId(boolean ignore, String channelId, public AuthenticationStatus getAuthenticationStatus(String nodeId, String securityToken); + public void resetNodeFailedLogins(String nodeId); + + public void incrementNodeFailedLogins(String nodeId); } \ No newline at end of file diff --git a/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/NodeService.java b/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/NodeService.java index e1644fbae1..5a3e90ae38 100644 --- a/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/NodeService.java +++ b/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/NodeService.java @@ -547,16 +547,28 @@ public Map findAllNodeSecurity(boolean useCache) { * A node must authenticate before it's allowed to sync data. */ public boolean isNodeAuthorized(String nodeId, String password) { + int maxFailedLogins = parameterService.getInt(ParameterConstants.NODE_PASSWORD_FAILED_ATTEMPTS); Map nodeSecurities = findAllNodeSecurity(true); NodeSecurity nodeSecurity = nodeSecurities.get(nodeId); if (nodeSecurity != null && !nodeId.equals(findIdentityNodeId()) && ((nodeSecurity.getNodePassword() != null && !nodeSecurity.getNodePassword().equals("") - && nodeSecurity.getNodePassword().equals(password)) || nodeSecurity.isRegistrationEnabled())) { + && nodeSecurity.getNodePassword().equals(password)) || nodeSecurity.isRegistrationEnabled()) + && (maxFailedLogins <= 0 || nodeSecurity.getFailedLogins() <= maxFailedLogins) || nodeSecurity.isRegistrationEnabled()) { return true; } return false; } + protected boolean isNodeAuthorizationLocked(String nodeId) { + int maxFailedLogins = parameterService.getInt(ParameterConstants.NODE_PASSWORD_FAILED_ATTEMPTS); + if (maxFailedLogins > 0) { + Map nodeSecurities = findAllNodeSecurity(true); + NodeSecurity nodeSecurity = nodeSecurities.get(nodeId); + return nodeSecurity != null && nodeSecurity.getFailedLogins() > maxFailedLogins; + } + return false; + } + public void flushNodeAuthorizedCache() { securityCacheTime = 0; } @@ -597,10 +609,11 @@ public boolean updateNodeSecurity(ISqlTransaction transaction, NodeSecurity secu security.getInitialLoadCreateBy(), security.getRevInitialLoadId(), security.getRevInitialLoadCreateBy(), + security.getFailedLogins(), security.getNodeId() }, new int[] { Types.VARCHAR, Types.INTEGER, Types.TIMESTAMP, Types.INTEGER, Types.TIMESTAMP, Types.VARCHAR, Types.INTEGER, Types.TIMESTAMP, - Types.BIGINT, Types.VARCHAR, Types.BIGINT, Types.VARCHAR, + Types.BIGINT, Types.VARCHAR, Types.BIGINT, Types.VARCHAR, Types.INTEGER, Types.VARCHAR }); boolean updated = (updateCount == 1); flushNodeAuthorizedCache(); @@ -898,6 +911,7 @@ public NodeSecurity mapRow(Row rs) { nodeSecurity.setInitialLoadCreateBy(rs.getString("initial_load_create_by")); nodeSecurity.setRevInitialLoadId(rs.getLong("rev_initial_load_id")); nodeSecurity.setRevInitialLoadCreateBy(rs.getString("rev_initial_load_create_by")); + nodeSecurity.setFailedLogins(rs.getInt("failed_logins")); return nodeSecurity; } } @@ -941,11 +955,46 @@ public AuthenticationStatus getAuthenticationStatus(String nodeId, String securi retVal = AuthenticationStatus.SYNC_DISABLED; } } else if (!isNodeAuthorized(nodeId, securityToken)) { - retVal = AuthenticationStatus.FORBIDDEN; + if (isNodeAuthorizationLocked(nodeId) ) { + retVal = AuthenticationStatus.LOCKED; + } else { + retVal = AuthenticationStatus.FORBIDDEN; + } } return retVal; } + public void resetNodeFailedLogins(String nodeId) { + if (parameterService.getInt(ParameterConstants.NODE_PASSWORD_FAILED_ATTEMPTS) >= 0) { + Map nodeSecurities = findAllNodeSecurity(true); + NodeSecurity nodeSecurity = nodeSecurities.get(nodeId); + if (nodeSecurity != null && nodeSecurity.getFailedLogins() > 0) { + nodeSecurity.setFailedLogins(0); + nodeSecurity = findNodeSecurity(nodeId); + if (nodeSecurity != null && nodeSecurity.getFailedLogins() > 0) { + nodeSecurity.setFailedLogins(0); + updateNodeSecurity(nodeSecurity); + } + } + } + } + + public void incrementNodeFailedLogins(String nodeId) { + if (parameterService.getInt(ParameterConstants.NODE_PASSWORD_FAILED_ATTEMPTS) >= 0) { + NodeSecurity nodeSecurity = findNodeSecurity(nodeId); + if (nodeSecurity != null) { + nodeSecurity.setFailedLogins(nodeSecurity.getFailedLogins() + 1); + updateNodeSecurity(nodeSecurity); + } + + Map cache = findAllNodeSecurity(true); + NodeSecurity cacheSecurity = cache.get(nodeId); + if (cacheSecurity != null) { + cacheSecurity.setFailedLogins(nodeSecurity.getFailedLogins()); + } + } + } + protected boolean syncEnabled(Node node) { boolean syncEnabled = false; if (node != null) { diff --git a/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/NodeServiceSqlMap.java b/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/NodeServiceSqlMap.java index 0df0c1b744..24067f09e3 100644 --- a/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/NodeServiceSqlMap.java +++ b/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/NodeServiceSqlMap.java @@ -78,7 +78,7 @@ public NodeServiceSqlMap(IDatabasePlatform platform, Map replace "select node_id, node_password, registration_enabled, registration_time, " + " initial_load_enabled, initial_load_time, created_at_node_id, " + " rev_initial_load_enabled, rev_initial_load_time, initial_load_id, " + - " initial_load_create_by, rev_initial_load_id, rev_initial_load_create_by " + + " initial_load_create_by, rev_initial_load_id, rev_initial_load_create_by, failed_logins " + " from $(node_security) where " + " node_id = ?"); @@ -89,7 +89,7 @@ public NodeServiceSqlMap(IDatabasePlatform platform, Map replace "select node_id, node_password, registration_enabled, registration_time, " + " initial_load_enabled, initial_load_time, created_at_node_id, " + " rev_initial_load_enabled, rev_initial_load_time, initial_load_id, " - + " initial_load_create_by, rev_initial_load_id, rev_initial_load_create_by " + + " initial_load_create_by, rev_initial_load_id, rev_initial_load_create_by, failed_logins " + " from $(node_security) " + " where initial_load_enabled=1 or rev_initial_load_enabled=1 "); @@ -97,7 +97,7 @@ public NodeServiceSqlMap(IDatabasePlatform platform, Map replace "select node_id, node_password, registration_enabled, registration_time, " + " initial_load_enabled, initial_load_time, created_at_node_id, " + " rev_initial_load_enabled, rev_initial_load_time, initial_load_id, " + - " initial_load_create_by, rev_initial_load_id, rev_initial_load_create_by " + + " initial_load_create_by, rev_initial_load_id, rev_initial_load_create_by, failed_logins " + " from $(node_security) "); putSql("deleteNodeSecuritySql", "delete from $(node_security) where node_id = ?"); @@ -163,7 +163,7 @@ public NodeServiceSqlMap(IDatabasePlatform platform, Map replace + "update $(node_security) set node_password = ?, registration_enabled = ?, " + " registration_time = ?, initial_load_enabled = ?, initial_load_time = ?, created_at_node_id = ?," + " rev_initial_load_enabled=?, rev_initial_load_time=?, initial_load_id=?, " + - " initial_load_create_by=?, rev_initial_load_id=?, rev_initial_load_create_by=? " + + " initial_load_create_by=?, rev_initial_load_id=?, rev_initial_load_create_by=?, failed_logins=? " + " where node_id = ? "); putSql("insertNodeSecuritySql", diff --git a/symmetric-core/src/main/resources/symmetric-default.properties b/symmetric-core/src/main/resources/symmetric-default.properties index 68bd344c4b..963c60c3a4 100644 --- a/symmetric-core/src/main/resources/symmetric-default.properties +++ b/symmetric-core/src/main/resources/symmetric-default.properties @@ -2359,6 +2359,13 @@ extensions.xml= \n\ # Type: boolean node.copy.mode.enabled=false +# Number of failed login attempts by a node before lockout (0 = never lockout, -1 = never lockout or record) +# +# DatabaseOverridable: true +# Tags: password +# Type: integer +node.password.failed.attempts=5 + # Maximum number of rows to write to file before copying to S3 and running with COPY statement # # DatabaseOverridable: true diff --git a/symmetric-core/src/main/resources/symmetric-schema.xml b/symmetric-core/src/main/resources/symmetric-schema.xml index 40aaa2fedd..6fdae66d32 100644 --- a/symmetric-core/src/main/resources/symmetric-schema.xml +++ b/symmetric-core/src/main/resources/symmetric-schema.xml @@ -573,6 +573,7 @@ + diff --git a/symmetric-core/src/test/java/org/jumpmind/symmetric/service/impl/MockNodeService.java b/symmetric-core/src/test/java/org/jumpmind/symmetric/service/impl/MockNodeService.java index 122f36a059..978b6c114f 100644 --- a/symmetric-core/src/test/java/org/jumpmind/symmetric/service/impl/MockNodeService.java +++ b/symmetric-core/src/test/java/org/jumpmind/symmetric/service/impl/MockNodeService.java @@ -339,4 +339,12 @@ public AuthenticationStatus getAuthenticationStatus(String nodeId, String securi public Node findRootNode() { return null; } + + @Override + public void resetNodeFailedLogins(String nodeId) { + } + + @Override + public void incrementNodeFailedLogins(String nodeId) { + } } \ No newline at end of file diff --git a/symmetric-server/src/main/java/org/jumpmind/symmetric/web/AuthenticationInterceptor.java b/symmetric-server/src/main/java/org/jumpmind/symmetric/web/AuthenticationInterceptor.java index 354669426f..96d4c27af8 100644 --- a/symmetric-server/src/main/java/org/jumpmind/symmetric/web/AuthenticationInterceptor.java +++ b/symmetric-server/src/main/java/org/jumpmind/symmetric/web/AuthenticationInterceptor.java @@ -106,6 +106,7 @@ public boolean before(HttpServletRequest req, HttpServletResponse resp) throws I if (AuthenticationStatus.ACCEPTED.equals(status)) { log.debug("Node '{}' successfully authenticated", nodeId); + nodeService.resetNodeFailedLogins(nodeId); return true; } else if (AuthenticationStatus.REGISTRATION_REQUIRED.equals(status)) { log.debug("Node '{}' failed to authenticate. It was not registered", nodeId); @@ -116,7 +117,12 @@ public boolean before(HttpServletRequest req, HttpServletResponse resp) throws I ServletUtils.sendError(resp, WebConstants.SYNC_DISABLED); return false; } else { - log.warn("Node '{}' failed to authenticate. It had the wrong password", nodeId); + if (AuthenticationStatus.LOCKED.equals(status)) { + log.warn("Node '{}' failed to authenticate. It had too many login attempts", nodeId); + } else { + log.warn("Node '{}' failed to authenticate. It had the wrong password", nodeId); + nodeService.incrementNodeFailedLogins(nodeId); + } ServletUtils.sendError(resp, WebConstants.SC_FORBIDDEN); return false; }