diff --git a/servers/zms/conf/zms.properties b/servers/zms/conf/zms.properties index 6a125b1329a..5a672f98122 100644 --- a/servers/zms/conf/zms.properties +++ b/servers/zms/conf/zms.properties @@ -526,3 +526,10 @@ athenz.zms.no_auth_uri_list=/zms/v1/schema # fact that we want roles/groups to be reviewed every 90 days and the server # generates reminder notifications starting 28 days before the expiry date. #athenz.zms.review_days_percentage=68 + +# When handling requests for resource lists, the server needs access to +# all trust roles map. Getting the data from the DB is expensive, so we're +# caching the result set for the specified number of milliseconds so any +# deletions to the rows can be reflected in the result set. If there are +# additions then the server always fetches the latest data. +#athenz.zms.mysql_server_trust_roles_update_timeout=600000 diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSConsts.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSConsts.java index db482f1aa8e..6e3dcc9afea 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSConsts.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSConsts.java @@ -163,6 +163,7 @@ public final class ZMSConsts { public static final String ZMS_PROP_QUOTA_SERVICE_TAG = "athenz.zms.quota_service_tag"; public static final String ZMS_PROP_MYSQL_SERVER_TIMEZONE = "athenz.zms.mysql_server_timezone"; + public static final String ZMS_PROP_MYSQL_SERVER_TRUST_ROLES_UPDATE_TIMEOUT = "athenz.zms.mysql_server_trust_roles_update_timeout"; public static final String ZMS_PRINCIPAL_AUTHORITY_CLASS = "com.yahoo.athenz.auth.impl.PrincipalAuthority"; diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java index 27ce1a9c701..7edd9365570 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java @@ -8961,11 +8961,11 @@ public Status getStatus(ResourceContext ctx) { validateRequest(ctx.request(), caller, true); - // for now we're going to verify our database connectivity - // in case of failure we're going to return not found + // we're going to verify our database connectivity + // by listing our system domains. In case of failure + // we're going to return not found - DomainList dlist = listDomains(null, null, null, null, 0, false); - if (dlist.getNames() == null || dlist.getNames().isEmpty()) { + if (dbService.listDomains(SYS_AUTH, 0, false).isEmpty()) { throw ZMSUtils.notFoundError("Error - no domains available", caller); } diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnection.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnection.java index 7db8cc6e71e..6525d1f41de 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnection.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnection.java @@ -47,7 +47,6 @@ public class JDBCConnection implements ObjectStoreConnection { private static final String MYSQL_EXC_STATE_DEADLOCK = "40001"; private static final String MYSQL_EXC_STATE_COMM_ERROR = "08S01"; - private static final String SQL_TABLE_DOMAIN = "domain"; private static final String SQL_TABLE_ROLE = "role"; private static final String SQL_TABLE_ROLE_MEMBER = "role_member"; @@ -675,6 +674,9 @@ public class JDBCConnection implements ObjectStoreConnection { + "JOIN domain ON domain_contacts.domain_id=domain.domain_id " + "WHERE domain_contacts.name=?;"; private static final String SQL_LIST_DOMAIN_CONTACTS = "SELECT type, name FROM domain_contacts WHERE domain_id=?;"; + private static final String SQL_GET_LAST_ASSUME_ROLE_ASSERTION = "SELECT policy.modified FROM policy " + + " JOIN assertion ON policy.policy_id=assertion.policy_id WHERE assertion.action='assume_role' " + + " ORDER BY policy.modified DESC LIMIT 1"; private static final String CACHE_DOMAIN = "d:"; private static final String CACHE_ROLE = "r:"; @@ -698,6 +700,10 @@ public class JDBCConnection implements ObjectStoreConnection { Map objectMap; boolean transactionCompleted; DomainOptions domainOptions; + private static Map> SERVER_TRUST_ROLES_MAP; + private static long SERVER_TRUST_ROLES_TIMESTAMP; + private static final long SERVER_TRUST_ROLES_UPDATE_TIMEOUT = Long.parseLong( + System.getProperty(ZMSConsts.ZMS_PROP_MYSQL_SERVER_TRUST_ROLES_UPDATE_TIMEOUT, "600000")); public JDBCConnection(Connection con, boolean autoCommit) throws SQLException { this.con = con; @@ -706,6 +712,14 @@ public JDBCConnection(Connection con, boolean autoCommit) throws SQLException { objectMap = new HashMap<>(); } + /** + * Used only by the test classes to reset the server trust roles map + */ + void resetTrustRolesMap() { + SERVER_TRUST_ROLES_MAP = null; + SERVER_TRUST_ROLES_TIMESTAMP = 0; + } + @Override public void setDomainOptions(DomainOptions domainOptions) { this.domainOptions = domainOptions; @@ -4612,12 +4626,75 @@ void getTrustedSubTypeRoles(String sqlCommand, Map> trusted } } + long lastTrustRoleUpdatesTimestamp() { + + final String caller = "lastTrustRoleUpdatesTimestamp"; + + long timeStamp = 0; + try (PreparedStatement ps = con.prepareStatement(SQL_GET_LAST_ASSUME_ROLE_ASSERTION)) { + try (ResultSet rs = executeQuery(ps, caller)) { + if (rs.next()) { + timeStamp = rs.getTimestamp(ZMSConsts.DB_COLUMN_MODIFIED).getTime(); + } + } + } catch (SQLException ignored) { + } + + return timeStamp; + } + Map> getTrustedRoles(String caller) { + // if our last timestamp has passed our timeout or our map has not been + // initialized, then we need to update our trust map so need for any + // extra timestamp checks + + long now = System.currentTimeMillis(); + if (SERVER_TRUST_ROLES_MAP == null || now - SERVER_TRUST_ROLES_TIMESTAMP > SERVER_TRUST_ROLES_UPDATE_TIMEOUT) { + updateTrustRolesMap(now, true, caller); + + } else { + + // we want to make sure to capture any additions right away, so we'll get + // the last modification timestamp of the latest policy that has an assume_role + // assertion + + long lastTimeStamp = lastTrustRoleUpdatesTimestamp(); + if (lastTimeStamp > SERVER_TRUST_ROLES_TIMESTAMP) { + updateTrustRolesMap(lastTimeStamp, false, caller); + } + } + + return SERVER_TRUST_ROLES_MAP; + } + + synchronized void updateTrustRolesMap(long lastTimeStamp, boolean timeoutUpdate, final String caller) { + + // a couple of simple checks in case we already have a valid + // map to see if we can skip updating the map + + if (SERVER_TRUST_ROLES_MAP != null) { + + // if our last timestamp is older than the one we have + // then we're going to skip the update + + if (SERVER_TRUST_ROLES_TIMESTAMP >= lastTimeStamp) { + return; + } + + // if this is a timeout update we're going to check if the map + // has already been updated by another thread while we were waiting + + if (timeoutUpdate && lastTimeStamp - SERVER_TRUST_ROLES_TIMESTAMP < SERVER_TRUST_ROLES_UPDATE_TIMEOUT) { + return; + } + } + Map> trustedRoles = new HashMap<>(); getTrustedSubTypeRoles(SQL_LIST_TRUSTED_STANDARD_ROLES, trustedRoles, caller); getTrustedSubTypeRoles(SQL_LIST_TRUSTED_WILDCARD_ROLES, trustedRoles, caller); - return trustedRoles; + SERVER_TRUST_ROLES_TIMESTAMP = lastTimeStamp; + SERVER_TRUST_ROLES_MAP = trustedRoles; } void addRoleAssertions(List principalAssertions, List roleAssertions) { diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnectionTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnectionTest.java index f6596086b2c..f1ae5ab125e 100644 --- a/servers/zms/src/test/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnectionTest.java +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/store/impl/jdbc/JDBCConnectionTest.java @@ -7435,11 +7435,16 @@ public void testGetRolePrincipals() throws Exception { public void testGetTrustedRoles() throws Exception { JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + jdbcConn.resetTrustRolesMap(); Mockito.when(mockResultSet.next()) .thenReturn(true) .thenReturn(true) .thenReturn(true) + .thenReturn(false) + .thenReturn(false) // end of first call + .thenReturn(true) // for timestamp lookup + .thenReturn(true) // single entry returned .thenReturn(false); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_NAME)) .thenReturn("trole1") @@ -7457,6 +7462,9 @@ public void testGetTrustedRoles() throws Exception { .thenReturn("101") .thenReturn("101") .thenReturn("103"); + long now = System.currentTimeMillis(); + Mockito.when(mockResultSet.getTimestamp(ZMSConsts.DB_COLUMN_MODIFIED)) + .thenReturn(new java.sql.Timestamp(now + 30000)); Map> trustedRoles = jdbcConn.getTrustedRoles("getTrustedRoles"); assertEquals(2, trustedRoles.size()); @@ -7472,6 +7480,29 @@ public void testGetTrustedRoles() throws Exception { assertEquals("103:trole3", roles.get(0)); + trustedRoles = jdbcConn.getTrustedRoles("getTrustedRoles"); + assertEquals(1, trustedRoles.size()); + + roles = trustedRoles.get("103:role3"); + assertEquals(1, roles.size()); + + assertEquals("103:trole3", roles.get(0)); + + // when we call the update trust map with timestamp in the past, it should return + // right away without making any mysql calls + + jdbcConn.updateTrustRolesMap(now - 30000, false, "trustMapTest"); + + // second time calling the api should now give us an empty map + // since our values will no longer match + + jdbcConn.updateTrustRolesMap(now + 60000, false, "trustMapTest"); + assertTrue(jdbcConn.getTrustedRoles("getTrustedRoles").isEmpty()); + + // specifying timeout update with a second difference should not + // trigger an update + + jdbcConn.updateTrustRolesMap(now + 60001, true, "trustMapTest"); jdbcConn.close(); } @@ -7656,6 +7687,7 @@ public void testSqlError() throws SQLException { public void testListResourceAccessNotRegisteredRolePrincipals() throws SQLException { JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + jdbcConn.resetTrustRolesMap(); // no role principals @@ -7678,6 +7710,7 @@ public void testListResourceAccessNotRegisteredRolePrincipals() throws SQLExcept public void testListResourceAccessRegisteredRolePrincipals() throws SQLException { JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + jdbcConn.resetTrustRolesMap(); // no role principals @@ -7705,6 +7738,7 @@ public void testListResourceAccessRegisteredRolePrincipals() throws SQLException public void testListResourceAccessEmptyRoleAssertions() throws SQLException { JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + jdbcConn.resetTrustRolesMap(); Mockito.when(mockResultSet.next()) .thenReturn(true) @@ -7743,6 +7777,7 @@ public void testListResourceAccessEmptyRoleAssertions() throws SQLException { public void testListResourceAccess() throws SQLException { JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + jdbcConn.resetTrustRolesMap(); Mockito.when(mockResultSet.next()) .thenReturn(true) @@ -7808,6 +7843,7 @@ public void testListResourceAccess() throws SQLException { public void testListResourceAccessAws() throws SQLException { JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + jdbcConn.resetTrustRolesMap(); Mockito.when(mockResultSet.next()) .thenReturn(true) @@ -7819,6 +7855,7 @@ public void testListResourceAccessAws() throws SQLException { .thenReturn(true) .thenReturn(true) .thenReturn(false) // up to here is role assertions + .thenReturn(true) // this is for last modified timestamp .thenReturn(true) .thenReturn(true) .thenReturn(true) @@ -7827,6 +7864,8 @@ public void testListResourceAccessAws() throws SQLException { .thenReturn(true) .thenReturn(true) .thenReturn(false); // up to here is aws domains + Mockito.when(mockResultSet.getTimestamp(ZMSConsts.DB_COLUMN_MODIFIED)) + .thenReturn(new java.sql.Timestamp(1454358916)); Mockito.when(mockResultSet.getString(ZMSConsts.DB_COLUMN_NAME)) .thenReturn("dom1") .thenReturn("dom2") @@ -7893,6 +7932,7 @@ public void testListResourceAccessAws() throws SQLException { jdbcConn.close(); } + @Test public void testGetResourceAccessObject() throws SQLException { @@ -15796,4 +15836,13 @@ public void testGetDomainContactsException() throws Exception { } jdbcConn.close(); } + + @Test + public void testLastTrustRoleUpdatesTimestampException() throws Exception { + + JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + Mockito.when(mockPrepStmt.executeQuery()).thenThrow(new SQLException("failed operation", "state", 1001)); + assertEquals(jdbcConn.lastTrustRoleUpdatesTimestamp(), 0); + jdbcConn.close(); + } }