Skip to content

Commit

Permalink
feat(pagination): add pagination feature for security domains + fix t…
Browse files Browse the repository at this point in the history
…echnical debt around pagination for applications closes gravitee-io/issues#5212
  • Loading branch information
DCourtneyGravitee authored and tcompiegne committed Apr 21, 2021
1 parent 9550d69 commit 6bec432
Show file tree
Hide file tree
Showing 18 changed files with 175 additions and 49 deletions.
Expand Up @@ -107,7 +107,7 @@ public void list(
.collect(Collectors.toList()))
.sorted((a1, a2) -> a2.getUpdatedAt().compareTo(a1.getUpdatedAt()))
.toList()
.map(applications -> new Page<>(applications.stream().skip(page * size).limit(size).collect(Collectors.toList()), pagedApplications.getCurrentPage(), applications.size()))))
.map(applications -> new Page<>(applications.stream().skip(page * size).limit(size).collect(Collectors.toList()), page, applications.size()))))
.subscribe(response::resume, response::resume);
}

Expand Down
Expand Up @@ -20,6 +20,7 @@
import io.gravitee.am.model.Acl;
import io.gravitee.am.model.Domain;
import io.gravitee.am.model.ReferenceType;
import io.gravitee.am.model.common.Page;
import io.gravitee.am.model.permissions.Permission;
import io.gravitee.am.service.ReporterService;
import io.gravitee.am.service.model.NewDomain;
Expand All @@ -35,6 +36,7 @@
import javax.ws.rs.container.Suspended;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.stream.Collectors;

