Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

maintain a static map of trusted roles for resource list lookup api #2476

Merged
merged 1 commit into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions servers/zms/conf/zms.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
8 changes: 4 additions & 4 deletions servers/zms/src/main/java/com/yahoo/athenz/zms/ZMSImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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:";
Expand All @@ -698,6 +700,10 @@ public class JDBCConnection implements ObjectStoreConnection {
Map<String, Integer> objectMap;
boolean transactionCompleted;
DomainOptions domainOptions;
private static Map<String, List<String>> 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;
Expand All @@ -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;
Expand Down Expand Up @@ -4612,12 +4626,75 @@ void getTrustedSubTypeRoles(String sqlCommand, Map<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<Assertion> principalAssertions, List<Assertion> roleAssertions) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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<String, List<String>> trustedRoles = jdbcConn.getTrustedRoles("getTrustedRoles");
assertEquals(2, trustedRoles.size());
Expand All @@ -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();
}

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

Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -7893,6 +7932,7 @@ public void testListResourceAccessAws() throws SQLException {

jdbcConn.close();
}

@Test
public void testGetResourceAccessObject() throws SQLException {

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