API-first with openapiprocessor and modelmapper
This commit is contained in:
		
							
								
								
									
										21
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								build.gradle
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | ||||
| plugins { | ||||
|     id 'java' | ||||
|     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 'com.github.jk1.dependency-license-report' version '2.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.liquibase:liquibase-core' | ||||
|     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' | ||||
|  | ||||
| @@ -75,11 +78,29 @@ tasks.named('test') { | ||||
|     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 { | ||||
|     java { | ||||
|         removeUnusedImports() | ||||
|         endWithNewline() | ||||
|         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) | ||||
|   | ||||
							
								
								
									
										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; | ||||
|  | ||||
| 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.web.bind.annotation.*; | ||||
| import org.springframework.http.ResponseEntity; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
| import javax.transaction.Transactional; | ||||
| import java.util.List; | ||||
| import java.util.UUID; | ||||
|  | ||||
| import static net.hostsharing.hsadminng.Mapper.map; | ||||
| import static net.hostsharing.hsadminng.Mapper.mapList; | ||||
|  | ||||
| @RestController | ||||
|  | ||||
| public class CustomerController { | ||||
| public class CustomerController implements CustomersApi { | ||||
|  | ||||
|     @Autowired | ||||
|     private Context context; | ||||
| @@ -18,34 +24,40 @@ public class CustomerController { | ||||
|     @Autowired | ||||
|     private CustomerRepository customerRepository; | ||||
|  | ||||
|     @GetMapping(value = "/api/customers") | ||||
|     @Override | ||||
|     @Transactional | ||||
|     public List<CustomerEntity> listCustomers( | ||||
|         @RequestHeader(value = "current-user") String userName, | ||||
|         @RequestHeader(value = "assumed-roles", required = false) String assumedRoles, | ||||
|         @RequestParam(required = false) String prefix | ||||
|     public ResponseEntity<List<CustomerResource>> listCustomers( | ||||
|         String userName, | ||||
|         String assumedRoles, | ||||
|         String prefix | ||||
|     ) { | ||||
|         context.setCurrentUser(userName); | ||||
|         if (assumedRoles != null && !assumedRoles.isBlank()) { | ||||
|             context.assumeRoles(assumedRoles); | ||||
|         } | ||||
|         return customerRepository.findCustomerByOptionalPrefixLike(prefix); | ||||
|         return ResponseEntity.ok( | ||||
|             mapList( | ||||
|                 customerRepository.findCustomerByOptionalPrefixLike(prefix), | ||||
|                 CustomerResource.class)); | ||||
|     } | ||||
|  | ||||
|     @PostMapping(value = "/api/customers") | ||||
|     @Override | ||||
|     @Transactional | ||||
|     public CustomerEntity addCustomer( | ||||
|         @RequestHeader(value = "current-user") String userName, | ||||
|         @RequestHeader(value = "assumed-roles", required = false) String assumedRoles, | ||||
|         @RequestBody CustomerEntity customer | ||||
|     ) { | ||||
|         context.setCurrentUser(userName); | ||||
|     public ResponseEntity<CustomerResource> addCustomer( | ||||
|         final String currentUser, | ||||
|         final String assumedRoles, | ||||
|         final CustomerResource customer) { | ||||
|         context.setCurrentUser(currentUser); | ||||
|         if (assumedRoles != null && !assumedRoles.isBlank()) { | ||||
|             context.assumeRoles(assumedRoles); | ||||
|         } | ||||
|         if (customer.getUuid() == null) { | ||||
|             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; | ||||
|  | ||||
| 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.web.bind.annotation.*; | ||||
| import org.springframework.http.ResponseEntity; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
| import javax.transaction.Transactional; | ||||
| import java.util.List; | ||||
|  | ||||
| import static net.hostsharing.hsadminng.Mapper.mapList; | ||||
|  | ||||
| @RestController | ||||
| public class PackageController { | ||||
| public class PackageController implements PackagesApi { | ||||
|  | ||||
|     @Autowired | ||||
|     private Context context; | ||||
| @@ -16,18 +21,19 @@ public class PackageController { | ||||
|     @Autowired | ||||
|     private PackageRepository packageRepository; | ||||
|  | ||||
|     @RequestMapping(value = "/api/packages", method = RequestMethod.GET) | ||||
|     @Override | ||||
|     @Transactional | ||||
|     public List<PackageEntity> listPackages( | ||||
|         @RequestHeader(value = "current-user") String userName, | ||||
|         @RequestHeader(value = "assumed-roles", required = false) String assumedRoles, | ||||
|         @RequestParam(required = false) String name | ||||
|     public ResponseEntity<List<PackageResource>> listPackages( | ||||
|         String userName, | ||||
|         String assumedRoles, | ||||
|         String name | ||||
|     ) { | ||||
|         context.setCurrentUser(userName); | ||||
|         if (assumedRoles != null && !assumedRoles.isBlank()) { | ||||
|             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; | ||||
|  | ||||
| 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.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.RequestHeader; | ||||
| import org.springframework.http.ResponseEntity; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
| import javax.transaction.Transactional; | ||||
| import java.util.List; | ||||
|  | ||||
| import static net.hostsharing.hsadminng.Mapper.mapList; | ||||
|  | ||||
| @RestController | ||||
|  | ||||
| public class RbacRoleController { | ||||
| public class RbacRoleController implements RbacrolesApi { | ||||
|  | ||||
|     @Autowired | ||||
|     private Context context; | ||||
| @@ -18,17 +22,13 @@ public class RbacRoleController { | ||||
|     @Autowired | ||||
|     private RbacRoleRepository rbacRoleRepository; | ||||
|  | ||||
|     @GetMapping(value = "/api/rbacroles") | ||||
|     @Override | ||||
|     @Transactional | ||||
|     public Iterable<RbacRoleEntity> listCustomers( | ||||
|         @RequestHeader(value = "current-user") String userName, | ||||
|         @RequestHeader(value = "assumed-roles", required = false) String assumedRoles | ||||
|     ) { | ||||
|         context.setCurrentUser(userName); | ||||
|     public ResponseEntity<List<RbacRoleResource>> listRoles(final String currentUser, final String assumedRoles) { | ||||
|         context.setCurrentUser(currentUser); | ||||
|         if (assumedRoles != null && !assumedRoles.isBlank()) { | ||||
|             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.responses.ApiResponse; | ||||
| 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.http.ResponseEntity; | ||||
| import org.springframework.web.bind.annotation.*; | ||||
|  | ||||
| import javax.transaction.Transactional; | ||||
| import java.util.List; | ||||
|  | ||||
| import static net.hostsharing.hsadminng.Mapper.mapList; | ||||
|  | ||||
| @RestController | ||||
| public class RbacUserController { | ||||
| public class RbacUserController implements RbacusersApi { | ||||
|  | ||||
|     @Autowired | ||||
|     private Context context; | ||||
| @@ -21,18 +27,9 @@ public class RbacUserController { | ||||
|     @Autowired | ||||
|     private RbacUserRepository rbacUserRepository; | ||||
|  | ||||
|     @GetMapping(value = "/api/rbacusers") | ||||
|     @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'") }) | ||||
|     @Override | ||||
|     @Transactional | ||||
|     public List<RbacUserEntity> listUsers( | ||||
|     public ResponseEntity<List<RbacUserResource>> listUsers( | ||||
|         @RequestHeader(name = "current-user") String currentUserName, | ||||
|         @RequestHeader(name = "assumed-roles", required = false) String assumedRoles, | ||||
|         @RequestParam(name="name", required = false) String userName | ||||
| @@ -41,19 +38,12 @@ public class RbacUserController { | ||||
|         if (assumedRoles != null && !assumedRoles.isBlank()) { | ||||
|             context.assumeRoles(assumedRoles); | ||||
|         } | ||||
|         return rbacUserRepository.findByOptionalNameLike(userName); | ||||
|         return ResponseEntity.ok(mapList(rbacUserRepository.findByOptionalNameLike(userName), RbacUserResource.class)); | ||||
|     } | ||||
|  | ||||
|     @GetMapping(value = "/api/rbacuser/{userName}/permissions") | ||||
|     @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") }) | ||||
|     @Override | ||||
|     @Transactional | ||||
|     public List<RbacUserPermission> listUserPermissions( | ||||
|     public ResponseEntity<List<RbacUserPermissionResource>> listUserPermissions( | ||||
|         @RequestHeader(name = "current-user") String currentUserName, | ||||
|         @RequestHeader(name = "assumed-roles", required = false) String assumedRoles, | ||||
|         @PathVariable(name= "userName") String userName | ||||
| @@ -62,6 +52,6 @@ public class RbacUserController { | ||||
|         if (assumedRoles != null && !assumedRoles.isBlank()) { | ||||
|             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 | ||||
|         mockMvc.perform(MockMvcRequestBuilders | ||||
|                 .get("/api/rbacroles") | ||||
|                 .get("/api/rbac-roles") | ||||
|                 .header("current-user", "mike@hostsharing.net") | ||||
|                 .accept(MediaType.APPLICATION_JSON)) | ||||
|  | ||||
|   | ||||
| @@ -12,6 +12,8 @@ import org.springframework.orm.jpa.JpaSystemException; | ||||
| import javax.persistence.EntityManager; | ||||
| import javax.transaction.Transactional; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| import static net.hostsharing.test.JpaAttempt.attempt; | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
|  | ||||
| @@ -160,7 +162,7 @@ class RbacRoleRepositoryIntegrationTest { | ||||
|         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) | ||||
|             .extracting(RbacRoleEntity::getRoleName) | ||||
|             .containsExactlyInAnyOrder(expectedRoleNames); | ||||
|   | ||||
| @@ -39,7 +39,7 @@ class RbacUserControllerRestTest { | ||||
|  | ||||
|         // when | ||||
|         mockMvc.perform(MockMvcRequestBuilders | ||||
|                 .get("/api/rbacusers") | ||||
|                 .get("/api/rbac-users") | ||||
|                 .header("current-user", "mike@hostsharing.net") | ||||
|                 .accept(MediaType.APPLICATION_JSON)) | ||||
|  | ||||
| @@ -59,7 +59,7 @@ class RbacUserControllerRestTest { | ||||
|  | ||||
|         // when | ||||
|         mockMvc.perform(MockMvcRequestBuilders | ||||
|                 .get("/api/rbacusers") | ||||
|                 .get("/api/rbac-users") | ||||
|                 .param("name", "admin@aaa") | ||||
|                 .header("current-user", "mike@hostsharing.net") | ||||
|                 .accept(MediaType.APPLICATION_JSON)) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user