Skip to content

Commit

Permalink
0004360: Record node login failures and prevent too many logins
Browse files Browse the repository at this point in the history
  • Loading branch information
erilong committed Apr 22, 2020
1 parent 0b6f64a commit 28b6fab
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 10 deletions.
Expand Up @@ -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";
Expand Down
Expand Up @@ -54,7 +54,9 @@ public class NodeSecurity implements Serializable {
private long revInitialLoadId;

private String revInitialLoadCreateBy;


private int failedLogins;

private String createdAtNodeId;

public String getNodeId() {
Expand Down Expand Up @@ -173,4 +175,12 @@ public long getRevInitialLoadId() {
return revInitialLoadId;
}

public int getFailedLogins() {
return failedLogins;
}

public void setFailedLogins(int failedLogins) {
this.failedLogins = failedLogins;
}

}
Expand Up @@ -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);
Expand Down Expand Up @@ -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);

}
Expand Up @@ -547,16 +547,28 @@ public Map<String, NodeSecurity> 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<String, NodeSecurity> 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<String, NodeSecurity> nodeSecurities = findAllNodeSecurity(true);
NodeSecurity nodeSecurity = nodeSecurities.get(nodeId);
return nodeSecurity != null && nodeSecurity.getFailedLogins() > maxFailedLogins;
}
return false;
}

public void flushNodeAuthorizedCache() {
securityCacheTime = 0;
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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<String, NodeSecurity> 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<String, NodeSecurity> 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) {
Expand Down
Expand Up @@ -78,7 +78,7 @@ public NodeServiceSqlMap(IDatabasePlatform platform, Map<String, String> 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 = ?");

Expand All @@ -89,15 +89,15 @@ public NodeServiceSqlMap(IDatabasePlatform platform, Map<String, String> 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 ");

putSql("findAllNodeSecuritySql",
"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 = ?");
Expand Down Expand Up @@ -163,7 +163,7 @@ public NodeServiceSqlMap(IDatabasePlatform platform, Map<String, String> 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",
Expand Down
Expand Up @@ -2359,6 +2359,13 @@ extensions.xml=<?xml version="1.0" encoding="UTF-8"?> \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
Expand Down
1 change: 1 addition & 0 deletions symmetric-core/src/main/resources/symmetric-schema.xml
Expand Up @@ -573,6 +573,7 @@
<column name="rev_initial_load_time" type="TIMESTAMP" required="false" description="The timestamp when this node last sent an initial load." />
<column name="rev_initial_load_id" type="BIGINT" description="A reference to the load_id in outgoing_batch for the last reverse load that occurred." />
<column name="rev_initial_load_create_by" type="VARCHAR" size="255" description="The user that created the reverse initial load. A null value means that the system created the batch." />
<column name="failed_logins" type="TINYINT" default="0" description="Number of failed login attempts" />
<column name="created_at_node_id" type="VARCHAR" size="50" description="The node_id of the node where this node was created. This is typically filled automatically with the node_id found in node_identity where registration was opened for the node. " />
<foreign-key foreignTable="node" name="fk_sec_2_node">
<reference local="node_id" foreign="node_id" />
Expand Down
Expand Up @@ -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) {
}
}
Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down

0 comments on commit 28b6fab

Please sign in to comment.