Stirling-PDF/build.gradle
Ludy 3711d8d6b1
refactor(security): clean up SSO handling, harden URL parsing, and bump dependencies (#5523)
# Description of Changes

This PR performs a broad cleanup and refactor across the security, SSO,
and dependency layers to improve correctness, maintainability, and
robustness.

### What was changed

- **SSO / Authentication cleanup**
- Removed deprecated and ambiguous `SSO` authentication handling in
favor of explicit `OAUTH2` and `SAML2`.
- Introduced a centralized helper (`isSsoAuthenticationTypeByUsername`)
to consistently detect SSO-backed users.
- Hardened user creation logic to strictly validate authentication types
and reject invalid values.
- Updated OAuth2 and SAML2 authentication success handlers to use
unified SSO detection logic and clearer control flow.
  - Adjusted tests to reflect the new canonical authentication types.

- **Security & robustness improvements**
- Replaced direct `new URL(...)` usage with `URI.create(...).toURL()` to
avoid malformed URL edge cases.
- Hardened `Referer` parsing logic to safely handle invalid or host-less
URIs.
- Improved string comparison patterns (`"literal".equals(x)`) to avoid
potential `NullPointerException`s.

- **Controller and API cleanup**
- Removed large blocks of unused and legacy admin settings endpoints
from `SettingsController`.
- Updated OpenAPI annotations to use `requiredMode` instead of
deprecated `required`.

- **Dependency and build maintenance**
  - Updated Spring Boot from `3.5.7` to `3.5.9`.
- Updated multiple dependencies (Spring Security, Jackson, Micrometer,
Jetty, Hibernate, SnakeYAML, Springdoc, Swagger UI, etc.).
- Synced dependency versions in `3rdPartyLicenses.json` and removed
duplicate or obsolete entries.
- Modernized Gradle DSL usage (`url =`, `username =`,
`allowInsecureProtocol = true`).
- Ensured Spotless disabling applies consistently across all
subprojects.
  - Added `.build-cache` to `.gitignore`.

### Why the change was made

- To eliminate legacy and ambiguous SSO handling that could lead to
incorrect authentication decisions.
- To improve security and stability when dealing with user-controlled
URLs and headers.
- To reduce technical debt by removing unused controllers and deprecated
patterns.
- To keep dependencies up to date and aligned with the current Spring
Boot release.
- To improve overall code clarity, consistency, and long-term
maintainability.

---

This pull request contains dependency updates, minor code cleanups, and
some refactoring to improve maintainability and correctness. The most
significant change is the removal of all admin settings endpoints
(GET/POST) from the `SettingsController`, which impacts how application
settings can be managed via the API. Additionally, there are dependency
version bumps, minor improvements to static resource checks, and small
refactors in certificate download logic and Telegram bot service.

**Major API changes:**

* Removed all admin settings endpoints (general, security, connections,
privacy, advanced) from `SettingsController`, including both GET and
POST handlers for updating and retrieving settings. This eliminates the
ability to manage these settings via the API.

**Dependency updates:**

* Upgraded `snakeyaml-engine` from 2.10 to 3.0.1 and
`springdoc-openapi-starter-webmvc-ui` from 2.8.14 to 2.8.15 in
`build.gradle`.

**Refactoring and bug fixes:**

* Refactored static resource check in `RequestUriUtils.isStaticResource`
to use constant-first string comparison for better null safety and
clarity.
* Updated certificate download logic in `CertificateValidationService`
to use `URI.create(urlStr).toURL()` instead of `new URL(urlStr)` for
improved URL parsing and error handling.
[[1]](diffhunk://#diff-d2646f37bfd3e0963cbce16ab13edb52f2092795f54203b999dd82651154f26dL513-R514)
[[2]](diffhunk://#diff-d2646f37bfd3e0963cbce16ab13edb52f2092795f54203b999dd82651154f26dL703-R704)
* Refactored `TelegramPipelineBot` to consistently use
`telegramProperties.getBotToken()` instead of `getBotToken()`, and
removed the `getBotToken()` method override.
[[1]](diffhunk://#diff-a2466b92f58750ea37960cd1533e3194d9ecc3b4ef5ad7b64a017ee0e636ad93L85-R85)
[[2]](diffhunk://#diff-a2466b92f58750ea37960cd1533e3194d9ecc3b4ef5ad7b64a017ee0e636ad93L395-R395)
[[3]](diffhunk://#diff-a2466b92f58750ea37960cd1533e3194d9ecc3b4ef5ad7b64a017ee0e636ad93L519-L523)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-22 19:39:34 +00:00

540 lines
18 KiB
Groovy

plugins {
id "java"
id "jacoco"
id "io.spring.dependency-management" version "1.1.7"
id "org.springframework.boot" version "3.5.9"
id "org.springdoc.openapi-gradle-plugin" version "1.9.0"
id "io.swagger.swaggerhub" version "1.3.2"
id "com.diffplug.spotless" version "8.1.0"
id "com.github.jk1.dependency-license-report" version "3.0.1"
//id "nebula.lint" version "19.0.3"
id "org.sonarqube" version "7.2.2.6593"
}
import com.github.jk1.license.render.*
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.xml.XmlSlurper
import org.gradle.api.tasks.testing.Test
ext {
springBootVersion = "3.5.9"
pdfboxVersion = "3.0.6"
imageioVersion = "3.13.0"
lombokVersion = "1.18.42"
bouncycastleVersion = "1.83"
springSecuritySamlVersion = "6.5.6"
openSamlVersion = "4.3.2"
commonmarkVersion = "0.27.0"
googleJavaFormatVersion = "1.28.0"
logback = "1.5.25"
junitPlatformVersion = "1.12.2"
}
ext.isSecurityDisabled = { ->
System.getenv('DOCKER_ENABLE_SECURITY') == 'false' ||
System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true' ||
(project.hasProperty('DISABLE_ADDITIONAL_FEATURES') &&
System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')
}
ext.mavenUrl = System.getenv("MAVEN_PUBLIC_URL") ?: ""
ext.username = System.getenv('MAVEN_USER') ?: ""
ext.password = System.getenv('MAVEN_PASSWORD') ?: ""
if (rootProject.ext.mavenUrl.isEmpty()) {
println "No custom MAVEN_PUBLIC_URL set, defaulting to Maven Central"
} else {
println "MAVEN_PUBLIC_URL set"
}
jar {
enabled = false
manifest {
attributes "Implementation-Title": "Stirling-PDF",
"Implementation-Version": project.version
}
}
bootJar {
enabled = false
}
// Configure main class for the root project
springBoot {
mainClass = 'stirling.software.SPDF.SPDFApplication'
}
allprojects {
group = 'stirling.software'
version = '2.3.1'
configurations.configureEach {
exclude group: 'commons-logging', module: 'commons-logging'
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
}
}
def writeIfChanged(File targetFile, String newContent) {
if (targetFile.getText('UTF-8') != newContent) {
targetFile.write(newContent, 'UTF-8')
}
}
def updateTauriConfigVersion(String version) {
File tauriConfig = file('frontend/src-tauri/tauri.conf.json')
def parsed = new JsonSlurper().parse(tauriConfig)
parsed.version = version
def formatted = JsonOutput.prettyPrint(JsonOutput.toJson(parsed)) + System.lineSeparator()
writeIfChanged(tauriConfig, formatted)
}
def updateSimulationVersion(File fileToUpdate, String version) {
def content = fileToUpdate.getText('UTF-8')
def matcher = content =~ /(appVersion:\s*')([^']*)(')/
if (!matcher.find()) {
throw new GradleException("Could not locate appVersion in ${fileToUpdate} for synchronization")
}
def updatedContent = matcher.replaceFirst("${matcher.group(1)}${version}${matcher.group(3)}")
writeIfChanged(fileToUpdate, updatedContent)
}
def rootProjectRef = project
tasks.register('syncAppVersion') {
group = 'versioning'
description = 'Synchronizes app version across desktop and simulation configs.'
doLast {
def appVersion = rootProjectRef.version.toString()
println "Synchronizing application version to ${appVersion}"
updateTauriConfigVersion(appVersion)
[
'frontend/src/core/testing/serverExperienceSimulations.ts',
'frontend/src/proprietary/testing/serverExperienceSimulations.ts'
].each { path ->
updateSimulationVersion(file(path), appVersion)
}
}
}
tasks.register('writeVersion', WriteProperties) {
destinationFile = layout.projectDirectory.file('app/common/src/main/resources/version.properties')
println "Writing version.properties to ${destinationFile.get().asFile.path}"
comment = "${new Date()}"
property 'version', project.provider { project.version.toString() }
}
subprojects {
apply plugin: 'java'
apply plugin: 'java-library'
apply plugin: 'com.diffplug.spotless'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'jacoco'
java {
// 17 is lowest but we support and recommend 21
sourceCompatibility = JavaVersion.VERSION_17
}
if (project.name != "stirling-pdf") {
bootJar {
enabled = false
}
}
repositories {
if (!rootProject.ext.mavenUrl.isEmpty()) {
maven {
url = rootProject.ext.mavenUrl + '/releases'
credentials(PasswordCredentials) {
username = rootProject.ext.username
password = rootProject.ext.password
}
authentication {
basic(BasicAuthentication)
}
allowInsecureProtocol = true
}
}
maven { url = "https://build.shibboleth.net/maven/releases" }
mavenCentral()
}
configurations.configureEach {
exclude group: 'commons-logging', module: 'commons-logging'
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
// Exclude vulnerable BouncyCastle version used in tableau
exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on'
exclude group: 'org.bouncycastle', module: 'bcutil-jdk15on'
exclude group: 'org.bouncycastle', module: 'bcmail-jdk15on'
}
dependencyManagement {
imports {
mavenBom "org.springframework.boot:spring-boot-dependencies:$springBootVersion"
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.github.pixee:java-security-toolkit:1.2.2'
//tmp for security bumps
implementation "ch.qos.logback:logback-core:$logback"
implementation "ch.qos.logback:logback-classic:$logback"
compileOnly "org.projectlombok:lombok:$lombokVersion"
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
testImplementation platform("com.squareup.okhttp3:okhttp-bom:5.3.2")
testImplementation "com.squareup.okhttp3:mockwebserver"
}
tasks.withType(JavaCompile).configureEach {
options.encoding = "UTF-8"
if (!project.hasProperty("noSpotless")) {
dependsOn "spotlessApply"
}
}
tasks.named("compileJava", JavaCompile).configure {
options.compilerArgs.add("-parameters")
}
def jacocoReport = tasks.named("jacocoTestReport")
tasks.withType(Test).configureEach {
useJUnitPlatform()
finalizedBy(jacocoReport)
}
jacocoReport.configure {
dependsOn(tasks.named("test"))
reports {
xml.required.set(true)
csv.required.set(false)
html.required.set(true)
}
doLast {
def xmlReport = reports.xml.outputLocation.get().asFile
if (!xmlReport.exists()) {
logger.lifecycle("Jacoco coverage report not found at ${xmlReport}")
return
}
def xmlContent = xmlReport.getText("UTF-8")
xmlContent = xmlContent.replaceFirst('(?s)<!DOCTYPE.*?>', '')
def report = new XmlSlurper(false, false).parseText(xmlContent)
def counters = report.counter.collectEntries { counter ->
def type = counter.@type.text()
def covered = counter.@covered.text() as BigDecimal
def missed = counter.@missed.text() as BigDecimal
[(type): [covered: covered, missed: missed]]
}
def thresholds = [
LINE : 0.16,
INSTRUCTION: 0.14,
BRANCH : 0.09
]
def types = ["LINE", "INSTRUCTION", "BRANCH"]
def headers = ["Metric", "Coverage", "Covered/Total", "Status", "Target"]
def rows = types.collect { String type ->
def data = counters[type]
if (!data) {
return [type, "—", "—", "No data", ""]
}
def total = data.covered + data.missed
if (total == 0) {
return [type, "—", "0/${total.toBigInteger()}", "No executions", ""]
}
def ratio = data.covered / total * 100
def coverageText = String.format(Locale.ROOT, "%.2f%%", ratio)
def coveredText = String.format(Locale.ROOT, "%d/%d",
data.covered.toBigInteger(),
total.toBigInteger())
def threshold = thresholds[type]
def thresholdPercent = threshold != null ? threshold * 100 : null
def targetText = thresholdPercent != null ?
String.format(Locale.ROOT, ">= %.2f%%", thresholdPercent) : ""
def passed = thresholdPercent != null ? ratio >= thresholdPercent : null
def statusText = passed == null ? "" : (passed ? "PASS" : "FAIL")
return [type, coverageText, coveredText, statusText, targetText]
}
def columnIndexes = (0..<headers.size())
def columnWidths = columnIndexes.collect { idx ->
Math.max(headers[idx].length(), rows.collect { row ->
row[idx] != null ? row[idx].toString().length() : 0
}.max() ?: 0)
}
def formatRow = { List<String> values ->
columnIndexes.collect { idx ->
def value = values[idx] ?: ""
value.padRight(columnWidths[idx])
}.join(" | ")
}
def separator = columnIndexes.collect { idx ->
''.padRight(columnWidths[idx], '-')
}.join("-+-")
logger.lifecycle("")
logger.lifecycle("==== JaCoCo Coverage Summary ====")
logger.lifecycle(formatRow(headers))
logger.lifecycle(separator)
rows.each { row ->
logger.lifecycle(formatRow(row))
}
logger.lifecycle(separator)
def htmlReport = reports.html.outputLocation.get().asFile
logger.lifecycle("Detailed HTML report available at: ${htmlReport}")
if (rows.any { it[3] == "FAIL" }) {
logger.lifecycle("Some coverage targets were missed. Please review the detailed report above.")
} else if (rows.any { it[3] == "PASS" }) {
logger.lifecycle("Great job! All tracked coverage metrics meet their targets.")
}
logger.lifecycle("=================================\n")
}
}
tasks.named("build") {
dependsOn jacocoReport
}
jacocoTestCoverageVerification {
dependsOn jacocoReport
violationRules {
rule {
enabled = true
element = 'BUNDLE'
// Bytecode-Anweisungen abgedeckt
limit {
counter = 'INSTRUCTION'
value = 'COVEREDRATIO'
minimum = 0.14
}
// wie viele Quellcode-Zeilen abgedeckt
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.16
}
// Verzweigungen (if/else, switch) abgedeckt; misst Logik-Abdeckung
limit {
counter = 'BRANCH'
value = 'COVEREDRATIO'
minimum = 0.09
}
}
}
}
tasks.named("processResources") {
dependsOn(rootProject.tasks.writeVersion)
}
if (name == 'stirling-pdf') {
apply plugin: 'org.springdoc.openapi-gradle-plugin'
openApi {
apiDocsUrl = "http://localhost:8080/v1/api-docs"
outputDir = file("$projectDir")
outputFileName = "SwaggerDoc.json"
waitTimeInSeconds = 60 // Increase the wait time to 60 seconds
}
tasks.named("forkedSpringBootRun") {
dependsOn(":common:jar")
dependsOn(":proprietary:jar")
}
tasks.register("copySwaggerDoc", Copy) {
doNotTrackState("Writes SwaggerDoc.json to project root")
from(layout.projectDirectory.file("SwaggerDoc.json"))
into(rootProject.projectDir)
dependsOn("generateOpenApiDocs")
}
tasks.register("cleanSwaggerInBuild", Delete) {
doNotTrackState("Cleans up SwaggerDoc.json in build directory")
delete(layout.projectDirectory.file("SwaggerDoc.json"))
dependsOn("copySwaggerDoc")
}
tasks.named("copySwaggerDoc") {
finalizedBy("cleanSwaggerInBuild")
}
tasks.named("generateOpenApiDocs") {
finalizedBy("copySwaggerDoc")
doNotTrackState("OpenAPI plugin writes outside build directory")
}
}
}
tasks.withType(JavaCompile).configureEach {
options.encoding = "UTF-8"
if (!project.hasProperty("noSpotless")) {
dependsOn "spotlessApply"
}
}
gradle.taskGraph.whenReady { graph ->
if (project.hasProperty("noSpotless")) {
allprojects { scopedProject ->
scopedProject.tasks.matching { it.name.startsWith("spotless") }.configureEach {
enabled = false
}
}
}
}
def allProjects = ((subprojects as Set<Project>) + project) as Set<Project>
licenseReport {
projects = allProjects
renderers = [new JsonReportRenderer()]
allowedLicensesFile = project.layout.projectDirectory.file("app/allowed-licenses.json").asFile
outputDir = project.layout.buildDirectory.dir("reports/dependency-license").get().asFile.path
configurations = [ "productionRuntimeClasspath", "runtimeClasspath" ]
}
// Configure the forked spring boot run task to properly delegate to the stirling-pdf module
tasks.named('forkedSpringBootRun') {
dependsOn ':stirling-pdf:bootRun'
doFirst {
println "Delegating forkedSpringBootRun to :stirling-pdf:bootRun"
}
}
spotless {
yaml {
target '*.yml', '*.yaml'
trimTrailingWhitespace()
leadingTabsToSpaces()
endWithNewline()
}
format 'gradle', {
target 'build.gradle', 'settings.gradle', 'gradle/*.gradle', 'gradle/**/*.gradle'
trimTrailingWhitespace()
leadingTabsToSpaces()
endWithNewline()
}
}
sonar {
properties {
property "sonar.projectKey", "Stirling-Tools_Stirling-PDF"
property "sonar.organization", "stirling-tools"
property "sonar.exclusions", "**/build-wrapper-dump.json, **/src/main/java/org/apache/**, **/src/main/resources/static/pdfjs/**, **/src/main/resources/static/pdfjs-legacy/**, **/src/main/resources/static/js/thirdParty/**"
property "sonar.coverage.exclusions", "**/src/main/java/org/apache/**, **/src/main/resources/static/pdfjs/**, **/src/main/resources/static/pdfjs-legacy/**, **/src/main/resources/static/js/thirdParty/**"
property "sonar.cpd.exclusions", "**/src/main/java/org/apache/**, **/src/main/resources/static/pdfjs/**, **/src/main/resources/static/pdfjs-legacy/**, **/src/main/resources/static/js/thirdParty/**"
}
}
swaggerhubUpload {
// dependsOn = generateOpenApiDocs // Depends on your task generating Swagger docs
api = "Stirling-PDF" // The name of your API on SwaggerHub
owner = "${System.getenv().getOrDefault('SWAGGERHUB_USER', 'Frooodle')}" // Your SwaggerHub username (or organization name)
version = project.version // The version of your API
inputFile = file("SwaggerDoc.json") // The path to your Swagger docs
token = "${System.getenv("SWAGGERHUB_API_KEY")}" // Your SwaggerHub API key, passed as an environment variable
oas = "3.0.0" // The version of the OpenAPI Specification you"re using
}
dependencies {
implementation project(':stirling-pdf')
implementation project(':common')
if (rootProject.ext.isSecurityDisabled()) {
implementation project(':proprietary')
}
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion"
testImplementation platform("com.squareup.okhttp3:okhttp-bom:5.3.2")
testImplementation "com.squareup.okhttp3:mockwebserver"
}
tasks.named("test") {
useJUnitPlatform()
}
// Make sure all relevant processes depend on writeVersion
processResources.dependsOn(writeVersion)
tasks.register('printVersion') {
doLast {
println project.version
}
}
tasks.named('bootRun') {
group = 'application'
description = 'Delegates to :stirling-pdf:bootRun'
dependsOn ':stirling-pdf:bootRun'
doFirst {
println "Delegating to :stirling-pdf:bootRun"
}
}
tasks.named('build') {
group = 'build'
description = 'Delegates to :stirling-pdf:bootJar'
dependsOn ':stirling-pdf:bootJar', 'buildRestartHelper', 'syncAppVersion'
doFirst {
println "Delegating to :stirling-pdf:bootJar"
}
}
// Task to compile RestartHelper.java
tasks.register('compileRestartHelper', JavaCompile) {
group = 'build'
description = 'Compiles the RestartHelper utility'
source = fileTree(dir: 'scripts', include: 'RestartHelper.java')
classpath = files()
destinationDirectory = file("${buildDir}/restart-helper-classes")
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
// Task to create restart-helper.jar
tasks.register('buildRestartHelper', Jar) {
group = 'build'
description = 'Builds the restart-helper.jar'
dependsOn 'compileRestartHelper'
from "${buildDir}/restart-helper-classes"
archiveFileName = 'restart-helper.jar'
destinationDirectory = file("${buildDir}/libs")
manifest {
attributes 'Main-Class': 'RestartHelper'
}
doLast {
println "restart-helper.jar created at: ${destinationDirectory.get()}/restart-helper.jar"
}
}