From bdff3abcd6d072a61e6399e694e3213732696561 Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Wed, 19 Apr 2023 08:58:25 -0500 Subject: [PATCH] NIFI-11461 Improve User and Group Tenants Search (#7181) * NIFI-11461 Improved User and Group Tenants Search - Added searchTenants method to NiFiServiceFacade and removed unnecessary object creation - Updated TenantsResource to use delegated NiFiServiceFacade.searchTenants method - Changed autocomplete delay from default 300 ms to 500 ms * NIFI-11461 Adjusted implementation to use EntityFactory.createTenantEntity This closes #7181 --- .../apache/nifi/web/NiFiServiceFacade.java | 9 ++ .../nifi/web/StandardNiFiServiceFacade.java | 44 +++++++ .../apache/nifi/web/api/TenantsResource.java | 52 +------- .../web/StandardNiFiServiceFacadeTest.java | 111 +++++++++++++++++- .../js/nf/canvas/nf-policy-management.js | 1 + 5 files changed, 163 insertions(+), 54 deletions(-) diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java index b60e8a891a..fa4cd52079 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java @@ -128,6 +128,7 @@ import org.apache.nifi.web.api.entity.SnippetEntity; import org.apache.nifi.web.api.entity.StartVersionControlRequestEntity; import org.apache.nifi.web.api.entity.StatusHistoryEntity; import org.apache.nifi.web.api.entity.TemplateEntity; +import org.apache.nifi.web.api.entity.TenantsEntity; import org.apache.nifi.web.api.entity.UserEntity; import org.apache.nifi.web.api.entity.UserGroupEntity; import org.apache.nifi.web.api.entity.VariableRegistryEntity; @@ -1960,6 +1961,14 @@ public interface NiFiServiceFacade { */ Set getUsers(); + /** + * Search for User and Group Tenants based on provided query string + * + * @param query Search query where null or empty returns unfiltered results + * @return Tenants Entity with zero or more matched Users and User Groups + */ + TenantsEntity searchTenants(String query); + /** * Updates the specified user. * @param revision Revision to compare with current base revision diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java index 6b25a98a18..1ee96d9327 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java @@ -237,6 +237,7 @@ import org.apache.nifi.web.api.dto.RevisionDTO; import org.apache.nifi.web.api.dto.SnippetDTO; import org.apache.nifi.web.api.dto.SystemDiagnosticsDTO; import org.apache.nifi.web.api.dto.TemplateDTO; +import org.apache.nifi.web.api.dto.TenantDTO; import org.apache.nifi.web.api.dto.UserDTO; import org.apache.nifi.web.api.dto.UserGroupDTO; import org.apache.nifi.web.api.dto.VariableRegistryDTO; @@ -317,6 +318,7 @@ import org.apache.nifi.web.api.entity.StartVersionControlRequestEntity; import org.apache.nifi.web.api.entity.StatusHistoryEntity; import org.apache.nifi.web.api.entity.TemplateEntity; import org.apache.nifi.web.api.entity.TenantEntity; +import org.apache.nifi.web.api.entity.TenantsEntity; import org.apache.nifi.web.api.entity.UserEntity; import org.apache.nifi.web.api.entity.UserGroupEntity; import org.apache.nifi.web.api.entity.VariableEntity; @@ -384,6 +386,8 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.apache.commons.lang3.StringUtils.containsIgnoreCase; + /** * Implementation of NiFiServiceFacade that performs revision checking. */ @@ -4461,6 +4465,46 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade { .collect(Collectors.toSet()); } + /** + * Search for User and Group Tenants with optimized conversion from specific objects to Tenant objects + * + * @param query Search query where null or empty returns unfiltered results + * @return Tenants Entity containing zero or more matching Users and Groups + */ + @Override + public TenantsEntity searchTenants(final String query) { + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant()); + + final Set usersFound = userDAO.getUsers() + .stream() + .filter(user -> isMatched(user.getIdentity(), query)) + .map(user -> { + final TenantDTO tenant = dtoFactory.createTenantDTO(user); + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(tenant.getId())); + return entityFactory.createTenantEntity(tenant, revision, permissions); + }) + .collect(Collectors.toSet()); + + final Set userGroupsFound = userGroupDAO.getUserGroups() + .stream() + .filter(userGroup -> isMatched(userGroup.getName(), query)) + .map(userGroup -> { + final TenantDTO tenant = dtoFactory.createTenantDTO(userGroup); + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(tenant.getId())); + return entityFactory.createTenantEntity(tenant, revision, permissions); + }) + .collect(Collectors.toSet()); + + final TenantsEntity tenantsEntity = new TenantsEntity(); + tenantsEntity.setUsers(usersFound); + tenantsEntity.setUserGroups(userGroupsFound); + return tenantsEntity; + } + + private boolean isMatched(final String label, final String query) { + return StringUtils.isEmpty(query) || containsIgnoreCase(label, query); + } + private UserEntity createUserEntity(final User user, final boolean enforceUserExistence) { final RevisionDTO userRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(user.getIdentifier())); final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant()); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/TenantsResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/TenantsResource.java index c8e5be6877..3fb6d8f4ef 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/TenantsResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/TenantsResource.java @@ -35,10 +35,8 @@ import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.NiFiServiceFacade; import org.apache.nifi.web.Revision; import org.apache.nifi.web.api.dto.RevisionDTO; -import org.apache.nifi.web.api.dto.TenantDTO; import org.apache.nifi.web.api.dto.UserDTO; import org.apache.nifi.web.api.dto.UserGroupDTO; -import org.apache.nifi.web.api.entity.TenantEntity; import org.apache.nifi.web.api.entity.TenantsEntity; import org.apache.nifi.web.api.entity.UserEntity; import org.apache.nifi.web.api.entity.UserGroupEntity; @@ -64,9 +62,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.net.URI; -import java.util.ArrayList; import java.util.Date; -import java.util.List; import java.util.Set; @Path("tenants") @@ -941,53 +937,7 @@ public class TenantsResource extends ApplicationResource { tenants.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser()); }); - final List userMatches = new ArrayList<>(); - final List userGroupMatches = new ArrayList<>(); - - // get the users - for (final UserEntity userEntity : serviceFacade.getUsers()) { - final UserDTO user = userEntity.getComponent(); - if (StringUtils.isBlank(value) || StringUtils.containsIgnoreCase(user.getIdentity(), value)) { - final TenantDTO tenant = new TenantDTO(); - tenant.setId(user.getId()); - tenant.setIdentity(user.getIdentity()); - tenant.setConfigurable(user.getConfigurable()); - - final TenantEntity entity = new TenantEntity(); - entity.setPermissions(userEntity.getPermissions()); - entity.setRevision(userEntity.getRevision()); - entity.setId(userEntity.getId()); - entity.setComponent(tenant); - - userMatches.add(entity); - } - } - - // get the user groups - for (final UserGroupEntity userGroupEntity : serviceFacade.getUserGroups()) { - final UserGroupDTO userGroup = userGroupEntity.getComponent(); - if (StringUtils.isBlank(value) || StringUtils.containsIgnoreCase(userGroup.getIdentity(), value)) { - final TenantDTO tenant = new TenantDTO(); - tenant.setId(userGroup.getId()); - tenant.setIdentity(userGroup.getIdentity()); - tenant.setConfigurable(userGroup.getConfigurable()); - - final TenantEntity entity = new TenantEntity(); - entity.setPermissions(userGroupEntity.getPermissions()); - entity.setRevision(userGroupEntity.getRevision()); - entity.setId(userGroupEntity.getId()); - entity.setComponent(tenant); - - userGroupMatches.add(entity); - } - } - - // build the response - final TenantsEntity results = new TenantsEntity(); - results.setUsers(userMatches); - results.setUserGroups(userGroupMatches); - - // generate an 200 - OK response + final TenantsEntity results = serviceFacade.searchTenants(value); return noCache(Response.ok(results)).build(); } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java index e02c294c34..d629b4ca61 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java @@ -27,7 +27,9 @@ import org.apache.nifi.authorization.AuthorizationRequest; import org.apache.nifi.authorization.AuthorizationResult; import org.apache.nifi.authorization.Authorizer; import org.apache.nifi.authorization.ComponentAuthorizable; +import org.apache.nifi.authorization.Group; import org.apache.nifi.authorization.Resource; +import org.apache.nifi.authorization.User; import org.apache.nifi.authorization.resource.Authorizable; import org.apache.nifi.authorization.resource.ResourceFactory; import org.apache.nifi.authorization.resource.ResourceType; @@ -66,9 +68,13 @@ import org.apache.nifi.web.api.dto.status.StatusHistoryDTO; import org.apache.nifi.web.api.entity.ActionEntity; import org.apache.nifi.web.api.entity.ProcessGroupEntity; import org.apache.nifi.web.api.entity.StatusHistoryEntity; +import org.apache.nifi.web.api.entity.TenantEntity; +import org.apache.nifi.web.api.entity.TenantsEntity; import org.apache.nifi.web.controller.ControllerFacade; import org.apache.nifi.web.dao.ProcessGroupDAO; import org.apache.nifi.web.dao.RemoteProcessGroupDAO; +import org.apache.nifi.web.dao.UserDAO; +import org.apache.nifi.web.dao.UserGroupDAO; import org.apache.nifi.web.revision.RevisionManager; import org.apache.nifi.web.revision.RevisionUpdate; import org.apache.nifi.web.revision.StandardRevisionUpdate; @@ -81,8 +87,10 @@ import org.mockito.Mockito; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -105,6 +113,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.same; @@ -113,11 +122,14 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; - public class StandardNiFiServiceFacadeTest { - private static final String USER_1 = "user-1"; - private static final String USER_2 = "user-2"; + private static final String USER_PREFIX = "user"; + private static final String USER_1 = String.format("%s-1", USER_PREFIX); + private static final String USER_1_ID = UUID.nameUUIDFromBytes(USER_1.getBytes(StandardCharsets.UTF_8)).toString(); + private static final String USER_2 = String.format("%s-2", USER_PREFIX); + private static final String USER_GROUP_1 = String.format("%s-group-1", USER_PREFIX); + private static final String USER_GROUP_1_ID = UUID.nameUUIDFromBytes(USER_GROUP_1.getBytes(StandardCharsets.UTF_8)).toString(); private static final Integer UNKNOWN_ACTION_ID = 0; @@ -727,6 +739,99 @@ public class StandardNiFiServiceFacadeTest { assertEquals(groupId, result.getBulletins().get(0).getGroupId()); } + @Test + public void testSearchTenantsNullQuery() { + setupSearchTenants(); + + final TenantsEntity tenantsEntity = serviceFacade.searchTenants(null); + + assertUserFound(tenantsEntity); + assertUserGroupFound(tenantsEntity); + } + + @Test + public void testSearchTenantsMatchedQuery() { + setupSearchTenants(); + + final TenantsEntity tenantsEntity = serviceFacade.searchTenants(USER_PREFIX); + + assertUserFound(tenantsEntity); + assertUserGroupFound(tenantsEntity); + } + + @Test + public void testSearchTenantsGroupMatchedQuery() { + setupSearchTenants(); + + final TenantsEntity tenantsEntity = serviceFacade.searchTenants(USER_GROUP_1); + + assertUserGroupFound(tenantsEntity); + + final Collection usersFound = tenantsEntity.getUsers(); + assertTrue(usersFound.isEmpty()); + } + + @Test + public void testSearchTenantsNotMatchedQuery() { + setupSearchTenants(); + + final TenantsEntity tenantsEntity = serviceFacade.searchTenants(String.class.getSimpleName()); + + assertNotNull(tenantsEntity); + + final Collection usersFound = tenantsEntity.getUsers(); + assertTrue(usersFound.isEmpty()); + + final Collection userGroupsFound = tenantsEntity.getUserGroups(); + assertTrue(userGroupsFound.isEmpty()); + } + + private void setupSearchTenants() { + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(new Builder().identity(USER_1).build())); + SecurityContextHolder.getContext().setAuthentication(authentication); + + authorizer = mock(Authorizer.class); + serviceFacade.setAuthorizer(authorizer); + final AuthorizableLookup authorizableLookup = mock(AuthorizableLookup.class); + serviceFacade.setAuthorizableLookup(authorizableLookup); + + final Authorizable authorizable = mock(Authorizable.class); + when(authorizableLookup.getTenant()).thenReturn(authorizable); + + final RevisionManager revisionManager = mock(RevisionManager.class); + serviceFacade.setRevisionManager(revisionManager); + final Revision revision = new Revision(1L, USER_1_ID, USER_1_ID); + when(revisionManager.getRevision(anyString())).thenReturn(revision); + + final UserDAO userDAO = mock(UserDAO.class); + serviceFacade.setUserDAO(userDAO); + final UserGroupDAO userGroupDAO = mock(UserGroupDAO.class); + serviceFacade.setUserGroupDAO(userGroupDAO); + + final User user = new User.Builder().identity(USER_1).identifier(USER_1_ID).build(); + final Set users = Collections.singleton(user); + when(userDAO.getUsers()).thenReturn(users); + + final Group group = new Group.Builder().name(USER_GROUP_1).identifier(USER_GROUP_1_ID).build(); + final Set groups = Collections.singleton(group); + when(userGroupDAO.getUserGroups()).thenReturn(groups); + } + + private void assertUserFound(final TenantsEntity tenantsEntity) { + assertNotNull(tenantsEntity); + final Collection usersFound = tenantsEntity.getUsers(); + assertFalse(usersFound.isEmpty()); + final TenantEntity firstUserFound = usersFound.iterator().next(); + assertEquals(USER_1_ID, firstUserFound.getId()); + } + + private void assertUserGroupFound(final TenantsEntity tenantsEntity) { + assertNotNull(tenantsEntity); + final Collection userGroupsFound = tenantsEntity.getUserGroups(); + assertFalse(userGroupsFound.isEmpty()); + final TenantEntity firstUserGroup = userGroupsFound.iterator().next(); + assertEquals(USER_GROUP_1_ID, firstUserGroup.getId()); + } private static class MockTestBulletinRepository extends MockBulletinRepository { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-policy-management.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-policy-management.js index 01e699c311..564ce0c5ec 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-policy-management.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-policy-management.js @@ -152,6 +152,7 @@ // configure the user auto complete $.widget('nf.userSearchAutocomplete', $.ui.autocomplete, { + delay: 500, reset: function () { this.term = null; },