Skip to content
ligojPublic

About

Modular REST+Front-End template with convention over naming

Resources

Stars

6 stars

Watchers

3 watching

Forks

Latest commit

Β 

History

2,289 Commits

Folders and files

Repository files navigation

πŸ”— Ligoj Bootstrap Maven Central

REST back-end template with many integrated components and enterprise features: RBAC, MFA, cache, plug-ins

Coverage Quality Gate Codacy Badge CodeFactor Known Vulnerabilities License

Key features:

  • Convention over code: HTTP error codes, Spring XML wiring by name, JAX-RS named parameters
  • RBAC with URL based authorization and dynamic roles, API tokens
  • Multi-factor authentication: TOTP authenticator applications and passkeys (WebAuthn)
  • Exception to REST/HTTP code mapping
  • Advanced JAX-RS validation
  • Tuned Jackson configuration for minified payload and validation
  • Test powered with CSV data load to/from JPA entities
  • TDD ready with pre-built asserts with Mockito and WireMock, target is 100% code coverage
  • Optional encrypted properties support and database configuration with Jasypt
  • Plug-in class loader with optional JAR signature verification
  • Spring-Data extensions for performance, minimal code and exception handling

A Spring based REST architecture

  • Spring Security/Web/Data
  • Apache CXF (JAX-RS)
  • JPA / Hibernate
  • Hibernate Validator for JPA and JAX-RS
  • Hazelcast / JCache
  • JUnit, WireMock

Requirements

  • Java 25
  • Maven 3.9.16

Overview

Ligoj Bootstrap (org.ligoj.bootstrap) is a Spring + Apache CXF JAX-RS architecture template consumed by downstream Ligoj projects as libraries and parent POMs. The Maven parent chain goes module β†’ parent/pom.xml β†’ org.ligoj.parent:project (external repository): build profiles and plugin defaults live there, dependency versions live in parent/pom.xml.

Commands

# Build everything (unit + integration tests)
mvn clean package

# All features (w/o deployment)
mvn clean package -Psources,javadoc,github,sonatype,jacoco,test

# Compile modules quickly (with their dependencies), tests included
mvn -pl bootstrap-core,bootstrap-business -am test-compile

# Tests of one module
mvn test -pl bootstrap-core

# Single test class
mvn test -pl bootstrap-core -Dtest=PluginsClassLoaderTest

# Coverage (target is 100%)
mvn clean package -Pjacoco -Djacoco.includes="org.ligoj.bootstrap.*"

# Check dependency updates
mvn versions:display-dependency-updates -Pjacoco -Dmaven.version.ignore="(?i)^(.*[.-](alpha|beta|rc|M|B|RC|pre|CR|jdk5)[.-]?[0-9]*|[0-9]{8}.*)$"

Test failures do NOT fail the build: the external parent POM (org.ligoj.parent:project) sets testFailureIgnore=true for both surefire and failsafe. BUILD SUCCESS is meaningless for tests β€” always check the Tests run: … Failures: … Errors: … lines or target/surefire-reports/.

Test naming splits unit vs integration: *Test = unit/Spring test, *IT = integration test that boots the real application on Jetty (port 6380). bootstrap-business runs them as two surefire executions via UTSuite/ITSuite (class-name regex suites); elsewhere failsafe runs *IT only with -Pit.

.mvn/jvm.config passes --sun-misc-unsafe-memory-access=allow β€” required for the build JVM.

Module map

