API-first with openapiprocessor and modelmapper
This commit is contained in:
parent
80f342eeae
commit
eeab68d63a
21
build.gradle
21
build.gradle
@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'org.springframework.boot' version '2.7.2'
|
id 'org.springframework.boot' version '2.7.2'
|
||||||
|
id 'io.openapiprocessor.openapi-processor' version '2021.3'
|
||||||
id 'io.spring.dependency-management' version '1.0.12.RELEASE'
|
id 'io.spring.dependency-management' version '1.0.12.RELEASE'
|
||||||
id 'com.github.jk1.dependency-license-report' version '2.1'
|
id 'com.github.jk1.dependency-license-report' version '2.1'
|
||||||
id "org.owasp.dependencycheck" version "7.1.1"
|
id "org.owasp.dependencycheck" version "7.1.1"
|
||||||
@ -45,6 +46,8 @@ dependencies {
|
|||||||
implementation 'org.springdoc:springdoc-openapi-ui:1.6.9'
|
implementation 'org.springdoc:springdoc-openapi-ui:1.6.9'
|
||||||
implementation 'org.liquibase:liquibase-core'
|
implementation 'org.liquibase:liquibase-core'
|
||||||
implementation 'com.vladmihalcea:hibernate-types-55:2.17.1'
|
implementation 'com.vladmihalcea:hibernate-types-55:2.17.1'
|
||||||
|
implementation 'org.openapitools:jackson-databind-nullable:0.2.3'// https://mvnrepository.com/artifact/org.modelmapper/modelmapper
|
||||||
|
implementation 'org.modelmapper:modelmapper:3.1.0'
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
|
|
||||||
@ -75,11 +78,29 @@ tasks.named('test') {
|
|||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openapiProcessor {
|
||||||
|
spring {
|
||||||
|
processor 'io.openapiprocessor:openapi-processor-spring:2021.4'
|
||||||
|
apiPath "$projectDir/src/main/resources/api-definition.yaml"
|
||||||
|
targetDir "$projectDir/build/generated/sources/openapi"
|
||||||
|
mapping "$projectDir/src/main/resources/api-mappings.yaml"
|
||||||
|
showWarnings true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourceSets.main.java.srcDir 'build/generated/sources/openapi'
|
||||||
|
compileJava.dependsOn('processSpring')
|
||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
java {
|
java {
|
||||||
removeUnusedImports()
|
removeUnusedImports()
|
||||||
endWithNewline()
|
endWithNewline()
|
||||||
toggleOffOn()
|
toggleOffOn()
|
||||||
|
|
||||||
|
// target 'src/main/java**/*.java', 'src/test/java**/*.java' // not generated
|
||||||
|
target project.fileTree(project.rootDir) {
|
||||||
|
include '**/*.java'
|
||||||
|
exclude '**/generated/**/*.java'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
project.tasks.check.dependsOn(spotlessCheck)
|
project.tasks.check.dependsOn(spotlessCheck)
|
||||||
|
15
doc/adr-concept.md
Normal file
15
doc/adr-concept.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
## ADR-Concept
|
||||||
|
|
||||||
|
This project uses ADRs (Architecture Decision Records), see also https://adr.github.io/.
|
||||||
|
|
||||||
|
There is a template available under [0000-00-00.adr-tempate.md](./0000-00-00.adr-tempate.md).
|
||||||
|
|
||||||
|
It's suggested to write an ADR if any of these is true:
|
||||||
|
|
||||||
|
- an architectural decision is hard to change,
|
||||||
|
- there is a dispute about an architectural decision,
|
||||||
|
- some unusual architectural decision was made (e.g. unusual library),
|
||||||
|
- some deeper investigation was necessary before the decision.
|
||||||
|
|
||||||
|
ADRs should not be written for minor decisions with limited impact.
|
||||||
|
|
39
doc/adr/0000-00-00.adr-template.md
Normal file
39
doc/adr/0000-00-00.adr-template.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# TITLE
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- [ ] proposed by (Proposer)
|
||||||
|
- [ ] accepted by (Participants)
|
||||||
|
- [ ] rejected by (Participants)
|
||||||
|
- [ ] superseded by (superseding ADR)
|
||||||
|
|
||||||
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
A short description, why and under which circumstances this decision had to be made.
|
||||||
|
|
||||||
|
### Technical Background
|
||||||
|
|
||||||
|
Some details about the technical challenge.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
* OPTION-1
|
||||||
|
* OPTION-...
|
||||||
|
|
||||||
|
### OPTION-n
|
||||||
|
|
||||||
|
A short overview about the option.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
A list of advantages.
|
||||||
|
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
A list of disadvantages.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Decision Outcome
|
||||||
|
|
||||||
|
Which option was chose and why.
|
108
doc/adr/2022-08-08.object-mapping.md
Normal file
108
doc/adr/2022-08-08.object-mapping.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# Object Mapping
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- [x] proposed by Michael Hönnig
|
||||||
|
- [ ] accepted by (Participants)
|
||||||
|
- [ ] rejected by (Participants)
|
||||||
|
- [ ] superseded by (superseding ADR)
|
||||||
|
|
||||||
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
Since we are using the *API first*-approach,
|
||||||
|
thus generating Java interfaces and model classes from an OpenAPI specification,
|
||||||
|
we cannot use the JPA-entities anymore at the API level,
|
||||||
|
not even if the data fields are 100% identical.
|
||||||
|
|
||||||
|
Therefore, we need some kind of mapping strategy.
|
||||||
|
|
||||||
|
|
||||||
|
### Technical Background
|
||||||
|
|
||||||
|
Java does not support duck-typing and therefore, objects of different classes have to be converted to each other, even if all data fields are identical.
|
||||||
|
|
||||||
|
In our case, the database query is usually the slowest part of handling a request.
|
||||||
|
Therefore, for the mapper, ease of use is more important than performance,
|
||||||
|
at least as long as the mapping part does not take more than 10% of the total request.
|
||||||
|
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
* specific programmatic conversion
|
||||||
|
* using the *MapStruct* library
|
||||||
|
* using the *ModelMapper* library
|
||||||
|
* Dozer, last update from 2014 + vulnerabilities => skipped
|
||||||
|
* Orika, last update from 2019 + vulnerabilities => skipped
|
||||||
|
* JMapper
|
||||||
|
|
||||||
|
### specific programmatic conversion
|
||||||
|
|
||||||
|
In this solution, we would write own code to convert the objects.
|
||||||
|
This usually means 3 converters for each entity/resource pair:
|
||||||
|
|
||||||
|
- entity -> resource
|
||||||
|
- resource -> entity
|
||||||
|
- list of entities -> list of resources
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
Very flexible and fast.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
Huge amounts of bloat code.
|
||||||
|
|
||||||
|
|
||||||
|
### using the *MapStruct* library
|
||||||
|
|
||||||
|
See https://mapstruct.org/.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
- Most popular mapping library in the Java-world.
|
||||||
|
- Actively maintained, last release 1.5.2 from Jun 18, 2022.
|
||||||
|
- very fast (see [^1])
|
||||||
|
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
- Needs interface declarations with annotations.
|
||||||
|
- Looks like it causes still too much bloat code for our purposes.
|
||||||
|
|
||||||
|
|
||||||
|
### using the *ModelMapper* library
|
||||||
|
|
||||||
|
See http://modelmapper.org/.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
- 1:1 mappings just need a simple method call without any bloat-code.
|
||||||
|
- Actively maintained, last release 3.1.0 from Mar 08, 2022.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
- could not find any, will give it a try
|
||||||
|
|
||||||
|
### using the *JMapper* library
|
||||||
|
|
||||||
|
See https://jmapper-framework.github.io/jmapper-core/.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
- Supports annotation-based and programmatic mapping exceptions.
|
||||||
|
- Actively maintained, last release 1.6.3 from May 27, 2022.
|
||||||
|
- very fast (see [^1])
|
||||||
|
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
- needs a separate mapper instance for each mapping pair
|
||||||
|
- cannot map collections (needs `stream().map(...).collect(toList())` or similar)
|
||||||
|
|
||||||
|
|
||||||
|
## Decision Outcome
|
||||||
|
|
||||||
|
We chose the option **"using the *ModelMapper* library"** because it has an acceptable performance without any bloat code.
|
||||||
|
|
||||||
|
If it turns out to be too slow after all, "using the *JMapper* library" seems to be a good alternative.
|
||||||
|
|
||||||
|
[^1]: https://www.baeldung.com/java-performance-mapping-frameworks
|
27
src/main/java/net/hostsharing/hsadminng/Mapper.java
Normal file
27
src/main/java/net/hostsharing/hsadminng/Mapper.java
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package net.hostsharing.hsadminng;
|
||||||
|
|
||||||
|
import org.modelmapper.ModelMapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A nicer API for ModelMapper.
|
||||||
|
*
|
||||||
|
* MOst
|
||||||
|
*/
|
||||||
|
public class Mapper {
|
||||||
|
private final static ModelMapper modelMapper = new ModelMapper();
|
||||||
|
|
||||||
|
|
||||||
|
public static <S, T> List<T> mapList(final List<S> source, final Class<T> targetClass) {
|
||||||
|
return source
|
||||||
|
.stream()
|
||||||
|
.map(element -> modelMapper.map(element, targetClass))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <S, T> T map(final S source, final Class<T> targetClass) {
|
||||||
|
return modelMapper.map(source, targetClass);
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,22 @@
|
|||||||
package net.hostsharing.hsadminng.hs.hscustomer;
|
package net.hostsharing.hsadminng.hs.hscustomer;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import net.hostsharing.hsadminng.generated.api.v1.api.CustomersApi;
|
||||||
|
import net.hostsharing.hsadminng.generated.api.v1.model.CustomerResource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.Mapper.map;
|
||||||
|
import static net.hostsharing.hsadminng.Mapper.mapList;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|
||||||
public class CustomerController {
|
public class CustomerController implements CustomersApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Context context;
|
private Context context;
|
||||||
@ -18,34 +24,40 @@ public class CustomerController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private CustomerRepository customerRepository;
|
private CustomerRepository customerRepository;
|
||||||
|
|
||||||
@GetMapping(value = "/api/customers")
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public List<CustomerEntity> listCustomers(
|
public ResponseEntity<List<CustomerResource>> listCustomers(
|
||||||
@RequestHeader(value = "current-user") String userName,
|
String userName,
|
||||||
@RequestHeader(value = "assumed-roles", required = false) String assumedRoles,
|
String assumedRoles,
|
||||||
@RequestParam(required = false) String prefix
|
String prefix
|
||||||
) {
|
) {
|
||||||
context.setCurrentUser(userName);
|
context.setCurrentUser(userName);
|
||||||
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
}
|
}
|
||||||
return customerRepository.findCustomerByOptionalPrefixLike(prefix);
|
return ResponseEntity.ok(
|
||||||
|
mapList(
|
||||||
|
customerRepository.findCustomerByOptionalPrefixLike(prefix),
|
||||||
|
CustomerResource.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/api/customers")
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public CustomerEntity addCustomer(
|
public ResponseEntity<CustomerResource> addCustomer(
|
||||||
@RequestHeader(value = "current-user") String userName,
|
final String currentUser,
|
||||||
@RequestHeader(value = "assumed-roles", required = false) String assumedRoles,
|
final String assumedRoles,
|
||||||
@RequestBody CustomerEntity customer
|
final CustomerResource customer) {
|
||||||
) {
|
context.setCurrentUser(currentUser);
|
||||||
context.setCurrentUser(userName);
|
|
||||||
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
}
|
}
|
||||||
if (customer.getUuid() == null) {
|
if (customer.getUuid() == null) {
|
||||||
customer.setUuid(UUID.randomUUID());
|
customer.setUuid(UUID.randomUUID());
|
||||||
}
|
}
|
||||||
return customerRepository.save(customer);
|
return ResponseEntity.ok(
|
||||||
|
map(
|
||||||
|
customerRepository.save(map(customer, CustomerEntity.class)),
|
||||||
|
CustomerResource.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
package net.hostsharing.hsadminng.hs.hspackage;
|
package net.hostsharing.hsadminng.hs.hspackage;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import net.hostsharing.hsadminng.generated.api.v1.api.PackagesApi;
|
||||||
|
import net.hostsharing.hsadminng.generated.api.v1.model.PackageResource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.Mapper.mapList;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class PackageController {
|
public class PackageController implements PackagesApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Context context;
|
private Context context;
|
||||||
@ -16,18 +21,19 @@ public class PackageController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private PackageRepository packageRepository;
|
private PackageRepository packageRepository;
|
||||||
|
|
||||||
@RequestMapping(value = "/api/packages", method = RequestMethod.GET)
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public List<PackageEntity> listPackages(
|
public ResponseEntity<List<PackageResource>> listPackages(
|
||||||
@RequestHeader(value = "current-user") String userName,
|
String userName,
|
||||||
@RequestHeader(value = "assumed-roles", required = false) String assumedRoles,
|
String assumedRoles,
|
||||||
@RequestParam(required = false) String name
|
String name
|
||||||
) {
|
) {
|
||||||
context.setCurrentUser(userName);
|
context.setCurrentUser(userName);
|
||||||
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
}
|
}
|
||||||
return packageRepository.findAllByOptionalNameLike(name);
|
final var result = packageRepository.findAllByOptionalNameLike(name);
|
||||||
|
return ResponseEntity.ok(mapList(result, PackageResource.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.rbacrole;
|
package net.hostsharing.hsadminng.rbac.rbacrole;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import net.hostsharing.hsadminng.generated.api.v1.api.RbacrolesApi;
|
||||||
|
import net.hostsharing.hsadminng.generated.api.v1.model.RbacRoleResource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.Mapper.mapList;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|
||||||
public class RbacRoleController {
|
public class RbacRoleController implements RbacrolesApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Context context;
|
private Context context;
|
||||||
@ -18,17 +22,13 @@ public class RbacRoleController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RbacRoleRepository rbacRoleRepository;
|
private RbacRoleRepository rbacRoleRepository;
|
||||||
|
|
||||||
@GetMapping(value = "/api/rbacroles")
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public Iterable<RbacRoleEntity> listCustomers(
|
public ResponseEntity<List<RbacRoleResource>> listRoles(final String currentUser, final String assumedRoles) {
|
||||||
@RequestHeader(value = "current-user") String userName,
|
context.setCurrentUser(currentUser);
|
||||||
@RequestHeader(value = "assumed-roles", required = false) String assumedRoles
|
|
||||||
) {
|
|
||||||
context.setCurrentUser(userName);
|
|
||||||
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
}
|
}
|
||||||
return rbacRoleRepository.findAll();
|
return ResponseEntity.ok(mapList(rbacRoleRepository.findAll(), RbacRoleResource.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,20 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import net.hostsharing.hsadminng.generated.api.v1.api.RbacusersApi;
|
||||||
|
import net.hostsharing.hsadminng.generated.api.v1.model.RbacUserPermissionResource;
|
||||||
|
import net.hostsharing.hsadminng.generated.api.v1.model.RbacUserResource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.Mapper.mapList;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class RbacUserController {
|
public class RbacUserController implements RbacusersApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Context context;
|
private Context context;
|
||||||
@ -21,18 +27,9 @@ public class RbacUserController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RbacUserRepository rbacUserRepository;
|
private RbacUserRepository rbacUserRepository;
|
||||||
|
|
||||||
@GetMapping(value = "/api/rbacusers")
|
@Override
|
||||||
@Operation(description = "List accessible RBAC users with optional filter by name.",
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(responseCode = "200",
|
|
||||||
content = @Content(array = @ArraySchema(
|
|
||||||
schema = @Schema(implementation = RbacUserEntity.class)))),
|
|
||||||
@ApiResponse(responseCode = "401",
|
|
||||||
description = "if the 'current-user' cannot be identified"),
|
|
||||||
@ApiResponse(responseCode = "403",
|
|
||||||
description = "if the 'current-user' is not allowed to assume any of the roles from 'assumed-roles'") })
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public List<RbacUserEntity> listUsers(
|
public ResponseEntity<List<RbacUserResource>> listUsers(
|
||||||
@RequestHeader(name = "current-user") String currentUserName,
|
@RequestHeader(name = "current-user") String currentUserName,
|
||||||
@RequestHeader(name = "assumed-roles", required = false) String assumedRoles,
|
@RequestHeader(name = "assumed-roles", required = false) String assumedRoles,
|
||||||
@RequestParam(name="name", required = false) String userName
|
@RequestParam(name="name", required = false) String userName
|
||||||
@ -41,19 +38,12 @@ public class RbacUserController {
|
|||||||
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
}
|
}
|
||||||
return rbacUserRepository.findByOptionalNameLike(userName);
|
return ResponseEntity.ok(mapList(rbacUserRepository.findByOptionalNameLike(userName), RbacUserResource.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(value = "/api/rbacuser/{userName}/permissions")
|
@Override
|
||||||
@Operation(description = "List all visible permissions granted to the given user; reduced ", responses = {
|
|
||||||
@ApiResponse(responseCode = "200",
|
|
||||||
content = @Content(array = @ArraySchema( schema = @Schema(implementation = RbacUserPermission.class)))),
|
|
||||||
@ApiResponse(responseCode = "401",
|
|
||||||
description = "if the 'current-user' cannot be identified"),
|
|
||||||
@ApiResponse(responseCode = "403",
|
|
||||||
description = "if the 'current-user' is not allowed to view permissions of the given user") })
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public List<RbacUserPermission> listUserPermissions(
|
public ResponseEntity<List<RbacUserPermissionResource>> listUserPermissions(
|
||||||
@RequestHeader(name = "current-user") String currentUserName,
|
@RequestHeader(name = "current-user") String currentUserName,
|
||||||
@RequestHeader(name = "assumed-roles", required = false) String assumedRoles,
|
@RequestHeader(name = "assumed-roles", required = false) String assumedRoles,
|
||||||
@PathVariable(name= "userName") String userName
|
@PathVariable(name= "userName") String userName
|
||||||
@ -62,6 +52,6 @@ public class RbacUserController {
|
|||||||
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
if (assumedRoles != null && !assumedRoles.isBlank()) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
}
|
}
|
||||||
return rbacUserRepository.findPermissionsOfUser(userName);
|
return ResponseEntity.ok(mapList(rbacUserRepository.findPermissionsOfUser(userName), RbacUserPermissionResource.class));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
306
src/main/resources/api-definition.yaml
Normal file
306
src/main/resources/api-definition.yaml
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
openapi: 3.0.1
|
||||||
|
info:
|
||||||
|
title: Hostsharing hsadmin-ng API
|
||||||
|
version: v0
|
||||||
|
servers:
|
||||||
|
- url: http://localhost:8080
|
||||||
|
description: Local development default URL.
|
||||||
|
|
||||||
|
paths:
|
||||||
|
|
||||||
|
/api/customers:
|
||||||
|
get:
|
||||||
|
summary: Returns a list of (optionally filtered) customers.
|
||||||
|
description: Returns the list of (optionally filtered) customers which are visible to the current user or any of it's assumed roles.
|
||||||
|
tags:
|
||||||
|
- customers
|
||||||
|
operationId: listCustomers
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/currentUser'
|
||||||
|
- $ref: '#/components/parameters/assumedRoles'
|
||||||
|
- name: prefix
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Customer-prefix to filter the results.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Customer'
|
||||||
|
"401":
|
||||||
|
description: Not Authorized
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
post:
|
||||||
|
summary: Adds a new customer.
|
||||||
|
tags:
|
||||||
|
- customers
|
||||||
|
operationId: addCustomer
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/currentUser'
|
||||||
|
- $ref: '#/components/parameters/assumedRoles'
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Customer'
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Customer'
|
||||||
|
|
||||||
|
/api/rbac-users:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- rbacusers
|
||||||
|
description: List accessible RBAC users with optional filter by name.
|
||||||
|
operationId: listUsers
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/currentUser'
|
||||||
|
- $ref: '#/components/parameters/assumedRoles'
|
||||||
|
- name: name
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RbacUser'
|
||||||
|
"401":
|
||||||
|
description: if the 'current-user' cannot be identified
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RbacUser'
|
||||||
|
"403":
|
||||||
|
description: if the 'current-user' is not allowed to assume any of the roles
|
||||||
|
from 'assumed-roles'
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RbacUser'
|
||||||
|
|
||||||
|
/api/rbac-users/{userName}/permissions:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- rbacusers
|
||||||
|
description: 'List all visible permissions granted to the given user; reduced '
|
||||||
|
operationId: listUserPermissions
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/currentUser'
|
||||||
|
- $ref: '#/components/parameters/assumedRoles'
|
||||||
|
- name: userName
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RbacUserPermission'
|
||||||
|
|
||||||
|
"401":
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
"403":
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
|
||||||
|
/api/rbac-roles:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- rbacroles
|
||||||
|
operationId: listRoles
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/currentUser'
|
||||||
|
- $ref: '#/components/parameters/assumedRoles'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RbacRole'
|
||||||
|
|
||||||
|
/api/ping:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- test
|
||||||
|
operationId: ping
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
/api/packages:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- packages
|
||||||
|
operationId: listPackages
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/currentUser'
|
||||||
|
- $ref: '#/components/parameters/assumedRoles'
|
||||||
|
- name: name
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Package'
|
||||||
|
|
||||||
|
components:
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
currentUser:
|
||||||
|
name: current-user
|
||||||
|
in: header
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Identifying name of the currently logged in user.
|
||||||
|
assumedRoles:
|
||||||
|
name: assumed-roles
|
||||||
|
in: header
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Semicolon-separated list of roles to assume. The current user needs to have the right to assume these roles.
|
||||||
|
|
||||||
|
responses:
|
||||||
|
NotFound:
|
||||||
|
description: The specified was not found.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
Unauthorized:
|
||||||
|
description: The current user is unknown or not authorized.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
Forbidden:
|
||||||
|
description: The current user or none of the assumed or roles is granted access to the .
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
Customer:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
uuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
prefix:
|
||||||
|
type: string
|
||||||
|
reference:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
adminUserName:
|
||||||
|
type: string
|
||||||
|
RbacUser:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
uuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
RbacUserPermission:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
objectUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
objectTable:
|
||||||
|
type: string
|
||||||
|
objectIdName:
|
||||||
|
type: string
|
||||||
|
roleName:
|
||||||
|
type: string
|
||||||
|
roleUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
permissionUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
op:
|
||||||
|
type: string
|
||||||
|
RbacRole:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
uuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
objectUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
objectTable:
|
||||||
|
type: string
|
||||||
|
objectIdName:
|
||||||
|
type: string
|
||||||
|
roleType:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- owner
|
||||||
|
- admin
|
||||||
|
- tenant
|
||||||
|
roleName:
|
||||||
|
type: string
|
||||||
|
Package:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
uuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
customer:
|
||||||
|
$ref: '#/components/schemas/Customer'
|
||||||
|
Error:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
- message
|
14
src/main/resources/api-mappings.yaml
Normal file
14
src/main/resources/api-mappings.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
openapi-processor-mapping: v2
|
||||||
|
|
||||||
|
options:
|
||||||
|
package-name: net.hostsharing.hsadminng.generated.api.v1
|
||||||
|
model-name-suffix: Resource
|
||||||
|
|
||||||
|
map:
|
||||||
|
result: org.springframework.http.ResponseEntity
|
||||||
|
|
||||||
|
|
||||||
|
types:
|
||||||
|
- type: array => java.util.List
|
||||||
|
- type: string:uuid => java.util.UUID
|
||||||
|
|
@ -36,7 +36,7 @@ class RbacRoleControllerRestTest {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
.get("/api/rbacroles")
|
.get("/api/rbac-roles")
|
||||||
.header("current-user", "mike@hostsharing.net")
|
.header("current-user", "mike@hostsharing.net")
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
|
|
||||||
|
@ -12,6 +12,8 @@ import org.springframework.orm.jpa.JpaSystemException;
|
|||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static net.hostsharing.test.JpaAttempt.attempt;
|
import static net.hostsharing.test.JpaAttempt.attempt;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@ -160,7 +162,7 @@ class RbacRoleRepositoryIntegrationTest {
|
|||||||
assertThat(context.getAssumedRoles()).as("precondition").containsExactly(assumedRoles.split(";"));
|
assertThat(context.getAssumedRoles()).as("precondition").containsExactly(assumedRoles.split(";"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void exactlyTheseRbacRolesAreReturned(final Iterable<RbacRoleEntity> actualResult, final String... expectedRoleNames) {
|
void exactlyTheseRbacRolesAreReturned(final List<RbacRoleEntity> actualResult, final String... expectedRoleNames) {
|
||||||
assertThat(actualResult)
|
assertThat(actualResult)
|
||||||
.extracting(RbacRoleEntity::getRoleName)
|
.extracting(RbacRoleEntity::getRoleName)
|
||||||
.containsExactlyInAnyOrder(expectedRoleNames);
|
.containsExactlyInAnyOrder(expectedRoleNames);
|
||||||
|
@ -39,7 +39,7 @@ class RbacUserControllerRestTest {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
.get("/api/rbacusers")
|
.get("/api/rbac-users")
|
||||||
.header("current-user", "mike@hostsharing.net")
|
.header("current-user", "mike@hostsharing.net")
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ class RbacUserControllerRestTest {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
.get("/api/rbacusers")
|
.get("/api/rbac-users")
|
||||||
.param("name", "admin@aaa")
|
.param("name", "admin@aaa")
|
||||||
.header("current-user", "mike@hostsharing.net")
|
.header("current-user", "mike@hostsharing.net")
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
|
Loading…
Reference in New Issue
Block a user