import static io.gravitee.am.management.service.permissions.Permissions.of;
import static io.gravitee.am.management.service.permissions.Permissions.or;
Expand All @@ -47,6 +49,9 @@
@Api(tags = {"domain"})
public class DomainsResource extends AbstractDomainResource {

private static final int MAX_DOMAINS_SIZE_PER_PAGE = 50;
private static final String MAX_DOMAINS_SIZE_PER_PAGE_STRING = "50";

@Autowired
private IdentityProviderManager identityProviderManager;

Expand All @@ -56,7 +61,7 @@ public class DomainsResource extends AbstractDomainResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(
value = "List security domains",
value = "List security domains for an environment",
notes = "List all the security domains accessible to the current user. " +
"User must have DOMAIN[LIST] permission on the specified environment or organization " +
"AND either DOMAIN[READ] permission on each security domain " +
Expand All @@ -69,20 +74,23 @@ public class DomainsResource extends AbstractDomainResource {
public void list(
@PathParam("organizationId") String organizationId,
@PathParam("environmentId") String environmentId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue(MAX_DOMAINS_SIZE_PER_PAGE_STRING) int size,
@QueryParam("q") String query,
@Suspended final AsyncResponse response) {

User authenticatedUser = getAuthenticatedUser();

checkAnyPermission(organizationId, environmentId, Permission.DOMAIN, Acl.LIST)
.andThen(domainService.findAllByEnvironment(organizationId, environmentId)
.flatMapMaybe(domain -> hasPermission(authenticatedUser,
or(of(ReferenceType.DOMAIN, domain.getId(), Permission.DOMAIN, Acl.READ),
of(ReferenceType.ENVIRONMENT, environmentId, Permission.DOMAIN, Acl.READ),
of(ReferenceType.ORGANIZATION, organizationId, Permission.DOMAIN, Acl.READ)))
.filter(Boolean::booleanValue).map(permit -> domain))
.map(this::filterDomainInfos)
.sorted((o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getName(), o2.getName()))
.toList())
.andThen(query != null ? domainService.search(organizationId, environmentId, query) : domainService.findAllByEnvironment(organizationId, environmentId))
.flatMapMaybe(domain -> hasPermission(authenticatedUser,
or(of(ReferenceType.DOMAIN, domain.getId(), Permission.DOMAIN, Acl.READ),
of(ReferenceType.ENVIRONMENT, environmentId, Permission.DOMAIN, Acl.READ),
of(ReferenceType.ORGANIZATION, organizationId, Permission.DOMAIN, Acl.READ)))
.filter(Boolean::booleanValue).map(permit -> domain))
.map(this::filterDomainInfos)
.sorted((o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getName(), o2.getName()))
.toList()
.map(domains -> new Page<Domain>(domains.stream().skip((long) page * size).limit(size).collect(Collectors.toList()), page, domains.size()))
.subscribe(response::resume, response::resume);
}

Expand Down
Expand Up @@ -15,10 +15,8 @@
*/
package io.gravitee.am.management.handlers.management.api.resources;

import io.gravitee.am.identityprovider.api.User;
import io.gravitee.am.management.handlers.management.api.JerseySpringTest;
import io.gravitee.am.model.*;
import io.gravitee.am.model.permissions.Permission;
import io.gravitee.am.service.exception.TechnicalManagementException;
import io.gravitee.am.service.model.NewDomain;
import io.gravitee.common.http.HttpStatusCode;
Expand All @@ -28,14 +26,11 @@

import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Response;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Map;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doReturn;
Expand All @@ -61,8 +56,8 @@ public void shouldGetDomains() {
final Response response = target("domains").request().get();
assertEquals(HttpStatusCode.OK_200, response.getStatus());

final List<Domain> responseEntity = readEntity(response, List.class);
assertTrue(responseEntity.size() == 2);
final Map responseEntity = readEntity(response, Map.class);
assertTrue(((List)responseEntity.get("data")).size() == 2);
}

@Test
Expand Down
Expand Up @@ -32,13 +32,15 @@
*/
public interface DomainRepository extends CrudRepository<Domain, String> {

Flowable<Domain> findAllByReferenceId(String environmentId);

Flowable<Domain> search(String environmentId, String query);

Maybe<Domain> findByHrid(ReferenceType referenceType, String referenceId, String hrid);

Single<Set<Domain>> findAll();

Single<Set<Domain>> findByIdIn(Collection<String> ids);

Flowable<Domain> findAllByEnvironment(String environmentId);

Flowable<Domain> findAllByCriteria(DomainCriteria criteria);
}
Expand Up @@ -50,6 +50,7 @@
*/
@Repository
public class JdbcDomainRepository extends AbstractJdbcRepository implements DomainRepository {

@Autowired
private SpringDomainRepository domainRepository;
@Autowired
Expand Down Expand Up @@ -119,8 +120,8 @@ public Single<Set<Domain>> findByIdIn(Collection<String> ids) {
}

@Override
public Flowable<Domain> findAllByEnvironment(String environmentId) {
LOGGER.debug("findAllByEnvironment({})", environmentId);
public Flowable<Domain> findAllByReferenceId(String environmentId) {
LOGGER.debug("findAllByReferenceId({})", environmentId);
Flowable<Domain> domains = domainRepository.findAllByReferenceId(environmentId, ReferenceType.ENVIRONMENT.name()).map(this::toDomain);
return domains.flatMap(this::completeDomain)
.doOnError((error) -> LOGGER.error("unable to retrieve all domain with environment {}", environmentId, error));
Expand Down Expand Up @@ -190,6 +191,31 @@ public Completable delete(String domainId) {
.doOnError((error) -> LOGGER.error("unable to delete Domain with id {}", domainId, error));
}

@Override
public Flowable<Domain> search(String environmentId, String query){
LOGGER.debug("search({}, {})", environmentId, query);

boolean wildcardMatch = query.contains("*");
String wildcardQuery = query.replaceAll("\\*+", "%");

String search = new StringBuilder("SELECT * FROM domains d WHERE")
.append(" d.reference_type = :referenceType AND d.reference_id = :referenceId")
.append(" AND d.name " + (wildcardMatch ? "LIKE" : "="))
.append(" :value")
.toString();

return fluxToFlowable(dbClient.execute(search)
.bind("referenceType", ReferenceType.ENVIRONMENT.name())
.bind("referenceId", environmentId)
.bind("value", wildcardMatch ? wildcardQuery : query)
.as(JdbcDomain.class)
.fetch()
.all())
.map(this::toDomain)
.flatMap(this::completeDomain)
.doOnError((error) -> LOGGER.error("Unable to search domains with referenceId {}", environmentId, error));
}

private Flowable<Domain> completeDomain(Domain entity) {
return Flowable.just(entity).flatMap(domain ->
identitiesRepository.findAllByDomainId(domain.getId()).map(JdbcDomain.Identity::getIdentity).toList().toFlowable().map(idps -> {
Expand Down
Expand Up @@ -15,6 +15,7 @@
*/
package io.gravitee.am.repository.mongodb.management;

import com.mongodb.BasicDBObject;
import com.mongodb.reactivestreams.client.MongoCollection;
import io.gravitee.am.common.utils.RandomString;
import io.gravitee.am.common.webauthn.AttestationConveyancePreference;
Expand Down Expand Up @@ -46,6 +47,7 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

import static com.mongodb.client.model.Filters.*;

Expand Down Expand Up @@ -95,9 +97,30 @@ public Single<Set<Domain>> findByIdIn(Collection<String> ids) {
}

@Override
public Flowable<Domain> findAllByEnvironment(String environmentId) {
public Flowable<Domain> findAllByReferenceId(String environmentId) {
Bson mongoQuery =and(
eq(FIELD_REFERENCE_TYPE, ReferenceType.ENVIRONMENT.name()),
eq(FIELD_REFERENCE_ID, environmentId));
return Flowable.fromPublisher(domainsCollection.find(mongoQuery)).map(MongoDomainRepository::convert);
}

@Override
public Flowable<Domain> search(String environmentId, String query) {
// currently search on client_id field
Bson searchQuery = eq(FIELD_NAME, query);
// if query contains wildcard, use the regex query
if (query.contains("*")) {
String compactQuery = query.replaceAll("\\*+", ".*");
String regex = "^" + compactQuery;
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
searchQuery = new BasicDBObject(FIELD_NAME, pattern);
}

Bson mongoQuery =and(
eq(FIELD_REFERENCE_TYPE, ReferenceType.ENVIRONMENT.name()),
eq(FIELD_REFERENCE_ID, environmentId), searchQuery);

return Flowable.fromPublisher(domainsCollection.find(and(eq(FIELD_REFERENCE_TYPE, ReferenceType.ENVIRONMENT.name()), eq(FIELD_REFERENCE_ID, environmentId)))).map(MongoDomainRepository::convert);
return Flowable.fromPublisher(domainsCollection.find(mongoQuery)).map(MongoDomainRepository::convert);
}

@Override
Expand All @@ -108,7 +131,6 @@ public Flowable<Domain> findAllByCriteria(DomainCriteria criteria) {
return toBsonFilter(criteria.isLogicalOR(), eqAlertEnabled)
.switchIfEmpty(Single.just(new BsonDocument()))
.flatMapPublisher(filter -> Flowable.fromPublisher(domainsCollection.find(filter))).map(MongoDomainRepository::convert);

}

@Override
Expand Down
Expand Up @@ -114,7 +114,7 @@ public void testFindAllByEnvironment() throws TechnicalException {
domainRepository.create(otherDomain).blockingGet();

// fetch domains
TestSubscriber<Domain> testObserver1 = domainRepository.findAllByEnvironment("environment#1").test();
TestSubscriber<Domain> testObserver1 = domainRepository.findAllByReferenceId("environment#1").test();
testObserver1.awaitTerminalEvent();

testObserver1.assertComplete();
Expand Down
Expand Up @@ -36,12 +36,14 @@
*/
public interface DomainService {

Flowable<Domain> findAllByEnvironment(String organizationId, String environment);

Flowable<Domain> search(String organizationId, String environmentId, String query);

Maybe<Domain> findById(String id);

Single<Domain> findByHrid(String environmentId, String hrid);

Flowable<Domain> findAllByEnvironment(String organizationId, String environment);

Single<Set<Domain>> findAll();

Flowable<Domain> findAllByCriteria(DomainCriteria criteria);
Expand Down
Expand Up @@ -163,13 +163,26 @@ public Single<Domain> findByHrid(String environmentId, String hrid) {
}

@Override
public Flowable<Domain> findAllByEnvironment(String organizationId, String environmentId) {
public Flowable<Domain> search(String organizationId, String environmentId, String query) {
LOGGER.debug("Search domains with query {} for environmentId {}", query, environmentId);
return environmentService.findById(environmentId, organizationId)
.map(Environment::getId)
.flatMapPublisher(envId -> domainRepository.search(environmentId, query))
.onErrorResumeNext(ex -> {
LOGGER.error("An error has occurred when trying to search domains with query {} for environmentId {}", query, environmentId, ex);
});
}

@Override
public Flowable<Domain> findAllByEnvironment(String organizationId, String environmentId) {
LOGGER.debug("Find all domains of environment {} (organization {})", environmentId, organizationId);

return environmentService.findById(environmentId, organizationId)
.map(Environment::getId)
.flatMapPublisher(environmentsId -> domainRepository.findAllByEnvironment(environmentId));
.flatMapPublisher(envId -> domainRepository.findAllByReferenceId(envId))
.onErrorResumeNext(ex -> {
LOGGER.error("An error has occurred when trying to find domains by environment", ex);
});
}

@Override
Expand Down
2 changes: 1 addition & 1 deletion gravitee-am-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "gravitee-am-webui",
"version": "3.8.0",
"version": "3.9.0-SNAPSHOT",
"license": "Apache-2.0",
"scripts": {
"ng": "ng",
Expand Down
Expand Up @@ -74,7 +74,7 @@ export class NavbarComponent implements OnInit, OnDestroy {

listDomains() {
if(this.hasCurrentEnvironment()) {
this.domainService.list().subscribe(data => this.domains = data);
this.domainService.findByEnvironment(0,5).subscribe(response => this.domains = response.data);
} else {
this.domains = [];
}
Expand Down
Expand Up @@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, OnInit } from '@angular/core';
import { DialogService } from "../../services/dialog.service";
import { SnackbarService } from "../../services/snackbar.service";
import { ActivatedRoute } from "@angular/router";
import { ApplicationService } from "../../services/application.service";
import {Component, OnInit} from '@angular/core';
import {DialogService} from "../../services/dialog.service";
import {SnackbarService} from "../../services/snackbar.service";
import {ActivatedRoute} from "@angular/router";
import {ApplicationService} from "../../services/application.service";

@Component({
selector: 'app-applications',
Expand Down Expand Up @@ -52,7 +52,7 @@ export class ApplicationsComponent implements OnInit {

loadApps() {
const findApps = (this.searchValue) ?
this.applicationService.search(this.domainId, this.searchValue + '*') :
this.applicationService.search(this.domainId, '*' + this.searchValue + '*') :
this.applicationService.findByDomain(this.domainId, this.page.pageNumber, this.page.size);

findApps.subscribe(pagedApps => {
Expand Down
4 changes: 2 additions & 2 deletions gravitee-am-ui/src/app/environment/environment.component.ts
Expand Up @@ -54,8 +54,8 @@ export class EnvironmentComponent implements OnInit, OnDestroy {
initDomains() {
// redirect user to the first domain, if any.
this.domainService.list().subscribe(response => {
if (response && response.length > 0) {
this.router.navigate(['domains', response[0].hrid], {relativeTo: this.route});
if (response.data && response.data.length > 0) {
this.router.navigate(['domains', response.data[0].hrid], {relativeTo: this.route});
} else {
this.isLoading = false;
this.readonly = !this.authService.hasPermissions(['domain_create']);
Expand Down
2 changes: 1 addition & 1 deletion gravitee-am-ui/src/app/resolvers/domains.resolver.ts
Expand Up @@ -34,7 +34,7 @@ export class DomainsResolver implements Resolve<any> {
});
}

return this.domainService.list();
return this.domainService.findByEnvironment(0, 10);
}

}
8 changes: 8 additions & 0 deletions gravitee-am-ui/src/app/services/domain.service.ts
Expand Up @@ -28,6 +28,14 @@ export class DomainService {

constructor(private http: HttpClient, private authService: AuthService) {}

findByEnvironment(page, size): Observable<any> {
return this.http.get<any>(this.domainsURL + '?page=' + page + '&size=' + size);
}

search(searchTerm, page, size): Observable<any> {
return this.http.get<any>(this.domainsURL + '?q=' + searchTerm + '&page=' + page + '&size=' + size);
}

list(): Observable<any> {
return this.http.get<any>(this.domainsURL);
}
Expand Down

0 comments on commit 6bec432

Please sign in to comment.