From 84ce9aa03875f485eeea870d4c1bbfbe5d8939d7 Mon Sep 17 00:00:00 2001 From: Henry Avetisyan Date: Tue, 24 Mar 2026 14:09:46 -0700 Subject: [PATCH] support for exteral member validator manager Signed-off-by: Henry Avetisyan --- .../server/store/ObjectStoreConnection.java | 4 + .../server/store/impl/JDBCConnection.java | 19 + .../server/store/impl/JDBCConnectionTest.java | 54 ++ .../java/com/yahoo/athenz/zms/DBService.java | 8 + .../zms/ExternalMemberValidatorManager.java | 155 ++++++ .../java/com/yahoo/athenz/zms/ZMSConsts.java | 3 + .../com/yahoo/athenz/zms/DBServiceTest.java | 48 ++ .../ExternalMemberValidatorManagerTest.java | 498 ++++++++++++++++++ .../zms/TestExternalMemberValidator.java | 26 + 9 files changed, 815 insertions(+) create mode 100644 servers/zms/src/main/java/com/yahoo/athenz/zms/ExternalMemberValidatorManager.java create mode 100644 servers/zms/src/test/java/com/yahoo/athenz/zms/ExternalMemberValidatorManagerTest.java create mode 100644 servers/zms/src/test/java/com/yahoo/athenz/zms/TestExternalMemberValidator.java diff --git a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/store/ObjectStoreConnection.java b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/store/ObjectStoreConnection.java index 827345ad0fd..a922fbb2fb8 100644 --- a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/store/ObjectStoreConnection.java +++ b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/store/ObjectStoreConnection.java @@ -20,6 +20,7 @@ import com.yahoo.rdl.Timestamp; import java.io.Closeable; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -56,6 +57,9 @@ public interface ObjectStoreConnection extends Closeable { AthenzDomain getAthenzDomain(String domainName) throws ServerResourceException; DomainMetaList listModifiedDomains(long modifiedSince) throws ServerResourceException; void setDomainOptions(DomainOptions domainOptions) throws ServerResourceException; + default Map listDomainsWithExternalMemberValidator() throws ServerResourceException { + return Collections.emptyMap(); + } // Domain tags diff --git a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/store/impl/JDBCConnection.java b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/store/impl/JDBCConnection.java index f42e90fc5a8..c0138e3d36a 100644 --- a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/store/impl/JDBCConnection.java +++ b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/server/store/impl/JDBCConnection.java @@ -87,6 +87,7 @@ public class JDBCConnection implements ObjectStoreConnection { private static final String SQL_GET_DOMAIN_WITH_YPM_ID = "SELECT name FROM domain WHERE ypm_id=?;"; private static final String SQL_GET_DOMAIN_WITH_PRODUCT_ID = "SELECT name FROM domain WHERE product_id=?;"; private static final String SQL_LIST_DOMAIN_WITH_BUSINESS_SERVICE = "SELECT name FROM domain WHERE business_service=?;"; + private static final String SQL_LIST_DOMAINS_WITH_EXTERNAL_MEMBER_VALIDATOR = "SELECT name, external_member_validator FROM domain WHERE external_member_validator!='';"; private static final String SQL_INSERT_DOMAIN = "INSERT INTO domain " + "(name, description, org, uuid, enabled, audit_enabled, account, ypm_id, application_id, cert_dns_domain," + " member_expiry_days, token_expiry_mins, service_cert_expiry_mins, role_cert_expiry_mins, sign_algorithm," @@ -1490,6 +1491,24 @@ public Map listDomainsByCloudProvider(String provider) throws Se return domains; } + @Override + public Map listDomainsWithExternalMemberValidator() throws ServerResourceException { + + final String caller = "listDomainsWithExternalMemberValidator"; + Map domains = new HashMap<>(); + try (PreparedStatement ps = con.prepareStatement(SQL_LIST_DOMAINS_WITH_EXTERNAL_MEMBER_VALIDATOR)) { + try (ResultSet rs = executeQuery(ps, caller)) { + while (rs.next()) { + domains.put(rs.getString(JDBCConsts.DB_COLUMN_NAME), + rs.getString(JDBCConsts.DB_COLUMN_EXTERNAL_MEMBER_VALIDATOR)); + } + } + } catch (SQLException ex) { + throw sqlError(ex, caller); + } + return domains; + } + @Override public List listDomains(String prefix, long modifiedSince) throws ServerResourceException { diff --git a/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/store/impl/JDBCConnectionTest.java b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/store/impl/JDBCConnectionTest.java index 623dc9afc7e..ab1ee27484e 100644 --- a/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/store/impl/JDBCConnectionTest.java +++ b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/server/store/impl/JDBCConnectionTest.java @@ -18002,4 +18002,58 @@ public void testEnforceGroupAuditLogLimitCleanUpException() throws SQLException Mockito.verify(mockPrepStmt, times(1)).executeUpdate(); jdbcConn.close(); } + + @Test + public void testListDomainsWithExternalMemberValidator() throws Exception { + + JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + + Mockito.when(mockResultSet.next()) + .thenReturn(true) + .thenReturn(true) + .thenReturn(false); + Mockito.when(mockResultSet.getString(JDBCConsts.DB_COLUMN_NAME)) + .thenReturn("domain1") + .thenReturn("domain2"); + Mockito.when(mockResultSet.getString(JDBCConsts.DB_COLUMN_EXTERNAL_MEMBER_VALIDATOR)) + .thenReturn("com.yahoo.athenz.ValidatorA") + .thenReturn("com.yahoo.athenz.ValidatorB"); + + Map domains = jdbcConn.listDomainsWithExternalMemberValidator(); + assertEquals(domains.size(), 2); + assertEquals(domains.get("domain1"), "com.yahoo.athenz.ValidatorA"); + assertEquals(domains.get("domain2"), "com.yahoo.athenz.ValidatorB"); + + jdbcConn.close(); + } + + @Test + public void testListDomainsWithExternalMemberValidatorEmpty() throws Exception { + + JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + + Mockito.when(mockResultSet.next()).thenReturn(false); + + Map domains = jdbcConn.listDomainsWithExternalMemberValidator(); + assertTrue(domains.isEmpty()); + + jdbcConn.close(); + } + + @Test + public void testListDomainsWithExternalMemberValidatorException() throws Exception { + + JDBCConnection jdbcConn = new JDBCConnection(mockConn, true); + + Mockito.when(mockPrepStmt.executeQuery()).thenThrow(new SQLException("sql error")); + + try { + jdbcConn.listDomainsWithExternalMemberValidator(); + fail(); + } catch (ServerResourceException ex) { + assertTrue(ex.getMessage().contains("sql error")); + } + + jdbcConn.close(); + } } diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java index fca2a2757bc..22991a08923 100644 --- a/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/DBService.java @@ -3586,6 +3586,14 @@ DomainList lookupDomainByRole(String roleMember, String roleName) { return domList; } + Map getDomainsWithExternalMemberValidator() { + try (ObjectStoreConnection con = store.getConnection(true, true)) { + return con.listDomainsWithExternalMemberValidator(); + } catch (ServerResourceException ex) { + throw ZMSUtils.error(ex); + } + } + List listRoles(String domainName) { return listRoles(domainName, false); } diff --git a/servers/zms/src/main/java/com/yahoo/athenz/zms/ExternalMemberValidatorManager.java b/servers/zms/src/main/java/com/yahoo/athenz/zms/ExternalMemberValidatorManager.java new file mode 100644 index 00000000000..663b485027a --- /dev/null +++ b/servers/zms/src/main/java/com/yahoo/athenz/zms/ExternalMemberValidatorManager.java @@ -0,0 +1,155 @@ +/* + * Copyright The Athenz Authors + * + * Licensed 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 com.yahoo.athenz.zms; + +import com.yahoo.athenz.auth.ExternalMemberValidator; +import com.yahoo.athenz.zms.utils.ZMSUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static com.yahoo.athenz.zms.ZMSConsts.*; + +public class ExternalMemberValidatorManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(ExternalMemberValidatorManager.class); + + private ScheduledExecutorService scheduledExecutor; + private final DBService dbService; + private final ConcurrentHashMap validators = new ConcurrentHashMap<>(); + private volatile Map domainValidatorClasses = Collections.emptyMap(); + + public ExternalMemberValidatorManager(final DBService dbService) { + LOGGER.info("Initializing ExternalMemberValidatorManager..."); + this.dbService = dbService; + refreshValidators(); + init(); + } + + private void init() { + scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); + long frequencyHours = Long.parseLong( + System.getProperty(ZMS_PROP_EXTERNAL_MEMBER_VALIDATOR_FREQUENCY_HOURS, + ZMS_PROP_EXTERNAL_MEMBER_VALIDATOR_FREQUENCY_HOURS_DEFAULT)); + scheduledExecutor.scheduleAtFixedRate(this::refreshValidators, frequencyHours, + frequencyHours, TimeUnit.HOURS); + } + + public void shutdown() { + if (scheduledExecutor != null) { + scheduledExecutor.shutdownNow(); + } + } + + void refreshValidators() { + + LOGGER.info("Refreshing external member validators from datastore"); + + Map currentDomainValidators; + try { + currentDomainValidators = dbService.getDomainsWithExternalMemberValidator(); + } catch (Exception ex) { + LOGGER.error("Unable to fetch domains with external member validators: {}", ex.getMessage()); + return; + } + + for (String domainName : validators.keySet()) { + if (!currentDomainValidators.containsKey(domainName)) { + LOGGER.info("Removing external member validator for domain: {}", domainName); + validators.remove(domainName); + } + } + + for (Map.Entry entry : currentDomainValidators.entrySet()) { + final String domainName = entry.getKey(); + final String validatorClass = entry.getValue(); + + final String existingClass = domainValidatorClasses.get(domainName); + if (validatorClass.equals(existingClass) && validators.containsKey(domainName)) { + continue; + } + + ExternalMemberValidator validator = newValidatorInstance(validatorClass); + if (validator != null) { + LOGGER.info("Loaded external member validator {} for domain: {}", validatorClass, domainName); + validators.put(domainName, validator); + } else { + LOGGER.error("Failed to instantiate external member validator {} for domain: {}", + validatorClass, domainName); + validators.remove(domainName); + } + } + + domainValidatorClasses = currentDomainValidators; + LOGGER.info("External member validator refresh complete. Active validators for {} domains", validators.size()); + } + + ExternalMemberValidator newValidatorInstance(final String className) { + try { + Class clazz = Class.forName(className); + return (ExternalMemberValidator) clazz.getDeclaredConstructor().newInstance(); + } catch (Exception ex) { + LOGGER.error("Unable to instantiate external member validator class {}: {}", + className, ex.getMessage()); + return null; + } + } + + /** + * Validate a member for the given domain. If the domain does not have + * an external member validator available, or the member is not valid, + * a bad request ResourceException is thrown. + * @param domainName the domain to validate in + * @param memberName the member name to validate + * @param caller the caller method name for error reporting + */ + public void validateMember(final String domainName, final String memberName, final String caller) { + ExternalMemberValidator validator = validators.get(domainName); + if (validator == null) { + throw ZMSUtils.requestError("External member validator for domain " + + domainName + " is not available", caller); + } + if (!validator.validateMember(domainName, memberName)) { + throw ZMSUtils.requestError("Member " + memberName + + " is not valid according to the external member validator for domain " + + domainName, caller); + } + } + + /** + * Returns an unmodifiable set of domain names that have an external + * member validator configured. + * @return unmodifiable set of domain names with active validators + */ + public Set getDomainNamesWithValidator() { + return Collections.unmodifiableSet(validators.keySet()); + } + + Map getValidators() { + return validators; + } + + Map getDomainValidatorClasses() { + return domainValidatorClasses; + } +} 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 9eac184ee7b..5186b6f2efc 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 @@ -254,6 +254,9 @@ public final class ZMSConsts { public static final String ZMS_PROP_SERVICE_PROVIDER_MANAGER_ROLE = "athenz.zms.service_provider_manager_role"; public static final String ZMS_PROP_SERVICE_PROVIDER_MANAGER_ROLE_DEFAULT = "service_providers"; + public static final String ZMS_PROP_EXTERNAL_MEMBER_VALIDATOR_FREQUENCY_HOURS = "athenz.zms.external_member_validator_frequency_hours"; + public static final String ZMS_PROP_EXTERNAL_MEMBER_VALIDATOR_FREQUENCY_HOURS_DEFAULT = "1"; + public static final String ZMS_PROP_QUOTA_ASSERTION_CONDITIONS = "athenz.zms.quota_assertion_conditions"; public static final String ZMS_PROP_MAX_POLICY_VERSIONS_DEFAULT = "3"; diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/DBServiceTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/DBServiceTest.java index e55b77c435e..584dcf4ac40 100644 --- a/servers/zms/src/test/java/com/yahoo/athenz/zms/DBServiceTest.java +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/DBServiceTest.java @@ -14666,4 +14666,52 @@ public void testUpdateTemplateGroup() { Group templateGroup = zms.dbService.updateTemplateGroup(group, "my-domain", List.of(param)); assertThat(templateGroup.getGroupMembers(), hasItems(groupMember1, groupMember)); } + + @Test + public void testGetDomainsWithExternalMemberValidator() { + + Map result = zms.dbService.getDomainsWithExternalMemberValidator(); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + public void testGetDomainsWithExternalMemberValidatorException() throws ServerResourceException { + + ObjectStore saveStore = zms.dbService.store; + zms.dbService.store = mockObjStore; + + Mockito.when(mockJdbcConn.listDomainsWithExternalMemberValidator()) + .thenThrow(new ServerResourceException(500, "DB Error")); + + try { + zms.dbService.getDomainsWithExternalMemberValidator(); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), 500); + } + + zms.dbService.store = saveStore; + } + + @Test + public void testGetDomainsWithExternalMemberValidatorWithData() throws ServerResourceException { + + ObjectStore saveStore = zms.dbService.store; + zms.dbService.store = mockObjStore; + + Map mockResult = new HashMap<>(); + mockResult.put("domain1", "com.yahoo.athenz.Validator1"); + mockResult.put("domain2", "com.yahoo.athenz.Validator2"); + + Mockito.when(mockJdbcConn.listDomainsWithExternalMemberValidator()).thenReturn(mockResult); + + Map result = zms.dbService.getDomainsWithExternalMemberValidator(); + assertNotNull(result); + assertEquals(result.size(), 2); + assertEquals(result.get("domain1"), "com.yahoo.athenz.Validator1"); + assertEquals(result.get("domain2"), "com.yahoo.athenz.Validator2"); + + zms.dbService.store = saveStore; + } } diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/ExternalMemberValidatorManagerTest.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/ExternalMemberValidatorManagerTest.java new file mode 100644 index 00000000000..5d89cb3c246 --- /dev/null +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/ExternalMemberValidatorManagerTest.java @@ -0,0 +1,498 @@ +/* + * Copyright The Athenz Authors + * + * Licensed 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 com.yahoo.athenz.zms; + +import com.yahoo.athenz.auth.ExternalMemberValidator; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.testng.Assert.*; + +public class ExternalMemberValidatorManagerTest { + + private static final String TEST_VALIDATOR_CLASS = "com.yahoo.athenz.zms.TestExternalMemberValidator"; + + @Mock private DBService dbService; + + @BeforeMethod + public void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testRefreshValidatorsNewDomains() { + + Map domainValidators = new HashMap<>(); + domainValidators.put("domain1", TEST_VALIDATOR_CLASS); + domainValidators.put("domain2", TEST_VALIDATOR_CLASS); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()).thenReturn(domainValidators); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + assertEquals(manager.getValidators().size(), 2); + assertNotNull(manager.getValidators().get("domain1")); + assertNotNull(manager.getValidators().get("domain2")); + assertEquals(manager.getDomainValidatorClasses().size(), 2); + + manager.shutdown(); + } + + @Test + public void testRefreshValidatorsRemoveDomains() { + + Map initialValidators = new HashMap<>(); + initialValidators.put("domain1", TEST_VALIDATOR_CLASS); + initialValidators.put("domain2", TEST_VALIDATOR_CLASS); + + Map updatedValidators = new HashMap<>(); + updatedValidators.put("domain1", TEST_VALIDATOR_CLASS); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(initialValidators) + .thenReturn(updatedValidators); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + assertEquals(manager.getValidators().size(), 2); + assertNotNull(manager.getValidators().get("domain1")); + assertNotNull(manager.getValidators().get("domain2")); + + manager.refreshValidators(); + + assertEquals(manager.getValidators().size(), 1); + assertNotNull(manager.getValidators().get("domain1")); + assertNull(manager.getValidators().get("domain2")); + + manager.shutdown(); + } + + @Test + public void testRefreshValidatorsUpdateClass() { + + Map initialValidators = new HashMap<>(); + initialValidators.put("domain1", TEST_VALIDATOR_CLASS); + + Map updatedValidators = new HashMap<>(); + updatedValidators.put("domain1", TEST_VALIDATOR_CLASS); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(initialValidators) + .thenReturn(updatedValidators); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + ExternalMemberValidator firstInstance = manager.getValidators().get("domain1"); + assertNotNull(firstInstance); + + manager.refreshValidators(); + + ExternalMemberValidator secondInstance = manager.getValidators().get("domain1"); + assertNotNull(secondInstance); + assertSame(firstInstance, secondInstance); + + manager.shutdown(); + } + + @Test + public void testRefreshValidatorsClassChanged() { + + Map initialValidators = new HashMap<>(); + initialValidators.put("domain1", TEST_VALIDATOR_CLASS); + + Map updatedValidators = new HashMap<>(); + updatedValidators.put("domain1", TEST_VALIDATOR_CLASS); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(initialValidators) + .thenReturn(updatedValidators); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + ExternalMemberValidator firstInstance = manager.getValidators().get("domain1"); + assertNotNull(firstInstance); + + manager.refreshValidators(); + + ExternalMemberValidator secondInstance = manager.getValidators().get("domain1"); + assertSame(firstInstance, secondInstance); + + manager.shutdown(); + } + + @Test + public void testRefreshValidatorsDbFailure() { + + Map initialValidators = new HashMap<>(); + initialValidators.put("domain1", TEST_VALIDATOR_CLASS); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(initialValidators) + .thenThrow(new ResourceException(500, "DB Error")); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + assertEquals(manager.getValidators().size(), 1); + + manager.refreshValidators(); + + assertEquals(manager.getValidators().size(), 1); + assertNotNull(manager.getValidators().get("domain1")); + + manager.shutdown(); + } + + @Test + public void testRefreshValidatorsInvalidClass() { + + Map domainValidators = new HashMap<>(); + domainValidators.put("domain1", TEST_VALIDATOR_CLASS); + domainValidators.put("domain2", "com.yahoo.athenz.zms.NonExistentValidator"); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()).thenReturn(domainValidators); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + assertEquals(manager.getValidators().size(), 1); + assertNotNull(manager.getValidators().get("domain1")); + assertNull(manager.getValidators().get("domain2")); + + manager.shutdown(); + } + + @Test + public void testRefreshValidatorsEmptyMap() { + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(Collections.emptyMap()); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + assertTrue(manager.getValidators().isEmpty()); + + manager.shutdown(); + } + + @Test + public void testRefreshValidatorsAddNewDomain() { + + Map initialValidators = new HashMap<>(); + initialValidators.put("domain1", TEST_VALIDATOR_CLASS); + + Map updatedValidators = new HashMap<>(); + updatedValidators.put("domain1", TEST_VALIDATOR_CLASS); + updatedValidators.put("domain2", TEST_VALIDATOR_CLASS); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(initialValidators) + .thenReturn(updatedValidators); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + assertEquals(manager.getValidators().size(), 1); + + manager.refreshValidators(); + + assertEquals(manager.getValidators().size(), 2); + assertNotNull(manager.getValidators().get("domain1")); + assertNotNull(manager.getValidators().get("domain2")); + + manager.shutdown(); + } + + @Test + public void testValidateMemberSuccess() { + + Map domainValidators = new HashMap<>(); + domainValidators.put("domain1", TEST_VALIDATOR_CLASS); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()).thenReturn(domainValidators); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + manager.validateMember("domain1", "user.validuser", "putMembership"); + + manager.shutdown(); + } + + @Test + public void testValidateMemberInvalid() { + + Map domainValidators = new HashMap<>(); + domainValidators.put("domain1", TEST_VALIDATOR_CLASS); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()).thenReturn(domainValidators); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + try { + manager.validateMember("domain1", "user.invalid-member", "putMembership"); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); + assertTrue(ex.getMessage().contains("user.invalid-member")); + assertTrue(ex.getMessage().contains("domain1")); + } + + manager.shutdown(); + } + + @Test + public void testValidateMemberNoValidator() { + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(Collections.emptyMap()); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + try { + manager.validateMember("domain-without-validator", "user.anyuser", "putMembership"); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); + assertTrue(ex.getMessage().contains("domain-without-validator")); + } + + manager.shutdown(); + } + + @Test + public void testNewValidatorInstanceSuccess() { + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(Collections.emptyMap()); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + ExternalMemberValidator validator = manager.newValidatorInstance(TEST_VALIDATOR_CLASS); + assertNotNull(validator); + assertTrue(validator instanceof TestExternalMemberValidator); + + manager.shutdown(); + } + + @Test + public void testNewValidatorInstanceInvalidClass() { + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(Collections.emptyMap()); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + ExternalMemberValidator validator = manager.newValidatorInstance("com.invalid.NonExistentClass"); + assertNull(validator); + + manager.shutdown(); + } + + @Test + public void testNewValidatorInstanceWrongType() { + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(Collections.emptyMap()); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + ExternalMemberValidator validator = manager.newValidatorInstance("java.lang.String"); + assertNull(validator); + + manager.shutdown(); + } + + @Test + public void testShutdown() { + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(Collections.emptyMap()); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + manager.shutdown(); + manager.shutdown(); + } + + @Test + public void testRefreshValidatorsAllDomainsRemoved() { + + Map initialValidators = new HashMap<>(); + initialValidators.put("domain1", TEST_VALIDATOR_CLASS); + initialValidators.put("domain2", TEST_VALIDATOR_CLASS); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(initialValidators) + .thenReturn(Collections.emptyMap()); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + assertEquals(manager.getValidators().size(), 2); + + manager.refreshValidators(); + + assertTrue(manager.getValidators().isEmpty()); + + manager.shutdown(); + } + + @Test + public void testRefreshValidatorsClassChangedForDomain() { + + Map initialValidators = new HashMap<>(); + initialValidators.put("domain1", TEST_VALIDATOR_CLASS); + + Map updatedValidators = new HashMap<>(); + updatedValidators.put("domain1", "com.yahoo.athenz.zms.NonExistentValidator"); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(initialValidators) + .thenReturn(updatedValidators); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + assertEquals(manager.getValidators().size(), 1); + assertNotNull(manager.getValidators().get("domain1")); + + manager.refreshValidators(); + + assertNull(manager.getValidators().get("domain1")); + + manager.shutdown(); + } + + @Test + public void testGetDomainNamesWithValidatorMultipleDomains() { + + Map domainValidators = new HashMap<>(); + domainValidators.put("domain1", TEST_VALIDATOR_CLASS); + domainValidators.put("domain2", TEST_VALIDATOR_CLASS); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()).thenReturn(domainValidators); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + Set domainNames = manager.getDomainNamesWithValidator(); + assertEquals(domainNames.size(), 2); + assertTrue(domainNames.contains("domain1")); + assertTrue(domainNames.contains("domain2")); + + manager.shutdown(); + } + + @Test + public void testGetDomainNamesWithValidatorEmpty() { + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(Collections.emptyMap()); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + Set domainNames = manager.getDomainNamesWithValidator(); + assertTrue(domainNames.isEmpty()); + + manager.shutdown(); + } + + @Test + public void testGetDomainNamesWithValidatorUnmodifiable() { + + Map domainValidators = new HashMap<>(); + domainValidators.put("domain1", TEST_VALIDATOR_CLASS); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()).thenReturn(domainValidators); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + Set domainNames = manager.getDomainNamesWithValidator(); + + try { + domainNames.add("domain2"); + fail(); + } catch (UnsupportedOperationException ignored) { + } + + try { + domainNames.remove("domain1"); + fail(); + } catch (UnsupportedOperationException ignored) { + } + + manager.shutdown(); + } + + @Test + public void testGetDomainNamesWithValidatorAfterRefresh() { + + Map initialValidators = new HashMap<>(); + initialValidators.put("domain1", TEST_VALIDATOR_CLASS); + + Map updatedValidators = new HashMap<>(); + updatedValidators.put("domain1", TEST_VALIDATOR_CLASS); + updatedValidators.put("domain2", TEST_VALIDATOR_CLASS); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(initialValidators) + .thenReturn(updatedValidators); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + Set domainNames = manager.getDomainNamesWithValidator(); + assertEquals(domainNames.size(), 1); + assertTrue(domainNames.contains("domain1")); + + manager.refreshValidators(); + + domainNames = manager.getDomainNamesWithValidator(); + assertEquals(domainNames.size(), 2); + assertTrue(domainNames.contains("domain1")); + assertTrue(domainNames.contains("domain2")); + + manager.shutdown(); + } + + @Test + public void testValidateMemberWithMockedValidator() { + + ExternalMemberValidator mockValidator = Mockito.mock(ExternalMemberValidator.class); + Mockito.when(mockValidator.validateMember("domain1", "user.gooduser")).thenReturn(true); + Mockito.when(mockValidator.validateMember("domain1", "user.baduser")).thenReturn(false); + + Mockito.when(dbService.getDomainsWithExternalMemberValidator()) + .thenReturn(Collections.emptyMap()); + + ExternalMemberValidatorManager manager = new ExternalMemberValidatorManager(dbService); + + manager.getValidators().put("domain1", mockValidator); + + manager.validateMember("domain1", "user.gooduser", "putMembership"); + + try { + manager.validateMember("domain1", "user.baduser", "putMembership"); + fail(); + } catch (ResourceException ex) { + assertEquals(ex.getCode(), ResourceException.BAD_REQUEST); + } + + Mockito.verify(mockValidator).validateMember("domain1", "user.gooduser"); + Mockito.verify(mockValidator).validateMember("domain1", "user.baduser"); + + manager.shutdown(); + } +} diff --git a/servers/zms/src/test/java/com/yahoo/athenz/zms/TestExternalMemberValidator.java b/servers/zms/src/test/java/com/yahoo/athenz/zms/TestExternalMemberValidator.java new file mode 100644 index 00000000000..fcb3e193e49 --- /dev/null +++ b/servers/zms/src/test/java/com/yahoo/athenz/zms/TestExternalMemberValidator.java @@ -0,0 +1,26 @@ +/* + * Copyright The Athenz Authors + * + * Licensed 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 com.yahoo.athenz.zms; + +import com.yahoo.athenz.auth.ExternalMemberValidator; + +public class TestExternalMemberValidator implements ExternalMemberValidator { + + @Override + public boolean validateMember(final String domainName, final String memberName) { + return !memberName.contains("invalid"); + } +}