Commit 5bb823af authored by Thomas Beermann's avatar Thomas Beermann
Browse files

Add hca request interface

parent cf5ee9bd
......@@ -6,6 +6,8 @@ webapp/src/main/frontend/node_modules
webapp/hm-server
webapp/hms.tar
coverage
**/venv/**
**/__pycache__/**
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/java/**
!**/src/main/resources/**
......
......@@ -131,6 +131,37 @@ docker_availability:
tags:
- "docker"
docker_hca_service:
only:
- main
- tags
- merge_requests
stage: docker
image: docker:20.10.10
services:
- name: docker:20.10.10-dind
alias: docker
before_script:
- docker info
script:
- docker login -u $GITLAB_DOCKER_USERNAME -p $GITLAB_DOCKER_PASSWORD $DOCKER_REGISTRY
- |-
if [[ "$CI_COMMIT_BRANCH" == "main" ]]; then
docker build -t $DOCKER_IMAGE_BASE/hca-service:latest -f hca-service/Dockerfile hca-service/
docker push $DOCKER_IMAGE_BASE/hca-service:latest
else
tag=$(echo $CI_COMMIT_REF_NAME | sed 's/^[0-9]*-//' | tr [:upper:] [:lower:] | tr [:punct:] -)
if [[ -n "$CI_COMMIT_TAG" ]]; then
tag=$CI_COMMIT_TAG
fi
docker build -t $DOCKER_IMAGE_BASE/hca-service:$tag -f hca-service/Dockerfile hca-service/
docker push $DOCKER_IMAGE_BASE/hca-service:$tag
fi
dependencies:
- package
tags:
- "docker"
trigger_deploy_mr:
only:
- merge_requests
......
......@@ -8,10 +8,8 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableTransactionManagement
@EnableMongoRepositories
@SpringBootApplication
public class HelmholtzCerebrumApplication
{
public static void main(String[] args)
{
public class HelmholtzCerebrumApplication {
public static void main(String[] args) {
SpringApplication.run(HelmholtzCerebrumApplication.class, args);
}
}
\ No newline at end of file
......@@ -12,15 +12,13 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@EnableWebMvc
@Configuration
public class CerebrumConfig
{
public class CerebrumConfig {
private final SslContextFactory.Client ssl = new SslContextFactory.Client();
private final HttpClient httpClient = new HttpClient(ssl);
ClientHttpConnector clientConnector = new JettyClientHttpConnector(httpClient);
@Bean
public WebClient authorisationServer()
{
public WebClient authorisationServer() {
return WebClient.builder()
.filter(new ServletBearerExchangeFilterFunction())
.clientConnector(clientConnector)
......
......@@ -5,5 +5,5 @@ import org.springframework.data.mongodb.config.EnableMongoAuditing;
@Configuration(proxyBeanMethods = false)
@EnableMongoAuditing
public class CerebrumDataConfig
{}
public class CerebrumDataConfig {
}
......@@ -4,4 +4,5 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CerebrumMethodSecurityConfig extends GlobalMethodSecurityConfiguration {}
public class CerebrumMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}
......@@ -25,47 +25,41 @@ import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
@EnableWebSecurity
public class CerebrumSecurityConfig extends WebSecurityConfigurerAdapter
{
public class CerebrumSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("#{'${cerebrum.allowed.origins}'.split(',')}")
List<String> allowedOrigins;
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
URL jwkSetUrl;
@Override
protected void configure(HttpSecurity http) throws Exception
{
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.authorizeRequests()
.mvcMatchers("/api/v0/admin/**").hasRole("ADMIN")
.mvcMatchers("/", "/swagger-ui/**", "/api/**", "/actuator/**", "/favicon.ico").permitAll()
.anyRequest().authenticated()
.and()
.mvcMatchers("/api/v0/admin/**").hasRole("ADMIN")
.mvcMatchers("/", "/swagger-ui/**", "/api/**", "/actuator/**", "/favicon.ico").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
}
@Bean
CorsConfigurationSource corsConfigurationSource()
{
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(allowedOrigins);
configuration.setAllowedMethods(Arrays.asList("GET","DELETE","PUT","POST","PATCH","OPTIONS"));
configuration.setAllowedMethods(Arrays.asList("GET", "DELETE", "PUT", "POST", "PATCH", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) throws KeySourceException {
JWSKeySelector<SecurityContext> jwsKeySelector =
JWSAlgorithmFamilyJWSKeySelector
.fromJWKSetURL(this.jwkSetUrl);
JWSKeySelector<SecurityContext> jwsKeySelector = JWSAlgorithmFamilyJWSKeySelector
.fromJWKSetURL(this.jwkSetUrl);
DefaultJWTProcessor<SecurityContext> jwtProcessor =
new DefaultJWTProcessor<>();
DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
jwtProcessor.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier(new JOSEObjectType("at+jwt")));
return new NimbusJwtDecoder(jwtProcessor);
......
package de.helmholtz.cloud.cerebrum.controller;
import java.util.List;
import javax.validation.constraints.Min;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
import de.helmholtz.cloud.hca.message.ResourceAllocateV1Schema;
import de.helmholtz.cloud.cerebrum.entity.HCARequest;
import de.helmholtz.cloud.cerebrum.errorhandling.CerebrumApiError;
import de.helmholtz.cloud.cerebrum.service.HCARequestService;
import de.helmholtz.cloud.cerebrum.utils.CerebrumControllerUtilities;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.SneakyThrows;
@RestController
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE, path = "${spring.data.rest.base-path}/hca")
@Tag(name = "HCA", description = "API to send messages to the Cloud Agent")
public class HCAController {
private final HCARequestService hcaRequestService;
public HCAController(HCARequestService hcaRequestService) {
this.hcaRequestService = hcaRequestService;
}
/* list requests for user */
@Operation(summary = "get all requests for a user")
@GetMapping(path = "/{userId}")
public Iterable<HCARequest> getRequestsByUserId(
@Parameter(description = "specify the user ID") @PathVariable(name = "userId") String userId,
@Parameter(description = "specify the page number") @RequestParam(value = "page", defaultValue = "0") @Min(0) Integer page,
@Parameter(description = "limit the number of records returned in one page") @RequestParam(value = "size", defaultValue = "20") @Min(1) Integer size,
@Parameter(description = "sort the fetched data in either ascending (asc) "
+ "or descending (desc) according to one or more of the images "
+ "properties. Eg. to sort the list in ascending order base on the "
+ "name property; the value will be set to name.asc") @RequestParam(value = "sort", defaultValue = "createdDate.desc") List<String> sorts) {
return hcaRequestService.getHCARequestsByUserId(userId,
PageRequest.of(page, size, Sort.by(CerebrumControllerUtilities.getOrders(sorts))));
}
/* list requests for user */
@Operation(summary = "get requests for a user for a service")
@GetMapping(path = "/{userId}/{serviceName}")
public Iterable<HCARequest> getRequestsByUserIdAndServiceName(
@Parameter(description = "specify the user ID") @PathVariable(name = "userId") String userId,
@Parameter(description = "specify the service name") @PathVariable(name = "serviceName") String serviceName,
@Parameter(description = "specify the page number") @RequestParam(value = "page", defaultValue = "0") @Min(0) Integer page,
@Parameter(description = "limit the number of records returned in one page") @RequestParam(value = "size", defaultValue = "20") @Min(1) Integer size,
@Parameter(description = "sort the fetched data in either ascending (asc) "
+ "or descending (desc) according to one or more of the images "
+ "properties. Eg. to sort the list in ascending order base on the "
+ "name property; the value will be set to name.asc") @RequestParam(value = "sort", defaultValue = "createdDate.desc") List<String> sorts) {
return hcaRequestService.getHCARequestsByUserIdAndServiceName(userId, serviceName,
PageRequest.of(page, size, Sort.by(CerebrumControllerUtilities.getOrders(sorts))));
}
/* allocate resource */
@SneakyThrows
@PreAuthorize("isAuthenticated()")
@Operation(summary = "submit a new resource allocation request", security = @SecurityRequirement(name = "hdf-aai"))
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "resource allocation request created", content = @Content(schema = @Schema(implementation = HCARequest.class))),
@ApiResponse(responseCode = "400", description = "invalid UUID supplied", content = @Content(schema = @Schema(implementation = CerebrumApiError.class))),
@ApiResponse(responseCode = "401", description = "unauthorised", content = @Content()),
@ApiResponse(responseCode = "403", description = "forbidden", content = @Content()),
@ApiResponse(responseCode = "404", description = "not found", content = @Content())})
@PostMapping(path = "/resource/{serviceName}")
public ResponseEntity<HCARequest> allocateResource(
@Parameter(description = "Unique identifier of the associated service") @PathVariable(name = "serviceName") String serviceName,
@RequestBody ResourceAllocateV1Schema request, UriComponentsBuilder uriComponentsBuilder) {
return hcaRequestService.createHCARequest(serviceName, request, uriComponentsBuilder);
}
}
......@@ -40,146 +40,108 @@ import de.helmholtz.cloud.cerebrum.utils.CerebrumControllerUtilities;
@RestController
@Validated
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE,
path = "${spring.data.rest.base-path}/images")
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE, path = "${spring.data.rest.base-path}/images")
@Tag(name = "images", description = "The Image API")
public class ImageController
{
private final ImageService imageService;
public class ImageController {
private final ImageService imageService;
public ImageController(ImageService imageService)
{
this.imageService = imageService;
}
public ImageController(ImageService imageService) {
this.imageService = imageService;
}
/* get Images */
@Operation(summary = "get array list of all images")
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "successful operation",
content = @Content(array = @ArraySchema(
schema = @Schema(implementation = Image.class)))),
@ApiResponse(responseCode = "400", description = "invalid request",
content = @Content(array = @ArraySchema(
schema = @Schema(implementation = CerebrumApiError.class))))
})
@GetMapping(path = "")
public Iterable<Image> getImages(
@Parameter(description = "specify the name of the image to search for")
@RequestParam(value = "name", defaultValue = "") String name,
@Parameter(description = "specify the page number")
@RequestParam(value = "page", defaultValue = "0") @Min(0) Integer page,
@Parameter(description = "limit the number of records returned in one page")
@RequestParam(value = "size", defaultValue = "20") @Min(1) Integer size,
@Parameter(description = "sort the fetched data in either ascending (asc) " +
"or descending (desc) according to one or more of the images " +
"properties. Eg. to sort the list in ascending order base on the " +
"name property; the value will be set to name.asc")
@RequestParam(value = "sort", defaultValue = "name.asc") List<String> sorts)
{
return (name == null || name.isEmpty()) ?
imageService.getImages(PageRequest.of(page, size,
Sort.by(CerebrumControllerUtilities.getOrders(sorts)))) :
imageService.getImages(name, PageRequest.of(page, size,
Sort.by(CerebrumControllerUtilities.getOrders(sorts))));
}
/* get Images */
@Operation(summary = "get array list of all images")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "successful operation", content = @Content(array = @ArraySchema(schema = @Schema(implementation = Image.class)))),
@ApiResponse(responseCode = "400", description = "invalid request", content = @Content(array = @ArraySchema(schema = @Schema(implementation = CerebrumApiError.class))))
})
@GetMapping(path = "")
public Iterable<Image> getImages(
@Parameter(description = "specify the name of the image to search for") @RequestParam(value = "name", defaultValue = "") String name,
@Parameter(description = "specify the page number") @RequestParam(value = "page", defaultValue = "0") @Min(0) Integer page,
@Parameter(description = "limit the number of records returned in one page") @RequestParam(value = "size", defaultValue = "20") @Min(1) Integer size,
@Parameter(description = "sort the fetched data in either ascending (asc) " +
"or descending (desc) according to one or more of the images " +
"properties. Eg. to sort the list in ascending order base on the " +
"name property; the value will be set to name.asc") @RequestParam(value = "sort", defaultValue = "name.asc") List<String> sorts) {
return (name == null || name.isEmpty()) ? imageService.getImages(PageRequest.of(page, size,
Sort.by(CerebrumControllerUtilities.getOrders(sorts))))
: imageService.getImages(name, PageRequest.of(page, size,
Sort.by(CerebrumControllerUtilities.getOrders(sorts))));
}
/* get Image */
@Operation(summary = "find image by ID", description = "Returns a detailed image information " +
"corresponding to the ID")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "successful operation", content = @Content(schema = @Schema(implementation = Image.class))),
@ApiResponse(responseCode = "400", description = "invalid image ID supplied", content = @Content(schema = @Schema(implementation = CerebrumApiError.class))),
@ApiResponse(responseCode = "404", description = "image not found", content = @Content(schema = @Schema(implementation = CerebrumApiError.class)))
})
@GetMapping(path = "/{uuid}")
public Image getImage(
@Parameter(description = "ID of the image that needs to be fetched") @PathVariable(name = "uuid") String uuid) {
return imageService.getImage(uuid);
}
/* get Image */
@Operation(summary = "find image by ID",
description = "Returns a detailed image information " +
"corresponding to the ID")
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "successful operation",
content = @Content(schema = @Schema(implementation = Image.class))),
@ApiResponse(responseCode = "400", description = "invalid image ID supplied",
content = @Content(schema = @Schema(implementation = CerebrumApiError.class))),
@ApiResponse(responseCode = "404", description = "image not found",
content = @Content(schema = @Schema(implementation = CerebrumApiError.class)))
})
@GetMapping(path = "/{uuid}")
public Image getImage(
@Parameter(description = "ID of the image that needs to be fetched")
@PathVariable(name = "uuid") String uuid)
{
return imageService.getImage(uuid);
}
/* create image */
@SneakyThrows
@PreAuthorize("isAuthenticated()")
@Operation(summary = "add a new image", security = @SecurityRequirement(name = "hdf-aai"))
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "image created", content = @Content(schema = @Schema(implementation = Image.class))),
@ApiResponse(responseCode = "400", description = "invalid ID supplied", content = @Content(schema = @Schema(implementation = CerebrumApiError.class))),
@ApiResponse(responseCode = "401", description = "unauthorised", content = @Content())
})
@PostMapping("")
public ResponseEntity<Image> createImage(
@Parameter(description = "name of the image") @RequestParam("name") String name,
@RequestParam("image") MultipartFile image,
UriComponentsBuilder uriComponentsBuilder) {
Image img = new Image();
img.setName(name);
img.setImage(
Base64.getEncoder().encodeToString(
new Binary(BsonBinarySubType.BINARY, image.getBytes()).getData()));
return imageService.createImage(img, uriComponentsBuilder);
}
/* create image */
@SneakyThrows
@PreAuthorize("isAuthenticated()")
@Operation(summary = "add a new image",
security = @SecurityRequirement(name = "hdf-aai"))
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "image created",
content = @Content(schema = @Schema(implementation = Image.class))),
@ApiResponse(responseCode = "400", description = "invalid ID supplied",
content = @Content(schema = @Schema(implementation = CerebrumApiError.class))),
@ApiResponse(responseCode = "401", description = "unauthorised", content = @Content())
})
@PostMapping("")
public ResponseEntity<Image> createImage(
@Parameter(description = "name of the image")
@RequestParam("name") String name,
@RequestParam("image") MultipartFile image,
UriComponentsBuilder uriComponentsBuilder)
{
Image img = new Image();
img.setName(name);
img.setImage(
Base64.getEncoder().encodeToString(new Binary(BsonBinarySubType.BINARY, image.getBytes()).getData()));
return imageService.createImage(img, uriComponentsBuilder);
}
/* update image */
@SneakyThrows
@PreAuthorize("isAuthenticated()")
@Operation(summary = "update an existing image", description = "Update part (or all) of an image information", security = @SecurityRequirement(name = "hdf-aai"))
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "successful operation", content = @Content(schema = @Schema(implementation = Image.class))),
@ApiResponse(responseCode = "201", description = "image created", content = @Content(schema = @Schema(implementation = Image.class))),
@ApiResponse(responseCode = "400", description = "invalid ID supplied", content = @Content(schema = @Schema(implementation = CerebrumApiError.class))),
@ApiResponse(responseCode = "401", description = "unauthorised", content = @Content())
})
@PutMapping(path = "/{uuid}", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Image> updateImage(
@Parameter(description = "Unique identifier of the image that needs to be updated") @PathVariable(name = "uuid") String uuid,
@Parameter(description = "name of the image") @RequestParam("name") String name,
@Parameter(description = "") @RequestParam("image") MultipartFile image,
UriComponentsBuilder uriComponentsBuilder) {
Image img = new Image();
img.setName(name);
img.setImage(Base64.getEncoder().encodeToString(
new Binary(BsonBinarySubType.BINARY, image.getBytes()).getData()));
/* update image */
@SneakyThrows
@PreAuthorize("isAuthenticated()")
@Operation(summary = "update an existing image",
description = "Update part (or all) of an image information",
security = @SecurityRequirement(name = "hdf-aai"))
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "successful operation",
content = @Content(schema = @Schema(implementation = Image.class))),
@ApiResponse(responseCode = "201", description = "image created",
content = @Content(schema = @Schema(implementation = Image.class))),
@ApiResponse(responseCode = "400", description = "invalid ID supplied",
content = @Content(schema = @Schema(implementation = CerebrumApiError.class))),
@ApiResponse(responseCode = "401", description = "unauthorised", content = @Content())
})
@PutMapping(path = "/{uuid}", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Image> updateImage(
@Parameter(description = "Unique identifier of the image that needs to be updated")
@PathVariable(name = "uuid") String uuid,
@Parameter(description = "name of the image")
@RequestParam("name") String name,
@Parameter(description = "")
@RequestParam("image") MultipartFile image, UriComponentsBuilder uriComponentsBuilder)
{
Image img = new Image();
img.setName(name);
img.setImage(Base64.getEncoder().encodeToString(
new Binary(BsonBinarySubType.BINARY, image.getBytes()).getData()));
return imageService.updateImage(uuid, img, uriComponentsBuilder);
}
return imageService.updateImage(uuid, img, uriComponentsBuilder);
}
/* delete Image */
@PreAuthorize("isAuthenticated()")
@Operation(summary = "deletes an image",
description = "Removes the record of the specified " +
"image id from the database. The image " +
"unique identification number cannot be null or empty",
security = @SecurityRequirement(name = "hdf-aai"))
@ApiResponses(value = {
@ApiResponse(responseCode = "204", description = "successful operation", content = @Content()),
@ApiResponse(responseCode = "401", description = "unauthorised", content = @Content())
})
@DeleteMapping(path = "/{uuid}")
public ResponseEntity<Image> deleteImage(
@Parameter(description="Image id to delete", required=true)
@PathVariable(name = "uuid") String uuid)
{
return imageService.deleteImage(uuid);
}
/* delete Image */
@PreAuthorize("isAuthenticated()")
@Operation(summary = "deletes an image", description = "Removes the record of the specified " +
"image id from the database. The image " +
"unique identification number cannot be null or empty", security = @SecurityRequirement(name = "hdf-aai"))
@ApiResponses(value = {
@ApiResponse(responseCode = "204", description = "successful operation", content = @Content()),
@ApiResponse(responseCode = "401", description = "unauthorised", content = @Content())
})
@DeleteMapping(path = "/{uuid}")
public ResponseEntity<Image> deleteImage(
@Parameter(description = "Image id to delete", required = true) @PathVariable(name = "uuid") String uuid) {
return imageService.deleteImage(uuid);
}
}
package de.helmholtz.cloud.cerebrum.controller;
import java.security.Principal;
import java.util.List;
import javax.validation.constraints.Min;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import de.helmholtz.cloud.cerebrum.service.PermissionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE, path = "${spring.data.rest.base-path}/permissions")
@Tag(name = "Permissions", description = "API to permission for a user")
public class PermissionController {
private final PermissionService PermissionService;
public PermissionController(PermissionService PermissionService) {
this.PermissionService = PermissionService;
}
/* list requests for user */
@PreAuthorize("isAuthenticated()")
@Operation(summary = "get all requests for a user")
@PostMapping(path = "")
public String getPermissionByEntitlement(
@Parameter(description = "specify the page number") @RequestParam(value = "page", defaultValue = "0") @Min(0) Integer page,
@Parameter(description = "limit the number of records returned in one page") @RequestParam(value = "size", defaultValue = "20") @Min(1) Integer size,
@Parameter(description = "sort the fetched data in either ascending (asc) "
+ "or descending (desc) according to one or more of the images "
+ "properties. Eg. to sort the list in ascending order base on the "
+ "name property; the value will be set to name.asc") @RequestParam(value = "sort", defaultValue = "createdDate.desc") List<String> sorts,
@RequestBody List<String> entitlements,
Principal user) {
return PermissionService.aggregateByEntitlement(entitlements).toJson();
}
}