Dependency layering: bootstrap-core β†’ bootstrap-business, plus bootstrap-core β†’ bootstrap-business-test (test scope dependency of bootstrap-business).

  • bootstrap-core β€” foundation library, no Spring Boot/CXF server. Bean & JPA base classes (AbstractPersistable, AbstractAudited), system entities (org.ligoj.bootstrap.model.system.*, tables prefixed S_), CSV engine, plug-in SPI + PluginsClassLoader, Jasypt crypto, TOTP/WebAuthn helpers, Hibernate naming strategies, custom validators.
  • bootstrap-business β€” the REST/JPA runtime: CXF wiring, exception mappers, Jackson config, Spring Data extensions, RBAC filters, Hazelcast/JCache, REST resources: /system (server information, time zone) and /system/** (configuration, cache, security, user, user settings, API tokens, MFA, hooks, files, bench), /session.
  • bootstrap-business-test β€” test-support library (compile-scope deps): the Abstract*Test hierarchy, HSQLDB Spring contexts, RBAC CSV fixtures, and the embedded Jetty launcher http.server.Main used by *IT.
  • bootstrap-business-parent β€” pom-only parent consumed by downstream back-end projects (org.ligoj.app:app-api); it preconfigures dependencies and resource filtering.
  • parent β€” BOM and shared build configuration of all the modules above.

Architecture

Spring wiring is XML-first: contexts live at src/*/resources/META-INF/spring/*.xml and compose via <import resource="classpath*:/META-INF/spring/…"/> so plug-in JARs can contribute. core-context-common.xml holds the single component-scan (org.ligoj.bootstrap); default-autowire="byName" is used pervasively β€” bean/field names matter.

Package conventions (root is always org.ligoj.bootstrap): model.system = JPA entities, dao.system = Spring Data repositories, resource.system.<feature> = JAX-RS resources under /system/**, core.* = reusable technical code. One deliberate exception: org.eclipse.jetty.util.resource.VisibleCombinedResource in bootstrap-business-test (package-private Jetty API access).

Spring Data extensions: all repositories in org.ligoj get RestRepository methods (findOneExpected, deleteAllExpected, …) through RestRepositoryFactoryBean. jqGrid/DataTables JSON filters translate to Criteria via PaginationDao/DynamicSpecification; grid responses use TableItem/PaginationJson.

Exception β†’ HTTP mapping: @Provider mappers in bootstrap-business/.../core/resource/mapper/ (registered in rest-context-common.xml) serialize a uniform ServerError JSON body. E.g. BusinessException/ValidationJsonException β†’ 400, EntityNotFoundException β†’ 404, DataIntegrityViolationException β†’ 412, FailSafeExceptionMapper β†’ 500 with message stripped. @OnNullReturn404 turns a null JAX-RS return into 404.

Jackson: the single objectMapper bean is ObjectMapperTrim (NON_NULL, lower-cased enums, custom date/time (de)serializers registered in its internal BootstrapModule). The codebase is on Jackson 3 (tools.jackson.* packages) with jackson-annotations still 2.x β€” watch package names when touching JSON code.

RBAC: SystemAuthorization rows hold role + HTTP method (null for all methods) + URL regex (type API or UI). AuthorizingFilter (last Spring Security filter) matches request path+method against a JCache-cached structure built by AuthorizationResource. API patterns apply to the decoded path without the context path (such as rest/system/user), from its start: ^rest/system/ and rest/system/ are equivalent, and a pattern never matches in the middle of the path. A role holding the .* API pattern for all methods makes its users administrators (SecurityHelper.ADMIN authority, $admin). Role names starting with $ are reserved, and $admin is never granted by a role or a provider having this name. Administrators are required by API delegation and by the administrator endpoints, such as the settings of another user, checked in code and with @Secured(SecurityHelper.ADMIN). RbacUserDetailsService loads roles (cache user-details). Each node keeps a local snapshot of the authorizations and of the hooks, reloaded only when these cluster caches change, and the security caches are cleared again after the commit of the changing transaction. Authentication is pre-authenticated header based: a principal header (SM_UNIVERSALID in the test contexts) + x-api-key via ApiTokenAuthenticationFilter. The Spring Security context itself is provided by the consuming application, see Deployment security requirements.

MFA: MfaResource (/system/mfa) manages the devices of the current user β€” TOTP authenticator applications and passkeys β€” and verifies codes and assertions. Enforcing the second factor after the primary authentication is the front-end's responsibility; server-side code can also check MfaResource.getVerifiedDate(login) (also exposed as verifiedDate by GET /system/mfa): the last successful verification, kept in the cluster-wide mfa-verified cache until the next POST /system/mfa/login or 12 hours (cache.mfa-verified.ttl). This state is per user, not per session. Passkeys require ligoj.mfa.rp-id (and optionally the accepted origins) to be set in production. Code verification (POST /system/mfa/verify, TOTP) is locked after ligoj.mfa.max-attempts consecutive attempts without success (default 5), counted atomically and shared by the cluster, until 15 minutes after the last attempt (cache.mfa-attempts.ttl). Passkey assertions are not counted: they cannot be guessed. Passkey challenges are single-use and shared by the cluster for 5 minutes (mfa-challenges cache).

Plug-in system: PluginsClassLoader scans ${ligoj.home}/plugins/*.jar (default ~/.ligoj/plugins), keeps only the newest version per artifact, and loads classes child-first so a plug-in JAR overrides the same plug-in bundled in the application. ligoj.plugin.enabled=false disables the external plug-ins (safe mode). JAR signatures are verified against a truststore of trusted vendor certificates, ligoj.plugin.signature.truststore (default ${ligoj.home}/plugin-vendors.p12, password ligoj.plugin.signature.truststore.password, default changeit): every content entry must be signed, and the signer certificate must be intended for code signing (not a CA nor a TLS-only certificate) and be pinned in or chain to the truststore to be VERIFIED. With ligoj.plugin.signature.required=true, only the VERIFIED plug-ins are loaded, and none without a usable truststore (fail closed). These are system properties. Plug-ins implement FeaturePlugin (key format a:b:c); their installation lifecycle is driven by the consuming application, this project only provides the SPI, the class loader and the SystemPlugin entity.

Configuration/crypto: values may be Jasypt-encrypted as ENC(...). The password is the first defined among: the app.crypto.password system property, the same key in the loaded properties files, the APP_CRYPTO_PASSWORD environment variable; then the content of the file named by app.crypto.file (system property, properties files) or APP_CRYPTO_FILE, also looked up in the classpath. See core-context-common.xml and the configuration reference.

Deployment security requirements

ApiTokenAuthenticationFilter authenticates requests from headers:

Request headers Authenticated user
principal header + x-api-key the principal, when the token belongs to this user
principal header + x-api-key + x-api-via-user the principal, when the token belongs to the x-api-via-user user, and this user is an administrator (delegation)
principal header only the principal, without any check

The last row is the SSO mode: an authenticating reverse proxy sets the principal header after its own authentication. It is only safe when:

  • the application port is never reachable without going through the proxy (private interface or network, firewall);
  • the proxy removes or overwrites the principal header of every client request, so it cannot be forged;
  • the proxy removes the x-api-via-user and x-api-local-roles client headers, unless API delegation is used.

Otherwise, any client sending SM_UNIVERSALID: admin is authenticated as admin. These requirements apply to every application built on this project.

The consuming application should also enable the method security (<security:method-security secured-enabled="true"/>) for the @Secured annotations; the administrator endpoints of this project also check the administrator authority in code.

Configuration reference

Three sources, depending on the key:

  • Runtime keys are read by ConfigurationResource: the Spring Environment (system properties, environment variables, and the Spring Boot property sources of the consuming application), then the S_CONFIGURATION table, managed with /system/configuration.
  • Placeholder keys (${...} in the Spring XML contexts and @Value) also come from admin.confidential.properties, maven-buildinfo.properties and application${app-env}.properties in the classpath, loaded by GlobalPropertyUtils.
  • System keys are JVM system properties only.
Key Default Source Usage
app-env empty system Suffix of the application${app-env}.properties file
app.crypto.password, app.crypto.file none system, properties, env (APP_CRYPTO_PASSWORD, APP_CRYPTO_FILE) Jasypt password, see above
ligoj.home ~/.ligoj system Home directory, plug-ins in plugins/
ligoj.plugin.enabled true system false: safe mode, no external plug-in
ligoj.plugin.signature.required false system Only the VERIFIED plug-ins are loaded
ligoj.plugin.signature.truststore ${ligoj.home}/plugin-vendors.p12 system Trusted vendor certificates (PKCS12, or JKS by extension)
ligoj.plugin.signature.truststore.password changeit system Truststore password
ligoj.mfa.issuer Ligoj runtime Issuer shown by authenticator applications, passkey relying party name
ligoj.mfa.rp-id localhost runtime Passkey relying party identifier: must be set in production, never changed afterwards
ligoj.mfa.origins empty: HTTPS origins of the rp-id host and sub-domains (HTTP on localhost) runtime Comma separated accepted passkey origins
ligoj.mfa.max-attempts 5 runtime Code verification attempts before the lock
ligoj.hook.path ^$ (none) runtime Comma separated regular expressions of the allowed hook commands
ligoj.hook.timeout 30 (seconds) runtime, system Process timeout of the hooks defining a zero timeout (a hook without timeout gets 10 seconds); the process is destroyed at the timeout
ligoj.file.path ^$ (none) runtime Comma separated regular expressions of the allowed /system/file paths, matched against the canonical path
cache.<name>.ttl per cache, see below Environment, at startup Cache entry lifetime in seconds, -1 for eternal
cache.<name>.cron none Environment, at startup Fixed expiry times of the cache entries, as a Spring CRON expression in the JVM time zone, such as 0 0 3 * * * for every day at 03:00: an entry expires at the first time after its creation or update (CronExpiryPolicy). Takes precedence over cache.<name>.ttl; an invalid expression is logged and the TTL applies
cache.location classpath:META-INF/hazelcast-local.xml placeholder Hazelcast configuration
hazelcast.statistics.enable false placeholder Cache statistics
security.filter.methods GET,POST,DELETE,PUT placeholder HTTP methods granted by an authorization without method
pagination.max-size 1000 placeholder Maximal page size a client can request; larger, zero or negative sizes are replaced by it
api.token.secret built-in value: override it placeholder Secret of the API token encryption
api.token.length, api.token.iterations, api.token.digest, api.token.crypt 128, 31, SHA-512, DESede placeholder API token generation, hash and encryption
api.token.purge 0 0 4 * * ? placeholder Cron of the expired API tokens purge
ligoj.sslVerify constructor argument system false disables the TLS checks of all the CurlProcessor instances
https.proxyHost, https.proxyPort constructor arguments, port 8080 system Proxy of the CurlProcessor instances
jdbc.driverClassName, jdbc.url, jdbc.username, jdbc.password, jdbc.maxPoolSize required placeholder Data source (jpa-context.xml)
jdbc.validationQuery SELECT 1 placeholder Connection validation
jpa.dialect org.ligoj.bootstrap.core.dao.MySQL5InnoDBUtf8Dialect placeholder Hibernate dialect
jpa.hbm2ddl, jpa.showSql, jpa.schema, jpa.log_queries_slower_than_ms, jpa.generate_statistics none, false, empty placeholder Hibernate settings
spring.data.jpa.repositories.bootstrap-mode lazy placeholder Spring Data repositories initialization
ligoj.name Ligoj placeholder Application name of the session settings

Caches (Hazelcast JCache, shared by the cluster), with their default lifetime: authorizations, hooks, configuration (eternal, cleared on change), user-details (1 hour), api-tokens (valid API token checks, 1 minute, cleared on any token change), mfa-attempts (15 minutes after the last attempt), mfa-verified (12 hours, cleared by a new login), mfa-challenges (5 minutes). A change of the database not made through the resources of this project is seen at the end of these lifetimes.

Testing

Extend the chain in bootstrap-business-test (org.ligoj.bootstrap package): AbstractTest β†’ AbstractDataGeneratorTest β†’ AbstractSecurityTest β†’ AbstractJpaTest β†’ AbstractAppTest β†’ AbstractServerTest (WireMock on port 8120). AbstractRestTest boots the real server for *IT. Canonical Spring test setup (see AbstractBootTest in bootstrap-business test sources):

@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "classpath:/META-INF/spring/application-context-test.xml")
@Rollback @Transactional

application-context-test.xml is not shipped by bootstrap-business-test: each module and downstream project provides its own, importing the shipped jpa-context-test.xml, business-context-test.xml and rest-context-web.xml.

  • DB is in-memory HSQLDB (jpa-context-test.xml), which imports the production jpa-context-common.xml β€” repositories/auditing behave as in production. Persistence unit name is always pu.
  • Test data loads from CSV via CsvForJpa: persistEntities(SystemRole.class, "csv/system-test/role.csv") for one file, or persistEntities("csv/root", Class...) which looks for <simplename-lowercase>.csv then <simple-name-kebab-case>.csv (e.g. system-role.csv). Header row = property names, foreign keys as dotted paths (role.name, user.login! to look the key up in the database instead of the already loaded rows).
  • AbstractAppTest.persistSystemEntities() seeds the RBAC tables; default principal is junit (initSpringSecurityContext(...) to switch).
  • src/main/resources and src/test/resources are Maven-filtered in bootstrap-business and downstream β€” literal ${...} in resources will be substituted at build time.
  • MatcherUtil asserts field/rule inside ValidationJsonException; AbstractBusinessEntityTest covers entity equals/hashCode reflectively.

About

Modular REST+Front-End template with convention over naming

Resources

Stars

6 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages