Backend service (svc)
Project setup
Now that project structure is set up for the Onecx Quarkus project, we can configure the backend service and extend the project configuration.
Parent configuration
Project maven parent configuration, artifactId and project version.
| More information about maven parent pom pattern Apache Maven Pom Documentation |
<parent>
<groupId>org.tkit.onecx</groupId>
<artifactId>onecx-quarkus3-parent</artifactId>
<version>0.72.0</version>
</parent>
Dependencies
| All version of the dependencies are defined in the parent project. Project itself should not define any version of these dependencies. |
By default, we will have the following maven dependencies:
-
tkit-quarkus are quarkus extensions for
JPA,log,json-logandrest-log -
quarkus cloud native extensions (health, metrics, tracing, …), quarkus-rest, hibernate-orm and liquibase
-
lombok to generate getters, setters and much more
-
mapstruct Generator to generate a source that maps
DTOtoJPAmodels and vice versa
Backend service default dependencies
<dependencies>
<!-- ONECX -->
<!-- 1000kit -->
<dependency>
<groupId>org.tkit.quarkus.lib</groupId>
<artifactId>tkit-quarkus-rest-context</artifactId>
</dependency>
<dependency>
<groupId>org.tkit.quarkus.lib</groupId>
<artifactId>tkit-quarkus-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.tkit.quarkus.lib</groupId>
<artifactId>tkit-quarkus-log-cdi</artifactId>
</dependency>
<dependency>
<groupId>org.tkit.quarkus.lib</groupId>
<artifactId>tkit-quarkus-log-rs</artifactId>
</dependency>
<dependency>
<groupId>org.tkit.quarkus.lib</groupId>
<artifactId>tkit-quarkus-log-json</artifactId>
</dependency>
<dependency>
<groupId>org.tkit.quarkus.lib</groupId>
<artifactId>tkit-quarkus-rest</artifactId>
</dependency>
<!-- QUARKUS -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-liquibase</artifactId>
</dependency>
<dependency>
<groupId>com.github.blagerweij</groupId>
<artifactId>liquibase-sessionlock</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-context-propagation</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-opentelemetry</artifactId>
</dependency>
<!-- OTHER -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
</dependencies>
Generate rest endpoint
In onecx project we do use API-first approach. First we need to defined our REST interface with openApi. Create a example-v1.yaml (pattern: <application>-<version>.yaml) in the src/main/openapi directory.
Example openapi file
---
openapi: 3.0.3
info:
title: onecx-theme internal service
version: 1.0.0
servers:
- url: "http://onecx-theme-svc:8080"
tags:
- name: themesInternal
paths:
/internal/themes/search:
post:
security:
- oauth2: [ ocx-th:all, ocx-th:read ]
tags:
- themesInternal
description: Search for themes
operationId: searchThemes
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ThemeSearchCriteria'
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ThemePageResult'
"400":
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetailResponse'
components:
securitySchemes:
oauth2:
type: oauth2
flows:
clientCredentials:
tokenUrl: https://oauth.simple.api/token
scopes:
ocx-th:all: Grants access to all operations
ocx-th:read: Grants read access
ocx-th:write: Grants write access
ocx-th:delete: Grants access to delete operations
schemas:
ThemeSearchCriteria:
type: object
properties:
name:
type: string
pageNumber:
format: int32
description: The number of page.
default: 0
type: integer
pageSize:
format: int32
description: The size of page
default: 100
maximum: 1000
type: integer
ThemePageResult:
type: object
properties:
totalElements:
format: int64
description: The total elements in the resource.
type: integer
number:
format: int32
type: integer
size:
format: int32
type: integer
totalPages:
format: int64
type: integer
stream:
type: array
items:
$ref: '#/components/schemas/Theme'
Theme:
required:
- name
- displayName
type: object
properties:
modificationCount:
format: int32
type: integer
creationDate:
$ref: '#/components/schemas/OffsetDateTime'
creationUser:
type: string
modificationDate:
$ref: '#/components/schemas/OffsetDateTime'
modificationUser:
type: string
id:
type: string
name:
minLength: 2
type: string
displayName:
minLength: 2
type: string
cssFile:
type: string
description:
type: string
assetsUrl:
type: string
logoUrl:
type: string
faviconUrl:
type: string
previewImageUrl:
type: string
assetsUpdateDate:
type: string
properties:
type: object
operator:
type: boolean
default: false
OffsetDateTime:
format: date-time
type: string
example: 2022-03-10T12:15:50-04:00
ProblemDetailResponse:
type: object
properties:
errorCode:
type: string
detail:
type: string
params:
type: array
items:
$ref: '#/components/schemas/ProblemDetailParam'
invalidParams:
type: array
items:
$ref: '#/components/schemas/ProblemDetailInvalidParam'
ProblemDetailParam:
type: object
properties:
key:
type: string
value:
type: string
ProblemDetailInvalidParam:
type: object
properties:
name:
type: string
message:
type: string
Now we can configure org.tkit.onecx.quarkus:onecx-openapi-generator Maven plugin. The important part of the Maven plugin is xml configuration element.
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<configuration>
<generatorName>jaxrs-spec</generatorName>
<modelNameSuffix>DTO</modelNameSuffix>
<generateApiTests>false</generateApiTests>
<generateApiDocumentation>false</generateApiDocumentation>
<generateModelTests>false</generateModelTests>
<generateModelDocumentation>false</generateModelDocumentation>
<generateSupportingFiles>false</generateSupportingFiles>
<addCompileSourceRoot>true</addCompileSourceRoot>
<library>quarkus</library>
<additionalProperties>onecx-scopes=true</additionalProperties>
<configOptions>
<sourceFolder>/</sourceFolder>
<openApiNullable>false</openApiNullable>
<returnResponse>true</returnResponse>
<useTags>true</useTags>
<interfaceOnly>true</interfaceOnly>
<serializableModel>true</serializableModel>
<singleContentTypes>true</singleContentTypes>
<dateLibrary>java8</dateLibrary>
<useMicroProfileOpenAPIAnnotations>true</useMicroProfileOpenAPIAnnotations>
<useJakartaEe>true</useJakartaEe>
<useBeanValidation>true</useBeanValidation>
<java17>true</java17>
</configOptions>
</configuration>
<executions>
<execution>
<id>internal</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>src/main/openapi/onecx-theme-internal-openapi.yaml</inputSpec>
<apiPackage>gen.org.tkit.onecx.theme.rs.internal</apiPackage>
<modelPackage>gen.org.tkit.onecx.theme.rs.internal.model</modelPackage>
<modelNameSuffix>DTO</modelNameSuffix>
</configuration>
</execution>
</executions>
</plugin>
After we run mvn clean package maven plugin will generate in corresponding source code in target/generated-sources directory.
REST Implementation
To implement the RoleRestController we need to create a java class which implements generated interface gen.org.tkit.onecx.example.rs.v1.RoleInternalApi.
RoleRestController
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.core.Response;
import jakarta.validation.ConstraintViolationException;
import org.tkit.quarkus.jpa.exceptions.ConstraintException;
import jakarta.persistence.OptimisticLockException;
import gen.org.tkit.onecx.example.rs.v1.RoleInternalApi;
import gen.org.tkit.onecx.example.rs.v1.model.CreateRoleRequestDTO;
@ApplicationScoped
public class RoleRestController implements RoleInternalApi {
@Inject
RoleMapper mapper;
@Inject
ExceptionMapper exceptionMapper;
@Context
UriInfo uriInfo;
@Override
public Response createRole(CreateRoleRequestDTO createRoleRequestDTO) {
var role = mapper.create(createRoleRequestDTO);
role = dao.create(role);
return Response
.created(uriInfo.getAbsolutePathBuilder().path(role.getId()).build())
.entity(mapper.map(theme))
.build();
}
// constraint exception exception handler
@ServerExceptionMapper
public RestResponse<ProblemDetailResponseDTO> exception(ConstraintException ex) {
return exceptionMapper.exception(ex);
}
// constraint violation exception handler
@ServerExceptionMapper
public RestResponse<ProblemDetailResponseDTO> constraint(ConstraintViolationException ex) {
return exceptionMapper.constraint(ex);
}
// Optimistic lock exception handler
@ServerExceptionMapper
public RestResponse<ProblemDetailResponseDTO> optimisticLockException(OptimisticLockException ex) {
return exceptionMapper.optimisticLock(ex);
}
}
For the backend service we need to define exception mapper methods for following exception:
-
jakarta.persistence.OptimisticLockException- this exception is throw when try to store object in the database with old or wrongOPTLOCKnumber. -
jakarta.validation.ConstraintViolationException- when we activate a validation frameworkio.quarkus:quarkus-hibernate-validatorthis exception will be thrown when request does not match to the requirements defined in the OpenApi. -
org.tkit.quarkus.jpa.exceptions.ConstraintException- this exception will be thrown for any database constraints.
With the annotation @ServerExceptionMapper we defined the exception mapper for each Exception in the context of the RestController.
|
To map the request DTO object to the JPA model we use the Mapstruct. We define the RoleMapper in our example.
import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper;
@Mapper(uses = { OffsetDateTimeMapper.class })
public interface RoleMapper {
@Mapping(target = "id", ignore = true)
@Mapping(target = "creationDate", ignore = true)
@Mapping(target = "creationUser", ignore = true)
@Mapping(target = "modificationDate", ignore = true)
@Mapping(target = "modificationUser", ignore = true)
@Mapping(target = "controlTraceabilityManual", ignore = true)
@Mapping(target = "modificationCount", ignore = true)
@Mapping(target = "persisted", ignore = true)
@Mapping(target = "tenantId", ignore = true)
Role create(CreateRoleRequestDTO dto);
// ... more mapping methods
}
The same approach we use for the exception mapper which could be shared between multiple RestController for the same rest interface.
import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper;
@Mapper(uses = { OffsetDateTimeMapper.class })
public interface ExceptionMapper {
default RestResponse<ProblemDetailResponseDTO> constraint(ConstraintViolationException ex) {
var dto = exception(ErrorKeys.CONSTRAINT_VIOLATIONS.name(), ex.getMessage());
dto.setInvalidParams(createErrorValidationResponse(ex.getConstraintViolations()));
return RestResponse.status(Response.Status.BAD_REQUEST, dto);
}
//.... more mapping methods
}
org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper mapper is predefined a mapstruct mapper for OffsetDateTime and LocalDateTime object.
|
JPA layer implementation
For our project we will use the tkit-quarkus-jpa and tkit-quarkus-jpa-models extension for Quarkus which have simple API on top of JPA. We have abstract DAO classes and abstract Entity classes which provides basic CRUD operation. For more information please read the extension tkit-quarkus-jpa documentation.
<dependency>
<groupId>org.tkit.quarkus.lib</groupId>
<artifactId>tkit-quarkus-jpa</artifactId>
</dependency>
Create User entity class in the org.tkit.onecx.user.domain.models package. The entity class extend the TraceableEntity. In the entity class can use the Lombok annotation @Getter and @Setter to generate the getters and setters.
Example user entity
import javax.persistence.Entity;
import javax.persistence.Table;
import org.tkit.quarkus.jpa.models.TraceableEntity;
@Entity
@Table(name = "T_USER")
public class User extends TraceableEntity {
@Column(name = "USERNAME")
private String username;
}
Example user DAO
import org.tkit.quarkus.jpa.daos.AbstractDAO;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class UserDAO extends AbstractDAO<User> {
}
Database configuration
In onecx project we do use the postgresql database for our backend services.
quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.max-size=30
quarkus.datasource.jdbc.min-size=10
quarkus.hibernate-orm.database.generation=validate
quarkus.hibernate-orm.jdbc.timezone=UTC
quarkus.liquibase.migrate-at-start=true
quarkus.liquibase.validate-on-migrate=true
The parameter quarkus.hibernate-orm.log.sql will activate the hibernate logs in the console.
|
Database schema change management
Liquibase is an open source tool for database schema change management. We will this framework in our example. Liquibase does have abstraction layer xml, json or yaml to support multi databases. Liquibase can generate the changes and compare our Hibernate model with existing database.
For the database schema change management we do use quarkus-liquibase extension. To generate a database changes or validate the database changes we do use tkit-liquibase-plugin which is configured in the onecx-quarkus3-parent.
Quarkus liquibase configuration
quarkus.liquibase.migrate-at-start=true
quarkus.liquibase.validate-on-migrate=true
By default, liquibase does not use session lock. To activate the liquibase session lock during the start of our service we do use com.github.blagerweij:liquibase-sessionlock liquibase extension. No configuration is need to this extension.
To generate the database changes run following command
mvn clean compile -Pdb-diff
To check if you are missing any changes run following command
mvn validate -Pdb-check
JPA date-time
First, you need to configure the database server to use the UTC timezone. For example PostgreSQL default is UTC. Second, you need to set hibernate.jdbc.time_zone Hibernate property to the value of UTC. For the Quarkus application we need set key quarkus.hibernate-orm.jdbc.timezone to UTC value in the application.properties.
quarkus.hibernate-orm.jdbc.timezone=UTC
The date representation in the database is store in the UTC as long number. Date fields in the JPA entity represents this database date as LocalDateTime. On the another hand the DTO represents the date as ISO text. To get it running we need to create a mapper which will map the LocalDateTime to OffsetDatetime. In the mapping we should not lose the Offset/Zone information.
| The date representation in the JPA entity is the LocalDateTime java type and in the DTO object is the OffsetDateTime java type. |
Example user entity
@Entity
@Table(name = "T_USER")
public class User extends TraceableEntity {
private String username;
private LocalDateTime date;
}
Example user DTO
@RegisterForReflection
public class UserDTO extends TraceableDTO {
private String username;
private OffsetDateTime date;
}
Example user mapper
import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper;
@Mapper(uses = OffsetDateTimeMapper.class)
public interface UserMapper {
UserDTO map(User model);
User create(UserDTO dto);
}
In the tests we need to configure the RestAssured to use text representation of the date-time.
public abstract class AbstractTest {
static {
RestAssured.config = RestAssuredConfig.config().objectMapperConfig(
ObjectMapperConfig.objectMapperConfig().jackson2ObjectMapperFactory(
(cls, charset) -> {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
return objectMapper;
}
)
);
}
}
JPA custom business ID
PostgresSQL SERIAL type
@GeneratorType(type = BidStringGenerator.class, when = GenerationTime.INSERT)
@Column(name = "CUSTOM")
String custom;
Generic sequence implementation
import javax.persistence.Column;
import org.my.group.BusinessId;
@BusinessId(sequence = "SEQ_MY_ENTITY_BID")
@Column(name = "bid_anno")
Long bid;
Follow the implementation of the custom annotation BusinessId.
BusinessId
import org.hibernate.annotations.ValueGenerationType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ValueGenerationType(generatedBy = BusinessIdValueGeneration.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface BusinessId {
String sequence() default "";
}
BusinessIdValueGeneration
import org.hibernate.tuple.AnnotationValueGeneration;
import org.hibernate.tuple.GenerationTiming;
import org.hibernate.tuple.ValueGenerator;
public class BusinessIdValueGeneration implements AnnotationValueGeneration<BusinessId> {
String sequence;
@Override
public void initialize(BusinessId annotation, Class<?> propertyType) { sequence = annotation.sequence(); }
@Override
public GenerationTiming getGenerationTiming() { return GenerationTiming.INSERT; }
@Override
public ValueGenerator<?> getValueGenerator() { return new BusinessIdGenerator(sequence); }
@Override
public boolean referenceColumnInSql() { return false; }
@Override
public String getDatabaseGeneratedReferencedColumnValue() { return null; }
}
BusinessIdGenerator
import org.hibernate.Session;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.jdbc.ReturningWork;
import org.hibernate.tuple.ValueGenerator;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class BusinessIdGenerator implements ValueGenerator<Long> {
String sequence;
BusinessIdGenerator(String sequence) {
this.sequence = sequence;
}
@Override
public Long generateValue(Session session, Object owner) {
SessionFactoryImpl sessionFactory = (SessionFactoryImpl) session.getSessionFactory();
String sql = sessionFactory.getJdbcServices().getDialect().getSequenceNextValString(sequence);
ReturningWork<Long> seq = connection -> {
try (PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery()) {
resultSet.next();
return resultSet.getLong(1);
}
};
return sessionFactory.getCurrentSession().doReturningWork(seq);
}
}
We need to add the sequence to the import.sql for the local development.
CREATE SEQUENCE IF NOT EXISTS seq_my_entity_bid;
Custom in-Memory generator
@GeneratorType(type = BidStringGenerator.class, when = GenerationTime.INSERT)
@Column(name = "CUSTOM")
String custom;
Follow the implementation of the custom generator BidStringGenerator.
public class BidStringGenerator implements ValueGenerator<String> {
@Override
public String generateValue(Session session, Object owner) {
MyEntity e = (MyEntity) owner;
return e.name + "+" + UUID.randomUUID();
}
}
Tests
For the test we add following dependencies to our project
Test dependencies
<dependencies>
<!-- TEST -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.tkit.quarkus.lib</groupId>
<artifactId>tkit-quarkus-test-db-import</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
| The Quarkus tests has support for the injection and mocking. This will work only for the Quarkus JVM tests and not for native image test or integration tests with a docker image. Try to avoid this in our tests to have much more reusable tests. In most cases, the mock service also fakes code to pass the test. |
For our test we will use this pattern for the test classes
-
<Name>RestControllerTest extends AbstractTest- the extended test for the common and unit test. For example: UserRestControllerTest -
<Name>RestControllerTestIT extends <Name>RestControllerTest- the extended test for the integration test. For example: UserRestControllerTestIT
The 1000kit test extension tkit-quarkus-test-db-import does have support to import test data for the tests during the test execution. We will create our test data as XML data. Store the xml file in the src/test/resources/data directory under name test-internal.xml. The magic is in the @WithDBData annotation which could be used on the Class or Method level. Example of the xml test data file:
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<ROLE guid="r21" optlock="0" name="n1" />
<ROLE guid="r22" optlock="0" name="n2" />
</dataset>
Only the XML DbUnit FlatXmlDataSet format is supported.
|
Example of the RoleRestControllerTest class.
@QuarkusTest
@TestHTTPEndpoint(RoleRestController.class)
@WithDBData(value = "data/test-internal.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true)
class RoleRestControllerTest extends AbstractTest {
@Test
void getRoleByIdTest() {
// get role by ID
var dto = given().contentType(APPLICATION_JSON).get("r12")
.then().statusCode(OK.getStatusCode()).contentType(APPLICATION_JSON)
.extract().body().as(RoleDTO.class);
// validate role
assertThat(dto).isNotNull();
assertThat(dto.getName()).isEqualTo("n2");
assertThat(dto.getId()).isEqualTo("r12");
// not found
given().contentType(APPLICATION_JSON).get("___").then().statusCode(NOT_FOUND.getStatusCode());
}
}
Example of the role rest controller integration test.
import io.quarkus.test.junit.QuarkusIntegrationTest;
@QuarkusIntegrationTest
class RoleRestControllerTestIT extends RoleRestControllerTest {
}
Security
To enable scope security for backend services we need to add these Maven dependencies.
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>org.tkit.onecx.quarkus</groupId>
<artifactId>onecx-security</artifactId>
</dependency>
</dependencies>
The next step is to add the OAuth scopes to our OpenApi definition. In our example, we add the read scope to the createRole method.
Example openapi with scope
openapi: 3.0.3
info:
title: example service
version: 1.0.0
servers:
- url: "http://onecx-example-svc:8080"
paths:
/internal/roles:
get:
security:
- oauth2: [read]
operationId: createRole
responses:
201:
description: "Role created"
components:
securitySchemes:
oauth2:
type: oauth2
flows:
clientCredentials:
tokenUrl: https://oauth.simple.api/token
scopes:
read: Grants read access
Now we need to add the extension org.tkit.onecx.quarkus:onecx-openapi-generator which will generate the Quarkus annotation @io.quarkus.security.PermissionsAllowed for the security interceptor from our open API definition file. We add this extension as a dependency for the Maven plugin org.openapitools:openapi-generator-maven-plugin.
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<configuration>
<additionalProperties>onecx-scopes=true</additionalProperties>
<!-- configuration -->
</configuration>
<executions><!-- executions --></executions>
<dependencies>
<dependency>
<groupId>org.tkit.onecx.quarkus</groupId>
<artifactId>onecx-openapi-generator</artifactId>
</dependency>
</dependencies>
</plugin>
To enable this extension, we also need to define additional properties onecx-scopes=true. Without this configuration, the generator uses the default Java source code template.
Now our generated source code contains the annotation @io.quarkus.security.PermissionsAllowed({"read"}) to the generated method.
public interface RoleInternalApi {
@io.quarkus.security.PermissionsAllowed({ "read" })
@GET
@Consumes({ "application/json" })
@Produces({ "application/json" })
Response createRole();
}
The org.tkit.onecx.quarkus:onecx-security extension creates security audit data for the default Quarkus security extension during the build process. After enabling this extension, it is no longer possible to call this method without a token and with the scope read.
To disable security setup following property tkit.security.auth.enabled=false. This property is part fo (tkit-quarkus-url)[1000kit Quarkus extension]
|
Security tests
For testing we use Quarkus Keycloak dev services. To activate this feature add this maven dependency.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
Quarkus will start and configure a Keycloak server for our tests but also in the dev mode by default. Now we can configure user and they roles.
For example, we add role role-admin to user alice in our example
# Enabled security for all requests /* and exclude /q/* quarkus console
quarkus.http.auth.permission.health.paths=/q/*
quarkus.http.auth.permission.health.policy=permit
quarkus.http.auth.permission.default.paths=/*
quarkus.http.auth.permission.default.policy=authenticated
# TEST
%test.quarkus.keycloak.devservices.roles.alice=role-admin
%test.quarkus.keycloak.devservices.roles.bob=role-user
# DEV
%dev.quarkus.keycloak.devservices.roles.alice=role-admin
%dev.quarkus.keycloak.devservices.roles.bob=role-user
How to import the whole realm or configure additional user in to Keycloak dev services please check the Quarkus documentation page.
|
Now we can use the io.quarkus.test.keycloak.client.KeycloakTestClient in our test.
Example test class
@QuarkusTest
@TestHTTPEndpoint(RoleRestController.class)
class RoleRestControllerTest {
// create keycloak client instance connected to keycloak dev service container
KeycloakTestClient keycloakClient = new KeycloakTestClient();
@Test
void oauthTest() {
// get the access token of the user alice
var token = keycloakClient.getAccessToken("alice");
// call rest endpoint
given().when()
.auth().oauth2(token)
.contentType(APPLICATION_JSON).get("1").then()
.statusCode(Response.Status.OK.getStatusCode());
// test without oauth token
given().when().contentType(APPLICATION_JSON).get("1").then()
.statusCode(Response.Status.UNAUTHORIZED.getStatusCode());
}
}
Multi tenancy
For multi tenancy we will to use onecx-quarkus-tenant extension.
| For more information about the configuration please check the documentation page onecx-quarkus-tenant |
Multi tenancy maven dependencies
<dependencies>
<!-- ONECX -->
<dependency>
<groupId>org.tkit.onecx.quarkus</groupId>
<artifactId>onecx-tenant</artifactId>
</dependency>
<!-- 1000kit -->
<dependency>
<groupId>org.tkit.quarkus.lib</groupId>
<artifactId>tkit-quarkus-jpa</artifactId>
</dependency>
</dependencies>
Now we need to configure our application
# hibernate multi-tenant configuration
quarkus.hibernate-orm.multitenant=DISCRIMINATOR
# enable or disable multi-tenancy support
tkit.rs.context.tenant-id.enabled=true
| Header configuration for the token, and token parsing is implemented in 1000kit tkit-rest-context quarkus extensions. Please check the documentation of the extension. |
Multi tenancy tests
For the test we can use mock settings where we can define which claim attribute of the token we will use: %test.tkit.rs.context.tenant-id.mock.claim-org-id=orgId and we must define mapping between token claim value and tenant ID %test.tkit.rs.context.tenant-id.mock.data.<claim-value>=<tenant-id>.
%test.tkit.rs.context.tenant-id.enabled=true
%test.tkit.rs.context.tenant-id.mock.enabled=true
%test.tkit.rs.context.tenant-id.mock.default-tenant=default
%test.tkit.rs.context.tenant-id.mock.claim-org-id=orgId
%test.tkit.rs.context.tenant-id.mock.data.org1=tenant-100
%test.tkit.rs.context.tenant-id.mock.data.org2=tenant-200
For the test we can create a simple help method which will creates a token for our tests
@SuppressWarnings("java:S2187")
public class AbstractTest {
// header ID of the principal token for tests
protected static final String APM_HEADER_PARAM = "apm-principal-token";
// token claim for tenant-id
protected static final String CLAIMS_ORG_ID = ConfigProvider.getConfig()
.getValue("%test.tkit.rs.context.tenant-id.mock.claim-org-id", String.class);
/**
* Method creates a principal token for test.
* @param organizationId organization ID
* @return the corresponding test
*/
protected static String createToken(String organizationId) {
try {
String userName = "test-user";
JsonObjectBuilder claims = Json.createObjectBuilder();
claims.add(Claims.preferred_username.name(), userName);
claims.add(Claims.sub.name(), userName);
claims.add(CLAIMS_ORG_ID, organizationId);
PrivateKey privateKey = KeyUtils.generateKeyPair(2048).getPrivate();
return Jwt.claims(claims.build()).sign(privateKey);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
In our test we can use createToken method to create a token
@QuarkusTest
@TestHTTPEndpoint(ExampleRestController.class)
class ExampleRestControllerTenantTest extends AbstractTest {
@Test
void createNewThemeTest() {
var dto = given()
.when()
.contentType(APPLICATION_JSON)
.header(APM_HEADER_PARAM, createToken("org1"))
.body(themeDto)
.post()
.then()
.statusCode(CREATED.getStatusCode())
.extract()
.body().as(ExampleDTO.class);
}
}